Go 语言基础语法

走进 Go 语言基础语法

时间处理

时间相关的函数,以及时间的格式化

now := time.Now()
fmt.Println(t)
fmt.Println(t.Format("2006-01-02 15:04:05"))

数字解析

将字符串转成int:strconv.ParseInt(字符串,进制,精度位数) 或者 strconv.Atoi(字符串)(自动识别)

func main() {
n, _ := strconv.ParseInt("111", 10, 64)
fmt.Println(n) // 111

n2, _ := strconv.Atoi("123")
fmt.Println(n2) // 123
}

进程信息

当我们在命令行中输入 go run example/20-env/main.go a b c d 时,os.Args可以获取到输入的参数 a b c d。

fmt.Println(os.Getenv(“PATH”))打印了环境变量 PATH 的值,它通常包含了一系列目录,用于指定在执行命令时搜索可执行文件的路径。

fmt.Println(os.Setenv(“AA”, “BB”))设置了一个名为 AA 的环境变量,其值为 BB。如果设置成功,它不会返回任何错误,即返回 nil。如果设置失败,比如内存不足或者其他系统错误,它会返回一个非 nil 的错误对象。

使用 exec.Command 函数执行了一个外部命令,即使用 grep 命令在 /etc/hosts 文件中搜索包含 127.0.0.1 的行。如果命令执行成功,它会打印出搜索到的行;如果执行失败,它会抛出一个 panic。

func main() {
fmt.Println(os.Args)
fmt.Println(os.Getenv("PATH"))
fmt.Println(os.Setenv("AA", "BB")) //nil
fmt.Println(os.Getenv("AA")) //BB

buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
if err != nil {
panic(err)
}
fmt.Println(string(buf)) // 127.0.0.1 localhost
}

JSON处理

序列化和反序列化

type userInfo struct {
Name string
Age int `json:"age"`
Hobby []string
}
a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}

//序列化
buf, err := json.Marshal(a)

//反序列化
err = json.Unmarshal(buf, &b)

小技巧:鼠标悬浮在函数上,可以看到链接,可以跳转过去,非常的方便

Go 语言的实战案例

猜谜游戏

猜谜游戏是一个很简单的小游戏,但是我们可以将这个小游戏才往下拆分成更小的模块,这也是重要的编程思想——大事化小,小事化了

猜谜游戏可以再拆分成随机数模块输入模块比较模块

先是随机数模块

直接使用rand.Intn(maxNum)会是伪随机数,一般情况下需要使用每次运行的时间戳初始化种子,但是没有设置种子的实际运行下每次运行的数都不一样,搜索得知在 Go 1.20 及更高版本中,程序会默认使用一个隐式的随机数生成器,可能会导致每次运行时生成不同的随机数。这是因为默认的随机数生成器会基于时间等因素来生成随机数,也就是说,在高版本中,优化了这个问题。

maxNum := 100
secretNumber := rand.Intn(maxNum)
fmt.Println("The secret number is ", secretNumber)

rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
fmt.Println("The secret number is ", secretNumber)

接下来是输入模块

这里采用的是bufio包,进行输入的处理

fmt.Println("Please input your guess")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
return
}
input = strings.Trim(input, "\r\n")

不过明显直接使用Scan明明简洁多了,这种方法和直接Scan,毕竟两者的复杂性差太多了

后面有解释道这个包以后会用到,所以就采用相对曲折的方式,想想也是,曲折说明比较底层,相对来说可以自定义的空间就大,可适用场景就多

比较模块什么说的

在线词典

在线词典拆解一下,就是调用APIJSON格式化提取信息

调用第三方API

输入:单词 ——-> 输出:音标词义

需要了解request和response的结构

HTTP请求的结构:请求行、请求头、请求体

JSON的部分属于请求体的模块

在页面中输入单词进行翻译,在开发者工具中就可以看到新的request,找到它(注意是POST)

{
trans_type: "en2zh", //英译中
source: "good",
user_id: "xxxxxxxxxxxxxxxxxxx"
}

再看response,可以看到JSON结构,直接在开发者工具中Copy-Copy as cURL(cmd),将复制下来的东西粘贴到终端中运行就可以得到网站的返回JSON

之后找一个代码自动生成的网站,将之前复制下来的东西直接丢进去,它就会自动生成响应的代码,粘贴到编译器里,就可以访问网站返回json数据

JSON格式化

request和response在收发过程中需要JSON序列化和反序列化

序列化是将一个数据结构(如结构体、对象、数组等)转换为字符串(通常是 JSON 格式)的过程。这使得数据能够以文本形式进行存储或传输。

JSON格式化没有想象中那么复杂

那么对应的API文档一个是怎么样的?

API 文档示例

关于API,这里想要展开一下

  1. 概述

该 API 提供了一个词典查询服务,可以将英文单词翻译成中文并返回相关信息。

  1. 基本信息
  • API Endpoint: POST https://api.interpreter.caiyunai.com/v1/dict
  • 请求格式: JSON
  • 响应格式: JSON
  1. 请求参数

请求体需要为 JSON 格式,包含以下字段:

字段 类型 必填 描述
trans_type string 翻译类型,固定值为 "en2zh"
source string 待翻译的英文单词
user_id string 用户 ID,用于标识请求用户

示例请求体:

{
"trans_type": "en2zh",
"source": "good",
"user_id": "xxxxxxxxxxxxxxxxxxx"
}
  1. 请求头

需要设置以下请求头:

请求头 类型 描述
Content-Type string 请求体类型,固定为 application/json;charset=UTF-8
User-Agent string 客户端信息
X-Authorization string 访问令牌
Accept string 接受的响应格式,通常为 application/json
  1. 响应参数

响应体为 JSON 格式,包含以下字段:

字段 类型 描述
rc int 响应状态码,0表示成功
wiki object 相关的维基信息,包括翻译描述和图片链接
dictionary object 包含单词发音、解释、同义词、反义词等信息

提取信息

对于接收到的数据,我们需要从中提取出我们想要的数据,在这里就是单词的音标和释义

首先,我们定义一个与 JSON 数据结构匹配的 Go 结构体,该结构体的字段应该与 JSON 中的键相对应,并且要使用 JSON 标签来指定映射关系。

可以直接访问结构体的字段来提取所需的信息

fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)

到这里我们已经实现了调用第三方API实现翻译的功能

即输入单词,输出单词音标和释义

go run 文件路径 beautiful
beautiful UK: [ˈbjuːtəful] US: [ˈbjutəfəl]
a.美丽的;优美的;完美的;完善的;美好的;高明的;很棒的
n.美丽的东西;美人

代理服务器

—SOCKS5

SOCKS5原理:协商、认证、请求、relay 四个阶段

协商:浏览器向代理说明自己支持的认证方式,然后代理从中选择一个自己支持的返回给浏览器

认证:(跳过)因为此处实现不加密的代理

请求:浏览器向代理发送collection请求,命令代理和某个IP端口建立连接

relay:浏览器正常发送请求,代理转发请求给真正服务器,服务器返回响应,代理也转发响应给浏览器,在转发过程中代理不注意流量的细节

TCP echo server

先做一个简单的 TCP echo server

客户端向服务器发送什么,服务器都会返回一样的数据

显示监听自己的本机端口,看看有没有客户端访问,如果有就存储在client里,给每个访问的客户端都分配一个单独的进程(或者不叫进程),总之这里应该涉及并发,也是能够体现Go语言特性和优势的地方,这里是一个典型的S/C模式

讲到这里会启动一个go-routine

server, err := net.Listen("tcp", "127.0.0.1:1080") 
if err != nil {
panic(err)
}
for {
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client)
}

那么goroutine是什么?

简单来说,goroutine是Go特色一种智能管理的、方便简洁的协程,也是Go天然支持高并发的主要条件

接下来就是逐步实现SOCKS5的四个阶段 协商、认证、请求、relay

协商+认证阶段

认证方法 authenticate(用户,程序)验证身份

func auth(reader *bufio.Reader, conn net.Conn) (err error)

简单来说,就是客户端向服务器发出请求,某协议,支持的认证方式,然后服务器从中选择支持的认证方式

请求阶段

浏览器发送报文:版本、请求类型、ATYP目标地址类型、地址、目标端口

ATYP 的使用允许代理服务器灵活地处理不同类型的地址,因此在实现 SOCKS 代理或其他网络协议时,理解 ATYP 的含义非常重要。

relay阶段

代理服务器也已经跑通了,但是代理服务器的输出中的有个addr的输出有点在意,按理来说addr应该是目标地址,但是我换终端执行addr居然也会变,这简直就像是addr是终端的地址一样,但是这不太对。

于是我打算进行调试,既然要调试,那么就把项目代码拷贝到本地,我认为这是必要的一步

最后在调试中发现代理接收到的ATYP(目标服务器地址类型)是IPv4,而不是域名,但是我发送的命令是

curl --socks5 127.0.0.1:1080 -v http://qq.com

也就是说客户端在curl中就把域名解析成IP地址了,应该是使用的本地DNS服务器

而为什么IP地址会不断变化呢,而且是随着终端的终端的变化而变化,为了验证以上前提,先进行一步检验

本地的不同终端似乎没有出现这种现象,只有云端的会出现

Q、不同终端访问qq.com网址使用的IP会不同吗?

A:是的,不同终端访问同一个网址(如 qq.com)时,使用的 IP 地址可能会不同。这主要受到以下几个因素的影响:

  1. DNS 解析:当你访问一个网址时,首先会通过 DNS(域名系统)将域名解析为 IP 地址。不同的 DNS 服务器可能返回不同的 IP 地址,尤其是对于大型网站,它们可能有多个服务器并采用负载均衡技术。
  2. 地理位置:某些网站可能会根据用户的地理位置返回不同的 IP 地址,以便提供更快的访问速度或更好的内容分发。例如,访问 qq.com 的用户如果在中国大陆,可能会解析到与在海外用户不同的 IP 地址。
  3. 网络环境:不同的网络环境(如家庭 Wi-Fi、移动网络等)可能使用不同的出口 IP 地址。在同一个网络下,不同的设备通常会共享同一个公网 IP 地址,但如果网络使用了 NAT(网络地址转换),内部设备的访问可能被不同的网络策略影响。
  4. 动态 IP 地址:某些 Internet 服务提供商(ISP)使用动态 IP 地址分配,因此同一用户在不同时间连接互联网时,可能会获得不同的 IP 地址。

探究到这里告一段落,先把课程看完

课程的最后,正如前面所说,讲了如何配置proxy,就只需要安装一个 Proxy SwichyOmega 即可,在 Proxy SwichyOmega 中配置代理服务器的地址,也就是说讲浏览器连接到代理服务器,由代理服务器全权负责我们 访问目标服务器的动作

到这里感觉,代理,就是一台主机请另一台主机帮忙访问,相当于请同学用他自己的电脑帮我访问我想要访问的网站,他访问之后得到的数据给我,就这样而已,由于是他访问,所以那端的服务器当然看到访问的IP地址是同学的IP地址,不会知道我的存在,就是一个这么简单的过程