【Go语言入门】环境配置-基础语法-项目实战 | 青训营笔记

155 阅读16分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天,今天学习的内容是Go的环境配置,基础的语法学习以及三个实战项目,整理学习笔记如下。

1 安装及环境配置

1.1 安装Go语言

go官网下载页进行下载安装

image-20230114152110734.png

一路next即可

image-20230114152225963.png

image-20230114152410033.png

1.2 配置Go代理

根据Goproxy配置Go代理,以下两个方法都可

1.2.1 终端配置

 go env -w GOPROXY=https://goproxy.cn,direct

1.2.2 环境变量直接配置

image-20230114212658005.png

image-20230114212746861.png

1.3 VsCode配置Go

打开Vscode,安装go插件

image-20230114152859412.png

打开任意go文件,安装go调试工具

image-20230114212910810.png

配置完成

image-20230114213508174.png

1.3 克隆课程代码

使用git克隆课程代码

 git clone https://github.com/wangkechun/go-by-example

2 Go语言上手 - 基础语法

参考学习资料:Go语言圣经(中文版)青训营课程1-走进 Go 语言基础语言

2.1 走进Go语言基础语言

2.1.1 Go语言简介

Go特点: 高性能(类C++)、高并发,语法简单(C简化)、学习曲线平缓,丰富的标准库,完善的工具链(编译、代码格式化、包管理等),静态链接,快速编译,跨平台(多种常见平台,无需交叉编译环境),垃圾回收(类java)

2.1.2 Go语言开发环境配置

课前准备(VSCode)

2.1.3 基础语法 - Hello World

直接运行(使用 run

go run example/01-hello/main.go

image-20230115122035770.png

直接输出运行结果

编译运行(使用 build + ./

go build example/01-hello/main.go
./main

image-20230115122157271.png image-20230115122316703.png

静态编译成 .exe 文件,可直接运行,输出运行结果

2.1.4 基础语法 - 变量

常见变量类型包括字符串、整数、布尔型、浮点型

声明方法1: var 变量名 类型(可选) = 初始值

image-20230115122554771.png

Tips: Go中的字符串类型 string 为内置类型,类C++可使用 + 等方法。

声明方法2: 变量名 := 初始值

image-20230115123315541.png

常量声明: const 常量名 = 初始值

image-20230115123606078.png

Tips: Go中常量没有确定类型,根据上下文确定

运行样例

go run example/02-var/main.go

image-20230115123745845.png

输出字符串 initial ,整数 1,2 ,布尔值直接输出 true ,使用 + 进行的字符串拼接 foo ,科学计数法表示输出, math 库中的函数 Sin 使用

2.1.5 基础语法 - if else

Tips: 条件不需要括号 () ,执行语句必须带括号 {}

image-20230115124044783.png

运行样例

go run example/04-if/main.go

image-20230115124131656.png

分别进行的判断是:奇数偶数判断 %2 ;8整除4;数字正负与位数判断(分别与0,10相比较)

2.1.6 基础语法 - 循环

Tips: Go中只有 for 循环(没有 while / do-while 语句)

image-20230115125105568.png

运行样例

go run example/03-for/main.go

image-20230115125145150.png

运行的样例分别为死循环(用 break 跳出);常见的赋值递增循环;continue语句跳过后面部分;可任意省略的参数

2.1.7 基础语法 - switch

switch不需要括号,除了与C/C++中类似的结果分支,也可以直接在分支case中进行条件判断等

image-20230115133504870.png

Tips: Go中switch的分支默认不需要加 break ,不会像C++中继续执行。

运行样例

go run example/05-switch/main.go

image-20230115133601224.png

2.1.8 基础语法 - 数组

长度固定,声明格式为 var 变量名 [数组长度]变量类型 {初始值1,2,...}(可选)变量名 := [数组长度]变量类型 {初始值1,2,...}(可选)

image-20230115134056629.png

Tips: Go中更常用切片slice进行数据存储(可变长度)

运行样例

go run example/06-array/main.go

image-20230115134223082.png

2.1.9 基础语法 - 切片

可变长度数组,一般使用 meke 进行声明,声明格式为 切片名 := make([]变量类型,初始长度)切片名 := []变量类型{初始值1,2,...}

Tips:

  1. Go中可使用 append 向切片中添加元素,格式为 切片名 = append(切片名,元素),注意需要将其赋值回变量(其底层实现为记录当前的长度、容量与数组指针,容量不够时类似 vector 发生扩容)
  2. 使用 cpoy(复制后切片名, 被复制切片名) 进行切片的复制
  3. 切片操作类似py

image-20230115141145389.png

运行样例

go run example/07-slice/main.go

image-20230115141418674.png

2.1.10 基础语法 - map

类似于py中的字典,一般使用 make 进行创建,创建格式为 名称 := make(map[索引变量类型]存放变量类型) ,或 名称 := map[索引变量类型]存放变量类型{索引1:初始值1, 索引2:初始值2, ...}

image-20230115142425231.png image-20230115142433363.png

删除匹配关系使用 delete ,格式为 delete(名称, 索引)

Tips:

  1. map中存放数据无序;
  2. 对于没有定义的索引,map会返回0,可以使用第二个变量获取该索引是否有相应的匹配,返回布尔值

image-20230115142400959.png

运行样例

go run example/08-map/main.go

image-20230115142024502.png

在获取 unknow 索引时,返回值0,且匹配状态为 false

2.1.11 基础语法 - range

类似于py,一般用在循环遍历中,遍历数组/切片返回值为索引以及该索引对应的值;遍历map时返回值为key以及对应的value

image-20230115143715012.png

运行样例

go run example/09-range/main.go

image-20230115143548372.png

2.1.12 基础语法 - 函数

Go中的函数定义格式为 func 函数名 (传入参数...) (可选:返回值...)

返回结果包括函数结果以及错误信息

image-20230115144022357.png

运行样例

go run example/10-func/main.go

image-20230115144230028.png

2.1.13 基础语法 - 指针

Go的指针支持的操作相比于C/C++较有限,常用的如支持参数根据地址修改参数值。

image-20230115144636382.png

运行样例

go run example/11-point/main.go

image-20230115144542514.png

2.1.14 基础语法 - 结构体

定义格式 type 结构体名称 struct {参数...} ,声明可类似于map或变量,支持缺省赋值,使用 . 进行结构体参数访问。

image-20230115145122211.png

运行样例

go run example/12-struct/main.go

image-20230115145310588.png

两个 false 结果表示的是传入拷贝与传入指针与结构体中值比较的结果(使用指针传入可减少开销)

image-20230115145400935.png

2.1.15 基础语法 - 结构体方法

实现结果类似于C++类中的类成员函数,具体方法是将结构体参数放在 func 定义后。这样可以使用 . 进行方法的调用。

Tips: 传入结构体带指针可修改结构体参数值。

image-20230115150114731.png

运行样例

go run example/13-struct-method/main.go

image-20230115150217961.png

2.1.16 基础语法 - 错误处理

Go中的错误处理需要引入 errors 库,在需要处理的函数定义 error 类型的返回值,如果正常则返回 nil 值(空值,类似NULL),否则可以使用 error.new(错误消息) 返回错误的消息。

image-20230115150925730.png

运行样例

go run example/14-error/main.go

image-20230115150951491.png

2.1.17 基础语法 - 字符串操作

引入 strings 库可使用相应的字符串方法(使用 string 变量及简单的拼接操作等不需要该库),使用 len(字符串) 方法获取字符串长度。其余常用函数如下:

image-20230115151347088.png

Tips: 中文字符在字符串中占3位

运行样例

go run example/15-string/main.go

image-20230115151442905.png

2.1.18 基础语法 - 字符串格式化/格式化输出

使用 fmt 库进行字符串格式化输出。

  • fmt.Println() :打印多个变量且换行(类似于py中的 print

  • fmt.Printf() :打印变量,格式类似于C,但不需要进行变量类型的区分,统一使用 %v 代替(可使用 %+v 以及 %#v 更进一步详细输出变量)

    但打印浮点数时与C类似(%整体长度(可加0补0,可选).小数点后保留位数f),但Go中变量与py类似,只有浮点数不需区分单双精度浮点

运行样例

go run example/16-fmt/main.go

image-20230115152223375.png

2.1.19 基础语法 - JSON处理

导入 encoding/json 包进行相关JSON处理函数调用

  • json.Marshal(v any) :将结构体编码成JSON字符串(要求结构体变量名首字母大写,第二个参数返回错误信息)
  • json.MarshalIndent(v any, prefix string, indent string) :将结构体格式化编码成JSON字符串(第二个参数为前缀,第三个参数为缩进)
  • json.Unmarshal(data []byte, v any) :将JSON字符串解码到相应的数据结构

image-20230115154144793.png

运行样例

go run example/17-json/main.go

image-20230115153340038.png

2.1.20 基础语法 - 时间处理

导入 time 包进行时间处理。常用方法如下:

  • time.Now() :获取当前时间(yyyy-mm-dd hh:mm:ss + nsec nanoseconds,带时区);
  • time.Date(year int, month time.Month, day int, hour int, min int, sec int, nsec int, loc *time.Location) :构造一个时间戳;
  • .Year()/.Month()/.Day()/.Hour()/.Minute()/.Second() :获取时间戳的各个部分(返回整数);
  • .Format("2006-01-02 15:04:05") :格式化时间戳;
  • .Sub(time.Time)/... :对两个时间戳进行加减等操作;
  • time.Parse("2006-01-02 15:04:05", string) :解析时间;
  • .Unix(time.Time) :获取一个 int64 的时间戳

image-20230115155658594.png

运行样例

go run example/18-time/main.go

image-20230115155606033.png

2.1.21 基础语法 - 数字解析

导入 strconv 包进行字符串与数字的转换。常用方法如下:

  • strconv.ParseFloat(s string, bitSize int) :将传入的字符串转为 bitSize 位浮点数(32/64);
  • strconv.ParseInt(s string, base int, bitSize int) :将传入的字符串以 base 进制转为 bitSize 位精度整数(当字符串前由 0x/0o 等标志位时 base 也可传入0自动推测);
  • strconv.Atoi(s string) :将传入的字符串转为整数(十进制)
  • strconv.Itoa(i int) :将传入的整数转为字符串(十进制)

image-20230115160721131.png

Tips: 输入不合法时返回错误

运行样例

go run example/19-strconv/main.go

image-20230115160633250.png

2.1.22 基础语法 - 进程信息

导入 os , os/exec 包获取进程信息。

image-20230115161200007.png

2.2 Go语言的实战案例

2.2.1 猜谜游戏

目标及程序逻辑: 使用Golang构建一个猜数字游戏。在这个游戏里面,程序先会生成一个介于1到100之间的随机整数,然后提示玩家进行猜测。玩家每次输入一个数字,程序会告诉玩家这个猜测的值是高于还是低于那个秘密的随机数,并且让玩家再次猜测。如果猜对了,就告诉玩家胜利并且退出程序。

编写获取随机数的程序

运行代码

go run guessing-game/v1/main.go

image-20230115162136223.png

发现此时的随机数并不随机,因为没有设置随机数种子,随机数种子相同输出的随机数也相同。

用当前时间作为随机数种子,修改代码:

运行如下:

go run guessing-game/v2/main.go

image-20230115162446386.png

此时可获得随机数。

使用 bufio 进行数字输入的读取与字符转换

image-20230115163135208.png

import (
    ...
	"bufio"
	"os"
	"strconv"
	"strings"
)
...

func main() {
	...

	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")

	guess, err := strconv.Atoi(input)
	if err != nil {
		fmt.Println("Invalid input. Please enter an integer value")
		return
	}
	fmt.Println("You guess is", guess)
}

运行如下:

go run guessing-game/v3/main.go

image-20230115163004044.png

实现判断逻辑

使用if else等进行逻辑判断

func main() {
	...
    
    if guess > secretNumber {
		fmt.Println("Your guess is bigger than the secret number. Please try again")
	} else if guess < secretNumber {
		fmt.Println("Your guess is smaller than the secret number. Please try again")
	} else {
		fmt.Println("Correct, you Legend!")
	}
}

运行如下:

go run guessing-game/v4/main.go

image-20230115163452628.png

修改实现游戏循环

使用一个 for 循环重复进行判断,直到正确退出。最终代码如下:

package main

import (
	"bufio"
	"fmt"
	"math/rand"
	"os"
	"strconv"
	"strings"
	"time"
)

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

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

		guess, err := strconv.Atoi(input)
		if err != nil {
			fmt.Println("Invalid input. Please enter an integer value")
			continue
		}
		fmt.Println("You guess is", guess)
		if guess > secretNumber {
			fmt.Println("Your guess is bigger than the secret number. Please try again")
		} else if guess < secretNumber {
			fmt.Println("Your guess is smaller than the secret number. Please try again")
		} else {
			fmt.Println("Correct, you Legend!")
			break
		}
	}
}

运行如下:

go run guessing-game/v5/main.go

image-20230115163802310.png

2.2.2 命令行词典

目标及程序逻辑: 使用Golang构建一个简单的命令行词典。用户可以在命令行里查询一个单词,我们通过调用第三方API查询到单词的翻译并打印单词的音标以及释义。在过程中我们会使用Go语言来发送HTTP请求、解析json,以及使用高效的代码生成工具。

检查测试API接口

打开在线翻译 - 彩云小译,右键-检查,查看翻译接口

image-20230115164240731.png image-20230115164353965.png image-20230115164431151.png

复制cURL

image-20230115165124852.png

打开代码转换网站curlconverter,选择go,粘贴cURL

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"strings"
)

func main() {
	client := &http.Client{}
	var data = strings.NewReader(`{"trans_type":"en2zh","source":"translate"}`)
	req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("authority", "api.interpreter.caiyunai.com")
	req.Header.Set("accept", "application/json, text/plain, */*")
	req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,id;q=0.6,ko;q=0.5")
	req.Header.Set("app-name", "xy")
	req.Header.Set("content-type", "application/json;charset=UTF-8")
	req.Header.Set("origin", "https://fanyi.caiyunapp.com")
	req.Header.Set("os-type", "web")
	req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
	req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"`)
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("sec-ch-ua-platform", `"Windows"`)
	req.Header.Set("sec-fetch-dest", "empty")
	req.Header.Set("sec-fetch-mode", "cors")
	req.Header.Set("sec-fetch-site", "cross-site")
	req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36")
	req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", bodyText)
}

将其保存为 my.go ,运行代码

go run simpledict/my.go

结果如下:

image-20230115170354737.png

运行示例代码

go run simpledict/v1/main.go

结果如下:

image-20230115170716348.png

生成与解析request body

我们将翻译的格式 TransType 以及需要翻译的文本 Source 和用户ID UserID 存储为结构体并转换为Json输入。

...
type DictRequest struct {
	TransType string `json:"trans_type"`
	Source    string `json:"source"`
	UserID    string `json:"user_id"`
}

func main() {
	client := &http.Client{}
	request := DictRequest{TransType: "en2zh", Source: "good"}
	buf, err := json.Marshal(request)
	if err != nil {
		log.Fatal(err)
	}
	var data = bytes.NewReader(buf)
	
	...
}

使用JSON转Golang Struct网站将复杂的body转换成嵌套结构体

type DictResponse struct {
	Rc   int `json:"rc"`
	Wiki struct {
		KnownInLaguages int `json:"known_in_laguages"`
		Description     struct {
			Source string      `json:"source"`
			Target interface{} `json:"target"`
		} `json:"description"`
		ID   string `json:"id"`
		Item struct {
			Source string `json:"source"`
			Target string `json:"target"`
		} `json:"item"`
		ImageURL  string `json:"image_url"`
		IsSubject string `json:"is_subject"`
		Sitelink  string `json:"sitelink"`
	} `json:"wiki"`
	Dictionary struct {
		Prons struct {
			EnUs string `json:"en-us"`
			En   string `json:"en"`
		} `json:"prons"`
		Explanations []string      `json:"explanations"`
		Synonym      []string      `json:"synonym"`
		Antonym      []string      `json:"antonym"`
		WqxExample   [][]string    `json:"wqx_example"`
		Entry        string        `json:"entry"`
		Type         string        `json:"type"`
		Related      []interface{} `json:"related"`
		Source       string        `json:"source"`
	} `json:"dictionary"`
}

转换并打印结构体

image-20230115171848084.png

运行示例代码

go run simpledict/v3/main.go

结果如下:

image-20230115171954734.png

加入状态检测以及结构体筛选

检测200的ok状态,并选取结构体中我们所需要的音标与解释进行打印

image-20230115172155234.png

完善输入

修改输入为自定义单词

image-20230115172506181.png

运行示例代码

go run simpledict/v4/main.go hello

结果如下:

image-20230115172609334.png

2.2.3 Socks5代理

Socks5工作原理: 正常浏览器访一个网站,如果不经过代理服务器的话,就是先和对方的网站建立TCP连接,然后三次握手,握手完之后发起HTTP请求,然后服务返回HTTP响应。

如果设置代理服务器之后,流程会变得复杂一些。首先是浏览器和 socks5代理建立TCP连接,代理再和真正的服务器建立TCP连接。这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、 relay阶段

第一个阶段握手阶段,浏览器会向 socks5代理发送请求,包的内容包括一个协议的版本号,还有支持的认证的种类, socks5服务器会选中一个其支持的认证方式,返回给浏览器。如果返回的是00的话就代表不需要认证,返回其他类型的话会开始认证流程,由于我们要实现不加密的传输,因此暂时跳过第二阶段的认证流程。

第三个阶段是请求阶段,认证通过之后浏览器会 socks5服务器发起请求。主要信息包括版本号,请求的类型,一般主要是 connection请求,就代表代理服务器要和某个域名或者某个IP地址某个端口建立TCP连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。

第四个阶段是relay阶段,此时浏览器会发送正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边。然后实际上代理服务器并不关心流量的细节,可以是HTTP流量,也可以是其它TCP流量。

image-20230115173611722.png

TCP echo server实现

运行示例代码

go run proxy/v1/main.go

打开另一终端,用 telnet 命令连接端口

telnet 127.0.0.1 1080 #Win
nc 127.0.0.1 1080 #MacOS

此时输入都会被echo打印回显

image-20230115180215844.png

Tips: 如果未安装Telnet服务则会报错:

image-20230115175326043.png

只需要安装telnet客户端。打开“打开或关闭Windows功能”,勾选Telnet客户端,下载安装即可。

image-20230115175437543.png

image-20230115175546047.png

实现认证阶段auth函数

将之前的回显死循环修改为auth函数,进行认证

image-20230115180459022.png image-20230115182755764.png

auth函数实现如下

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
	// +----+----------+----------+
	// |VER | NMETHODS | METHODS  |
	// +----+----------+----------+
	// | 1  |    1     | 1 to 255 |
	// +----+----------+----------+
	// VER: 协议版本,socks5为0x05
	// NMETHODS: 支持认证的方法数量
	// METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
	// X’00’ NO AUTHENTICATION REQUIRED
	// X’02’ USERNAME/PASSWORD

	ver, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read ver failed:%w", err)
	}
	if ver != socks5Ver {
		return fmt.Errorf("not supported ver:%v", ver)
	}
	methodSize, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read methodSize failed:%w", err)
	}
	method := make([]byte, methodSize)
	_, err = io.ReadFull(reader, method)
	if err != nil {
		return fmt.Errorf("read method failed:%w", err)
	}
	log.Println("ver", ver, "method", method)
	// +----+--------+
	// |VER | METHOD |
	// +----+--------+
	// | 1  |   1    |
	// +----+--------+
	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%w", err)
	}
	return nil
}

运行示例代码

go run proxy/v2/main.go

打开另一终端,执行以下命令:

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

image-20230115182422960.png

可以看到当前auth是成功运行了的

实现请求阶段connect函数

类似于 auth 函数,我们编写一个 connect 函数,在执行完 auth 函数后调用进行连接。接受解析包格式:

image-20230115183556855.png

返回包格式:

image-20230115183727136.png

其中我们定义一个4个字节的缓冲区,使用 io.ReadFull 将其填满来方便我们对字段进行读取。

connect 函数完整实现如下:

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
	// +----+-----+-------+------+----------+----------+
	// |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
	// +----+-----+-------+------+----------+----------+
	// | 1  |  1  | X'00' |  1   | Variable |    2     |
	// +----+-----+-------+------+----------+----------+
	// VER 版本号,socks5的值为0x05
	// CMD 0x01表示CONNECT请求
	// RSV 保留字段,值为0x00
	// ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
	//   0x01表示IPv4地址,DST.ADDR为4个字节
	//   0x03表示域名,DST.ADDR是一个可变长度的域名
	// DST.ADDR 一个可变长度的值
	// DST.PORT 目标端口,固定2个字节

	buf := make([]byte, 4)
	_, err = io.ReadFull(reader, buf)
	if err != nil {
		return fmt.Errorf("read header failed:%w", err)
	}
	ver, cmd, atyp := buf[0], buf[1], buf[3]
	if ver != socks5Ver {
		return fmt.Errorf("not supported ver:%v", ver)
	}
	if cmd != cmdBind {
		return fmt.Errorf("not supported cmd:%v", ver)
	}
	addr := ""
	switch atyp {
	case atypIPV4:
		_, err = io.ReadFull(reader, buf)
		if err != nil {
			return fmt.Errorf("read atyp failed:%w", err)
		}
		addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
	case atypeHOST:
		hostSize, err := reader.ReadByte()
		if err != nil {
			return fmt.Errorf("read hostSize failed:%w", err)
		}
		host := make([]byte, hostSize)
		_, err = io.ReadFull(reader, host)
		if err != nil {
			return fmt.Errorf("read host failed:%w", err)
		}
		addr = string(host)
	case atypeIPV6:
		return errors.New("IPv6: no supported yet")
	default:
		return errors.New("invalid atyp")
	}
	_, err = io.ReadFull(reader, buf[:2])
	if err != nil {
		return fmt.Errorf("read port failed:%w", err)
	}
	port := binary.BigEndian.Uint16(buf[:2])

	log.Println("dial", addr, port)

	// +----+-----+-------+------+----------+----------+
	// |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
	// +----+-----+-------+------+----------+----------+
	// | 1  |  1  | X'00' |  1   | Variable |    2     |
	// +----+-----+-------+------+----------+----------+
	// VER socks版本,这里为0x05
	// REP Relay field,内容取值如下 X’00’ succeeded
	// RSV 保留字段
	// ATYPE 地址类型
	// BND.ADDR 服务绑定的地址
	// BND.PORT 服务绑定的端口DST.PORT
	_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
	if err != nil {
		return fmt.Errorf("write failed: %w", err)
	}
	return nil
}

运行示例代码

go run proxy/v3/main.go

打开另一终端,执行以下命令:

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

image-20230115184009707.png

可以看到我们正确打印出需要访问的IP与端口,实现正确

实现relay阶段

接着我们需要实现代理与服务器建立连接。使用 net.Dial 函数建立连接,并创建两个Goroutine(类似于子线程,但开销较小)执行 io.copy 函数双向转发数据。同时我们使用 context.WithCancel 函数避免其直接执行到返回 nil ,当两边任意一方出现异常退出时, Copy 函数退出并继续往下执行到 cancel() 函数,此时才会继续执行返回 nil

image-20230115184748574.png

运行示例代码

go run proxy/v4/main.go

打开另一终端,执行以下命令:

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

image-20230115193253433.png

可以看到此时成功建立连接。

配置浏览器代理

使用SwitchyOmega进行浏览器代理配置

image-20230115193655648.png

按照以下配置

image-20230115193944318.png

配置完成后使用配置,同时在本地运行我们实现的Socks5

image-20230115194407097.png image-20230115194502347.png

可以看到,我们的课程地址与端口都经过该端口进行了转发。