这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天
本堂课重点内容:
-
Golang 开发环境搭建
-
Golang 基础语法与标准库的讲解
-
通过实战案例进一步熟悉 Golang 的使用
- 猜谜游戏 - 一些标准库的使用和流程控制
- 在线词典 - 发送 http 请求,返回 http 响应、JSON 解析、生成 http 请求代码的技巧
- socks5 代理 - socks5 协议的实现
1.语言特点
- 高性能、高并发
- 语法简单、学习曲线平缓
- 丰富的标准库
- 完善的工具链
- 静态链接
- 快速编译
- 跨平台
- 垃圾回收
2.快速入门
2.1 开发环境
a.安装 Golang
- 访问 go.dev/ ,点击 Download ,下载对应平台安装包,安装即可
- 如果无法访问上述网址,可以改为访问 studygolang.com/dl 下载安装
- 如果访问 github 速度比较慢,建议配置 go mod proxy,参考 goproxy.cn/ 里面的描述配置,下载第三方依赖包的速度可以大大加快
b.配置集成开发环境
可以选择安装 VS Code , 或者 Goland ,对于 VS Code,需要安装 Go 插件
c.基于云的开发环境
gitpod.io/#github.com… 短链接:https:/hi-hi.cn/gitpod
2.2 基础语法与标准库
a.Hello World
编译和运行
// 直接运行
go run example/01-hello/main.go
// 编译成二进制再运行
go build example/01-hello/main.go
./main
第一行 package main 代表这个文件属于main包的一部分,main包也就是程序的入口包: 第三行导入了标准库里面的 fmt 包。这个包主要是用来往屏幕输入输出字符串、格式化字符串。
当我们要看函数的文档时,可以直接将鼠标悬浮在你的代码上,或者直接访问 pkg.go.dev ,后面加上你要查看的包名,就能看到这个包的在线文档
b.变量
go 语言的字符串是内置类型,可以直接通过加号拼接,也能够直接用等于号去比较两个字符串。
变量声明:(允许一次初始化多个变量)
1.标准初始化
var name string = ""
2.类型推导
var name = ""
3.冒号等于
name := ""
常量声明:
const name = ""
值在一提的是 go 语言里面的常量,它没有确定的类型,会根据使用的上下文来自动确定类型。
c.if else
if 条件判断没有括号,后面必须接大括号
d.循环
go 中没有 while 、do while 循环,只有 for 循环
for 后面什么都不写,代表一个死循环
可以使用经典的 C 循环,中间三段,任何一段部可以省略。
循环里面可以用 break 或者 continue 来跳出或者继续循环
e.switch
在switch后面的那个变量名,并不需要加括号。 这里有个很大的一点不同的是,在 c++ 里面,switch case 如果不加 break 的话会然后会继续往下跑完所有的 case ,在 go 里面的话是不需要加break. 相比C或者C+,go语言里面的switch功能更强大。可以使用任意的变量类型,甚至可以用来取代任意的 if else 语句。你可以在 switch 后面不加任何的变量,然后在case里面写条件分支。
f.数组
数组就是一个具有编号且长度固定的元素序列。
对于一个数组,可以很方便地取特定索引的值或者往特定索引取存储值,然后也能够直接去打印一个数组。不过,在真实业务代码里面,我们很少直接使用数组,因为它长度是固定的,我们用的更多的是切片。
g.切片
切片不同于数组可以任意更改长度,然后也有更多丰富的操作。比如说我们可以用 make 来创健一个切片,可以像数组一样去取值,使用 append 来追加元素。 注意 append 的用法的话,你必须把 append 的结果赋值为原数组。 因为 slice 的原理实际上是它有一个它存储了一个长度和一个容量,加一个指向一个数组的指针,在你执行 append 操作的时候,如果容量不够的话,会扩容并目返回新的 slice. slice 初始化的时候也可以指定长度。 sice拥有像 python 一样的切片操作,比如这个代表取出第二个到第五个位置的元素,不包括第五个元素。不过不同于 python,这里不支持负数索引
h.map
我们跟我们可以用 make 来创建一个空map,这里会需要两个类型,第一个是那个 key 的类型,这里是 string 另一个是value的类型,这里是 int。我们可以从里面去存储或者取出键值对。
可以用 delete 从里面删键值对。 golang 的 map 是完全无序的,遍历的时候不会按照字母顺序,也,不会按照插入顺序输出,而是随机顺序
i.range
对于一个 slice 或者一个 map 的话,我们可以用 range 来快速遍历,这样代码能够更加简洁。range 遍历的时候,对于数组会返回两个值,第一个是索引,第二个是对应位置的值。如果我们不需要索引的话,我们可以用下划线来忽略。
j.函数
这个是 Golang 里面一个简单的实现两个变量相加的函数。Golang 和其他很多语言不一样的是,变量类型是后置的.
Golang 里面的函数原生支持返回多个值。在实际的业务逻辑代码里面几乎所有的函数都返回两个值,第一个是真正的返回结果,第二个值是一个错误信息。
k.指针
go 里面也支持指针。当然,相比 C 和 C++ 里面的指针,支持的操作很有限。指针的一个主要用途就是对于传入参数进行修改 我们来看这个函数。这个函数试图把一个变量 +2。但是单纯像上面这种写法其实是无效的。因为传入函数的参数实际上是一个拷贝,那也说这个 +2,是对那个拷贝进行了 +2,并不起作用。如果我们需要起作用的话,那么我们需要把那个类型写成指针类型,那么为了类型匹配,调用的时候会加一个 & 符号。
l.结构体
结构体的话是带类型的字段的集合。 比如这里 user 结构包含了两个字段,name 和 password。我们可以用结构体的名称去初始化一个结构体变量,构造的时候需要传入每个字段的初始值。也可以用这种键值对的方式去指定初始值,这样可以只对一部分字设进行初始化。同样的结构体我们也能支持指针,这样就能够实现对于结构体的修改,也可以在某些情况下避免一些大结构体的拷贝开销。
m.结构体方法
在 Golang 里面可以为结构体去定义一些方法。会有一点类以其他语言里面的类成员函数。比如这里,我们把上面一个例子的 checkPassword 的实现,从一个普通函数,改成了结构体方法。这样用户可以像 a.checkPassword("xx”) 这样去调用。具体的代码修改,就是把第一个参数,加上括号,写到函数名称前面
在实现结构体的方法的时候地有两种写法,一种是带指针,一种是不带指针。这个它们的区别的话是说放如果你带指针的话,那你就可以对这个结构体去做修改。如果你不带指针的话,那你实际上操作的是一个拷贝,你就无法对结构体进行修改。
n.错误处理
错误处理在 go 语言里面符合语言习惯的做法就是使用一个单独的返回值来传递错误信息。 不同于Java 使用的异常。go 语言的处理方式,能够很清晰地知道哪个函数返回了错误,并且能用简单的 if else来处理错误 在函数里面,我们可以在那个函数的返回值类型里面,后面加一个 eror,就代表这个函数可能会返回错误 那么在函数实现的时候,return 需要同时 return 两个值,要么就是如果出现错误的话,那么可以 return nil和一个error。如果没有的话,那么返回原本的结果和 nil。
o.字符串操作
下面我们来看一下 go 语言里面的字符串操作。在标准库 strings 包里面有很多常用的字符串工具函数,
比如 contains 判断一个字符串里面是否有包含另一个字符串,count 字符串计数,index 查找某个字符串的位置。join 连接多个字符串,repeat 重复多个字符串,replace 替换字符串。
p.字符串格式化
字符串格式化。在标准库的 FMT 包里面有很多的字符串格式相关的方法,比如 printf 这个类以于 C 语言里面的 printf 函数。不同的是,在 go 语言里面的话,你可以很轻松地用 %v 来打印任意类型的变量,而不需要区分数字还是字符串。你也可以用 %+V 打印详细结果,%#v 则更详细。
q.JSON 处理
go 语言里面的 JSON 操作非常简单,对于一个已有的结构体,我们可以什么都不做,只要保证每个字段的第一个字母是大写,也就是是公开字段,能被外部包访问。那么这个结构体就能用 JSON.marshaler 去序列化,变成一个 JSON 的字符串。 序列化之后的字符串也能够用 JSON.unmarshaler 去反序列化到一个空的变量里面. 这样默认序列化出来的字符串的话,它的风格是大写字母开头,而不是下划线。我们可以在后面用 json tag 等语法来去修改输出 JSON 结果里面的字段名。
r.时间处理
在 go 语言里面最常用的就是 time.now() 来获取当前时间,然后你也可以用 time.date 去构造一个带时区的时间,构造完的时间。上面有很多方法来获取这个时间点的年月日小时分钟秒,然后也能用 .sub 去对两个时间进行减法,得到一个时间段。时间段又可以去得到它有多少小时,多少分钟、多少秒。 在和某些系统交互的时候,我们经常会用到时间戳。那您可以用 .UNIX 来获取时间戳。 可以用 time.format、time.parse 格式化和解析时间
s.数字解析
字符串和数字之间的转换。
在 go 语言当中,关于字符串和数字类型之间的转换都在 strconv 这个包下,这个包是 string convert 这两个单词的缩写。 我们可以用 parseInt 或者 parseFloat 来解析一个字符串。 我们可以用 Atoi 把一个十进制字符串转成数字。可以用 itoA 把数字转成字符串。
如果输入不合法,那么这些函数都会返回error
t.进程信息
在 go 里面,我们能够用 os.argv 来得到程序执行的时候的指定的命令行参数。 比如我们编译的一个二进制文件,command。后面接 abcd 来启动,输出就是 os.argv 会是一个长度为 5 的 slice 第一个成员代表二进制自身的名字。 我们可以用 os.getenv 来读取环境变量。 exec
3.实战案例
3.1 猜谜游戏
a.生成随机数
使用 math 下的 rand 包中的 func Intn(n int) int ,返回在 [0,n) 中的随机数,
但是当我们多次运行,得到是同一个随机数,这不是我们想要的结果
b.生成随机数种子
我们通常 时间戳 用做随机数种子,这样每次得到的随机数就不一样了。
c.读取用户输入
然后接下来我们需要实现用户输入输出,并理解析成数字。 每个程序执行的时候都会打开几个文件,stdin、stdout、stderr等,stdin文件可以用 os.Stdin 来得到。然后直接操作这个文件很不方便,我们会用 bufio.NewReader 把个文件转换成一个 reader 变量。reader 变量上会有很多用来操作一个流的操作,我们可以用它的 ReadString 方法来读取一行。如果失败了的话,我们会打印错误并退出。ReadString 返回的结果包含结尾的换行符,我们把它去掉,再转换成数字。如果转换失败,我们同样打印错误,退出。
d.实现判断逻辑
使用 if else 语句实现猜谜游戏的基本逻辑
e.实现游戏循环
使用 for 死循环,使游戏能够重复猜,猜错继续猜、直到猜对游戏才结束。
这样,一个完整的猜谜游戏就完成了。
3.2 在线词典
调用第三方 API 查询单词的翻译并打印出来,原理就是使用 go 语言来发送 HTTP 请求、解析 json ,还会学习如何使用代码生成来提高开发效率。
a.抓包
我们要用到的 API 以彩云科技提供的在线翻译为例。 先请打开彩云翻译的网页 fanyi.caiyunapp.com/ ,然后右键检查打开浏览器的开发者工具。
此时我们点一下翻译按钮,浏览器会发送一系列请求,我们能很轻松地找到那个用来查询单词的请求 这是一个 HTTP 的 post 的请求,请求的 header 的相当的复杂,有十来个。然后请求头是一个 json 里面有两个字段,一个是代表你要你是从什么语言转化成什么语言,source 就是你要查询的单词。API 的返回结果里面会有 Wiki 和 dictionary 两个字段。我们需要用的结果主要在 dictionary.explanations 字段里面。其他有些字段里面还包括音标等信息
b.代码生成
我们需要在 Golang 里面去发送这个请求。因为这个请求比较复杂,用代码构造很麻烦,实际上我们有一种非常简单的方式来生成代码,我们可以右键浏览器里面的 copy as curl 。copy 完成,之后大家可以在终端粘贴一下 curl 命令,应该可以成功返回一大串 json。
然后我们打开一个网站 curlconverter.com/go/ ,粘贴 curl 请求,在右边的语言里面选 Golang 就能够看到一串很长的代码,我们直接把它 copy 到我们的编辑器里面。有几个header 比较复杂,生成代码有转义导致的编译错误,删掉这几行即可。
c.生成代码解读
我们来看一下这生成的代码,首先第 12 行我们创建了一个 HTTP client,创建的时候可以指定很多参数,包括比如请求的超时是否使用 cookie 等。接下来是构造一个 HTTP 请求,这是一个 post 请求,然后会用到 HTTP.NewRequest,第一个参数是 http 方法 POST,第二个参数是 URL,最后一个参数是body,body 因为可能很大,为了支持流式发送,是一个只读流。我们用了 strings.NewReader 来把字符串转换成一个流。 这样我们就成功构造了一个 HTTP request,接下来我们需要对这个 HTTP request 来设置一堆 header。 接下来我们把我们调用 client.do request,就得到 response ,如果请求失败的话,那么这个 error 会返回非 nil,会打印错误并且退出进程。response 有它的 HTTP 状态码,response header和 body,body 同样是一个流,在 golang 里面,为了避免资源泄露,你需要加一个 defer 来手动关闭这个流,这个 defer 会在这个函数运行结束,之后去执行。接下来我们是用 ioutil.ReadAll (现在可直接用 io.ReadAll)来读取这个流,能得到整个body。我们再用 print 打印出来。
我们来运行生成的代码,能看到我们已经能够成功地发出请求,把返回的 JSON 打印出来。但是现在那个输入是固定的,我们是要从一个变量来输入,我们需要用到 JSON 序列化
d.生成 request body
我们初始化一个结构体用来存放 request payload ,然后再调用 JSON.marshaler 来得到这个序列化之后的字符串。
不同于之前这里是个字符串,我们这里是个字节数组。所以我们把 strings.newReader 改成 bytes.newreader 然后来构造 request 的 body 接下来代码不变。然后我们就能成功地用一个变量来发送 HTTP 请求。
e.解析 response body
接下来我们要做的是把这个 response body 来解析出来。
在 js/Python 这些脚本语言里面,body是一个字典或者 map 的结构,可以直接从里面取值。但是 golang 是个强类型语言,这种做法并不是最佳实践。 更常用的方式是和 request 的一样,写一个结构体,把返回的 JSON 反序列化到结构体里面。但是我们在浏览器里面可以看到这个 API 返回的结构非常复杂,如果要定义结构体字段,非常繁琐并且容易出错。
此时有一个小技巧的是,网上有对应的代码生成工具(oktools.net/json2go),我们可以打开这个网站,把 json 字符串粘贴进去,这样我们就能够生成对应结构体。在某些时刻,我们如果不需要对这个返回结果,做很多精细的操作,我们可以选择转换嵌套,上生成的代码更加紧凑。
这样我们就得到了一个 response 结构体。接下来我们修改代码,我们先定一个 response 结构体的对象,然后我们用 JSON.unmarshal 把 body 反序列化到这个结构体里面,再试图打印出来
现在我们再运行一下,这里打印的时候使用了 %#v ,这样可以让打印出来的结果比较容易读。我们现在离最终版本已经很近了,接下来我们需要修改代码为打印 response 里面的特定字段。
f.打印结果
观察那个 json 可以看出我们需要的结果是在 Dictionary.explanations. 我们用 for range 循环来迭代它,然后直接打印结构,参照一些词典的显示方式,我们可以在那个前面打印出这个单词和它的音标,这里有英式音标和美式音标。
g.完善代码
现在我们的程序的输入还是写死的。我们把代码的主体改成一个 query 函数,查询的单词作为参数传递进来。然后我们写一个简单的 main 函数,这个 main 函数首先判断一下命令和参数的个数,如果它不是两个,那 么我们就打印出错误信息,退出程序。否侧就获取到用户输入的单词,然后执行 qury 函数。
这样我们的命令行词典就完成了
3.3 SOCKS5 代理
a.socks5 代理介绍
我们来写一个 socks5 代理服务器,对于大家来说,一提到代理服务器,第一想到的是翻墙。不过很遗憾的是,socks5 协议它虽然是代理协议,但它并不能用来翻墙,它的协议都是明文传输。
这个协议历史比较久远,诞生于互联网早期。它的用途是,比如某些企业的内网为了确保安全性,有很严格的防火墙策略,但是带来的副作用就是访问某些资源会很麻烦。 socks5 相当于在防火墙开了个口子,让授权的用户可以通过单个端口去方问内部的所有资源。实际上很多翻墙软件,最终暴露的也是一个 socks5 协议的端口。如果有同学开发过爬虫的话,就知道,在爬取过程中很容易会遇到 IP 访问频率超过限制。这个时候很多人就会去网上找一些代理 IP 池,这些代理 IP 池里面的很多代理的协议就是 IP 。
我们先来看一下最终写完的代理服务器的效果。
我们启动这个程序,然后在浏览器里面配置使用这个代理,此时我们打开网页。代理服务器的日志,会打印出你访问的网站的域名或者 IP,这说明我们的网络流量是通过这个代理服务器的。我们也能在命令行去测试我们的代理服务器。我们可以用 curl --socks5 + 代理服务器地址,后面加一个可访问的 URL,如果代理服务器工作正常的话,那么 curl 命令就会正常返回。
b.socks5 代理原理
接下来我们来了解一下 socks5 协议的工作原理。正常浏览器访访问一个网站,如果不经过代理服务器的话,就是先和对方的网站建立TCP连接,然后三次握手,握手完之后发起 HTTP 请求,然后服务返回 HTTP 响应。
如果设置代理服务器之后,流程会变得复杂一些。
首先是浏览器和 socks5 代理建立 TCP 连接,代理再和真正的服务器建立 TCP 连接。这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、relay 阶段
第一个握手阶段,浏览器会向 socks5 代理发送请求,包的内容包活一个协议的版本号,还有支持的认证的种类,socks5 服务器会选中一个认证方式,返回给浏览器。如果返回的是 00 的话就代表不需要认证,返回其他类型的话会开始认证流程,这里我们就不对认证流程进行概述了。
第三个阶段是请求阶段,认证通过之后浏览器会 socks5 服务器发起请求。主要信息包括版本号,请求的类型,一般主要是 connection 请求,就代表代理服务器要和某个域名或者某个 IP 地址某个端口建立 TCP 连接。
代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。
第四个阶段是 relay 阶阶段。此时浏览器会发送正常发送请求,然后代理服务器接收到请求,之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边。
然后实际上代理服务器并不关心流量的细节,可以是 HTTP 流量,也可以是其它 TCP 流量。这个就是 socks5 协议的工作原理,接下来我们就会试图去简单地实现它。
c.TCP echo server
第一步,我们先在 go 里面写一个简单的 TCP echo server 。为了方便测试,server 的工作逻辑很简单,你给他发送啥,他就回复啥.
首先我们在 main 函数里面先用 net.listen 去监听一个端口,会返回一个 server,然后在一个死循环里面,每次去 accept 一个请求,成功就会返回一个连接。
接下来的话我们在一个process函数里面去处理这个连接。
注意这前面会有个 go 关键字,这个代表启动一个 goroutinue ,可以暂时类比为其他语言里面的启动一个子线程。只是这里的 goroutinue 的开销会比子线程要小很多,可以很轻松地处理上万的并发。
接下来是这个 process 函数的实现。首先第一步的话会先动加一个defer connection.close(),defer 是 Golan g里面的一个语法,这一行的含义就是代表在这个函数退出的时候要把这个连接关掉,否则会有资源的泄露。
接下来的话我们会用 bufio.NewReader 来创建一个带缓冲的只读流,这个在前面的猜迷游戏里面也有用到,带缓冲的流的作用是,可以减少底层系统调用的次数,比如这里为了方便是一个字节一个字节的读取,但是底层可能合并成几次大的读取操作。并且带缓冲的流会有更多的一些工具函数用来读取数据。
我们可以简单地调用那个 readByte 函数来读取单个字节,再把这一个字节写进进连接。
我们来简单测试一下我们的第一个 TCP 服务器,然后测试会需要用到 nc 命令,我们用 nc 127.0.0.1:1080,输入 hello 然后服务器就会给你返回 hello。
d.auth
就这样我们就已经完成了一个能的够返回你输入信息的一个 TCP server,接下来我们是要开始实现协议的第一步,认证阶段,从这一部分开始会变得比较复杂。
我们实现一个空的 auth 函数,在 process 函数里面调用,再来编写 auth 函数的代码。
我们回忆一下认证阶段的逻辑,首先第一步的话,浏览器会给代理服务器发送一个包,然后这个包有三个字段,
第一个字段,version 也就是协议版本号,固定是 5 第二个字段 methods ,认证的方法数目第三个字段每个 method 的编码,0 代表不需要认证,2 代表用户名密码认证
我们先用 readBytes 来把版本号读出来,然后如果版本号不是 sockes5 的话直接返回报错,接下来我们再读取 method size,也是一个字节。然后我们需要我们去 make 一个相应长度的一个 slice ,用 io.ReadFull把它去填充进去。
写到这里,我们把获取到的版本号和认证方式打印一下。
此时,代理服务器还需要返回一个 response ,返回包包括两个字段,一个是 version 一个是 method,也就是我们选中的鉴传方式,我们当前只准备实现不需要鉴传的方式,也就是 00。 我们用 curl 命令测试一下当前版本的效果
此时 curl 命令肯定是不成功的,因为我们的协议还没实现完成。但是我们看日志会发现,version 和 method 可以正常打印,说明当前我们的实现是正确的。
e.请求阶段
接下来我们开始做第三步,实现请求阶设,我们试图读取到携带 URL 或者 IP地址+端口的包,然后把它打印出来。我们实现一个和 auth 函数类似的 connect 函数,同样在 process 里面去调用。再来实现connect函数的代码.
我们来回忆一下请求阶段的逻辑。浏览器会发送一个包,包里面包含如下6个字段,
- 1.version 版本号,还是 5。
- 2.command,代表请求的类型,我们只支持 connection 请求,也就是让代理服务建立新的 TCP 连接。
- 3.RSV 保留字段,不理会。
- 4.atype 就是目标地址类型,可能是 IPV 4 IPV 6或者域名。
- 5.addr,这个地址的长度是根据 atype 的类型而不同的。
- 6.port 端口号,两个字节,我们需要逐个去读取这些字设。
前面这四个字段总共四个字节,我们可以一次性把它读出来。我们定义一个长度为 4 的 buffer 然后把它读满。读满之后,然后第0个、第1个、第3个、分别是version、cmd和 type。 version需要判断是 socket5,cmd需要判断是1。然后 atype,可能是 ipv4,ipv6,或者是 host。如果 IPV4 的话,我们再次读满这个 buffer,因为这个 buffer 长度刚好也是 4 个字节,然后逐个字节打印成 IP 地址的格式保存到 addr 变量。如果是个 host 的话,需要先读它的长度,再 make 一个相应长度的 buf 填充它。再转换成字符串保存到 addr 变量。IPV6 用得比较少,我们就暂时先不支持。
最后还有两个字节那个是 port,我们读取它,然后按协议规定的大端字节序转换成数字。由于上面的 bufer 已经不会被其他变量使用了,我们可以直接复用之前的内存,建立一个临时的 slice,长度是 2 用于读取,这样的话最多会只读两个字节回来。接下来我们把这个地址和端口打印出来用于调试。收到浏览器的这个请求包之后,我们需要返回一个包,这个包有很多字段,但其实大部分部不会使用。
- 第一个是版本号还是 socket5。
- 第二个就是返回的类型,这里是成功就返回 0。
- 第三个是保留字段,填 0 。
- 第四个 atype 地址类型,填 1。
- 第五个、第六个暂时用不到,都填成 0。
一共 4 + 4 + 2 个字节,后面 6 个字节都是 0 填充。
现在我们来测试一下当前阶段的成果,简单 curl 一下。
此时请求还是会失败,我们现在已经能看到正常打印出来访问的 IP 地址和端口,这说明我们当前的实现正常,这样我们就可以做最后一步,我们真正和这个端口建立连接,双向转发数据。
f.relay 阶段
我们直接用 net.dial 建立一个 TCP 连接。建立完连接之后,我们同样要加一个 defer 来关闭连接。 接下来需要建立浏览器和下游服务器的双向数据转发。 标准库的 io.copy 可以实现一个单向数据转发,双向转发的话,需要启动两个 goroutinue。
现在有一个问题,connect 函数会立刻返回,返回的时候连接就被关闭了。因此需要等待任意一个方向 copy 出错的时候,再返 connect函数。 这里可以使用到标准库里面的一个 context 机制,用 context.WithCancel 来创建一个 context。在最后等待 ctx.Done(),只要 cancel 被调用,ctx.Done 就会立刻返回。然后在上面的两个 goroutinue 里面调用一次 cancel 即可。
这样我们的代理服务器就终于完工了,我们来测试一下。执行 curl 命令,此时,终于返回了成功。
我们可以试着在浏览器里面再测试一下,在浏览器里面测试代理需要安装这个 switchomega 插件,然后里面新建一个情景模式,代理服务器选 socks5,端口1080,保存并启用。
此时你应该还能够正常地访问网站,代理服务器这边会显示出浏览器版本的域名和端口。
代码链接
GitHub 地址: github.com/wangkechun/…
个人总结
- Golang 语法简单易上手。
- Golang 特有的语法(比如说错误处理)需多多练习,养成习惯。
- 编写 socks5 代理时,socks5 协议的理解和使用有一定难度。