Go基础语法与简单实战 | 青训营笔记

55 阅读28分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

Go基础语法

hello world !

Go语言中常用fmt包进行输出

package main
​
import (
    "fmt"
)
​
func main() {
    fmt.Println("hello world")
}

Go的基本数据类型

Go是一个强变量语言,每一个变量都有确定的类型

Go中常用四种基础变量,整型浮点型字符串布尔型

Go中使用var来定义一个变量,可以在后面显式声明变量类型,也可以直接赋值,根据上下文推测变量类型。

Go中字符串相加是两个字符串进行拼接,字符串支持使用等号进行比较

Go中使用const来定义常量

package main
​
import (
    "fmt"
    "math"
)
​
func main() {
​
    var a = "initial"var b, c int = 1, 2var d = truevar e float64
​
    f := float32(e)
​
    g := a + "foo"
    fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0
    fmt.Println(g)                // initialappleconst s string = "constant"
    const h = 500000000
    const i = 3e20 / h
    fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}

Go的循环

Go中只有一种循环,for循环,格式与c/c++基本相同

Go中的for循环必须写花括号,不像c/c++如果for下面的语句只有一行可以不写花括号

package main
​
import "fmt"func main() {
​
    i := 1
    for {
        fmt.Println("loop")
        break
    }
    for j := 7; j < 9; j++ {
        fmt.Println(j)
    }
​
    for n := 0; n < 5; n++ {
        if n%2 == 0 {
            continue
        }
        fmt.Println(n)
    }
    for i <= 3 {
        fmt.Println(i)
        i = i + 1
    }
}

if分枝结构

Go中的if分枝结构与c/c++基本相同

Go中的if后面的条件不需要加小括号,if的内容必须加花括号,不像c/c++如果if后的内容只有一行可以不加花括号

Go可以在if的条件中对变量进行赋值操作,是一种常用的写法

package main
​
import "fmt"
​
func main() {
​
    if 7 % 2 == 0 {
        fmt.Println("7 is even")
    } else {
        fmt.Println("7 is odd")
    }
​
    if 8 % 4 == 0 {
        fmt.Println("8 is divisible by 4")
    }
​
    if num := 9; num < 0 {
        fmt.Println(num, "is negative")
    } else if num < 10 {
        fmt.Println(num, "has 1 digit")
    } else {
        fmt.Println(num, "has multiple digits")
    }
}

switch分枝结构

Go中的switch,在每一个case后面不需要写break,执行一个case后会自动跳出switch

Go中case的条件可以有更多的类型,比如可以是一个函数,变相是一种多层if的结构。

package main
​
import (
    "fmt"
    "time"
)
​
func main() {
​
    a := 2
    switch a {
    case 1:
        fmt.Println("one")
    case 2:
        fmt.Println("two")
    case 3:
        fmt.Println("three")
    case 4, 5:
        fmt.Println("four or five")
    default:
        fmt.Println("other")
    }
​
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("It's before noon")
    default:
        fmt.Println("It's after noon")
    }
}

数组

Go中数组的使用和c/c++类似,可以通过下标直接索引。

Go中数组是定长的,声明时需要显式给出数组的长度

package mainimport "fmt"
​
func main() {
​
    var a [5]int
    a[4] = 100
    fmt.Println("get:", a[2])
    fmt.Println("len:", len(a))
​
    b := [5]int{1, 2, 3, 4, 5}
    fmt.Println(b)
​
    var twoD [2][3]int
    for i := 0; i < 2; i++ {
        for j := 0; j < 3; j++ {
            twoD[i][j] = i + j
        }
    }
    fmt.Println("2d: ", twoD)
}

切片

Go中使用make方法创造一个切片

Go中的切片类似一种变长的数组

Go中使用append在切片后面添加元素

append在添加完后需要将地址重新赋值给原切片,因为在添加完新元素后,原来的地址可能不够用,这时需要分配一个新的地址,就要对新的地址进行接收。

Go中使用copy函数对切片进行复制

Go中可以使用类似Python处理切片的方法对切片进行分割,Go在分割时不支持负数索引

package main
​
import "fmt"func main() {
​
    s := make([]string, 3)
    s[0] = "a"
    s[1] = "b"
    s[2] = "c"
    fmt.Println("get:", s[2])   // c
    fmt.Println("len:", len(s)) // 3
​
    s = append(s, "d")
    s = append(s, "e", "f")
    fmt.Println(s) // [a b c d e f]
​
    c := make([]string, len(s))
    copy(c, s)
    fmt.Println(c) // [a b c d e f]
​
    fmt.Println(s[2:5]) // [c d e]
    fmt.Println(s[:5])  // [a b c d e]
    fmt.Println(s[2:])  // [c d e f]
​
    good := []string{"g", "o", "o", "d"}
    fmt.Println(good) // [g o o d]
}
​

map

Go中使用make创建一个map

Go中的map类似别的语言中的哈希,使用key-val对进行索引。

Go中的map使用key进行访问。

使用delete对map中的元素进行删除

package main
​
import "fmt"func main() {
    m := make(map[string]int)
    m["one"] = 1
    m["two"] = 2
    fmt.Println(m)           // map[one:1 two:2]
    fmt.Println(len(m))      // 2
    fmt.Println(m["one"])    // 1
    fmt.Println(m["unknow"]) // 0
​
    r, ok := m["unknow"]
    fmt.Println(r, ok) // 0 falsedelete(m, "one")
​
    m2 := map[string]int{"one": 1, "two": 2}
    var m3 = map[string]int{"one": 1, "two": 2}
    fmt.Println(m2, m3)
}

range

Go中使用range快速对map或切片等结构进行访问。

在访问切片时,返回两个参数,第一个参数为索引,第二个参数为索引上的值

在访问map时,返回两个参数,第一个参数为key,第二个参数为val

Go中的map访问是无序的,既不是按照字典序,也不是按照插入顺序,而是一种奇怪的随机顺序。

package main

import "fmt"

func main() {
	nums := []int{2, 3, 4}
	sum := 0
	for i, num := range nums {
		sum += num
		if num == 2 {
			fmt.Println("index:", i, "num:", num) // index: 0 num: 2
		}
	}
	fmt.Println(sum) // 9

	m := map[string]string{"a": "A", "b": "B"}
	for k, v := range m {
		fmt.Println(k, v) // b 8; a A
	}
	for k := range m {
		fmt.Println("key", k) // key a; key b
	}
}

函数

Go的函数,要素的位置很多都是相反的,返回值写在后面,形参的类型写在后面(这也是Go一贯的风格)

Go支持多返回值,一般在返回时,第一个返回值是真正的结果,第二个返回值是错误信息。

package main

import "fmt"

func add(a int, b int) int {
	return a + b
}

func add2(a, b int) int {
	return a + b
}

func exists(m map[string]string, k string) (v string, ok bool) {
	v, ok = m[k]
	return v, ok
}

func main() {
	res := add(1, 2)
	fmt.Println(res) // 3

	v, ok := exists(map[string]string{"a": "A"}, "a")
	fmt.Println(v, ok) // A True
}

指针

Go中支持简单的指针操作,主要用途是修改地址上的值,有时也会使用地址传递来避免对数据频繁地进行拷贝

package main

import "fmt"

func add2(n int) {
	n += 2
}

func add2ptr(n *int) {
	*n += 2
}

func main() {
	n := 5
	add2(n)
	fmt.Println(n) // 5
	add2ptr(&n)
	fmt.Println(n) // 7
}

结构体

Go的结构体用法与c/c++类似

结构体作为参数时,同样存在传地址和传值的区别。

package main

import "fmt"

type user struct {
	name     string
	password string
}

func main() {
	a := user{name: "wang", password: "1024"}
	b := user{"wang", "1024"}
	c := user{name: "wang"}
	c.password = "1024"
	var d user
	d.name = "wang"
	d.password = "1024"

	fmt.Println(a, b, c, d)                 // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
	fmt.Println(checkPassword(a, "haha"))   // false
	fmt.Println(checkPassword2(&a, "haha")) // false
}

func checkPassword(u user, password string) bool {
	return u.password == password
}

func checkPassword2(u *user, password string) bool {
	return u.password == password
}

结构体方法

Go中的结构体方法类似其他语言当中的类方法

Go会自动识别指针和变量。

package main

import "fmt"

type user struct {
	name     string
	password string
}

func (u user) checkPassword(password string) bool {
	return u.password == password
}

func (u *user) resetPassword(password string) {
	u.password = password
}

func main() {
	a := user{name: "wang", password: "1024"}
	a.resetPassword("2048")
	fmt.Println(a.checkPassword("2048")) // true
}

错误处理

Go中的错误不像Java中使用try-catch的方式捕捉错误,而是在函数中让用户自己定义错误类型并返回,这样可以更加简介清晰的知道程序到底出了什么问题。

Go中的error是一种类型,一般作为函数的第二个参数返回

Go中使用errors.New来定义一个错误。

package main

import (
	"errors"
	"fmt"
)

type user struct {
	name     string
	password string
}

func findUser(users []user, name string) (v *user, err error) {
	for _, u := range users {
		if u.name == name {
			return &u, nil
		}
	}
	return nil, errors.New("not found")
}

func main() {
	u, err := findUser([]user{{"wang", "1024"}}, "wang")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(u.name) // wang

	if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
		fmt.Println(err) // not found
		return
	} else {
		fmt.Println(u.name)
	}
}

strings包下常用的函数

具体见代码,比较容易理解

package main

import (
	"fmt"
	"strings"
)

func main() {
	a := "hello"
	fmt.Println(strings.Contains(a, "ll"))                // true
	fmt.Println(strings.Count(a, "l"))                    // 2
	fmt.Println(strings.HasPrefix(a, "he"))               // true
	fmt.Println(strings.HasSuffix(a, "llo"))              // true
	fmt.Println(strings.Index(a, "ll"))                   // 2
	fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
	fmt.Println(strings.Repeat(a, 2))                     // hellohello
	fmt.Println(strings.Replace(a, "e", "E", -1))         // hEllo
	fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]
	fmt.Println(strings.ToLower(a))                       // hello
	fmt.Println(strings.ToUpper(a))                       // HELLO
	fmt.Println(len(a))                                   // 5
	b := "你好"
	fmt.Println(len(b)) // 6
}

格式化输出

一般使用fmt包下的函数对数据进行格式化输出

Println既对参数进行默认的格式化输出并打印回车

Printf类似c/c++中的printf,只不过Go中统一使用%v对数据进行输出,%+v表示输出详细信息,%#v表示输出更详细的信息

使用%.2f对浮点数进行四舍五入保留两位小数输出。

package main

import "fmt"

type point struct {
	x, y int
}

func main() {
	s := "hello"
	n := 123
	p := point{1, 2}
	fmt.Println(s, n) // hello 123
	fmt.Println(p)    // {1 2}

	fmt.Printf("s=%v\n", s)  // s=hello
	fmt.Printf("n=%v\n", n)  // n=123
	fmt.Printf("p=%v\n", p)  // p={1 2}
	fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
	fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}

	f := 3.141592653
	fmt.Println(f)          // 3.141592653
	fmt.Printf("%.2f\n", f) // 3.14
}

Json

在struct中定义的变量,首字母大写就可以通过json.Marshal进行序列化,序列化后会变成一个byte数组

json格式输出时,需要强制转换为string类型,否则会输出16进制的编码

Go中序列化后的byte数组可以使用Unmarshal进行反序列化,要反序列化到一个空的结构当中。

package main

import (
	"encoding/json"
	"fmt"
)

type userInfo struct {
	Name  string
	Age   int `json:"age"`
	Hobby []string
}

func main() {
	a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
	buf, err := json.Marshal(a)
	if err != nil {
		panic(err)
	}
	fmt.Println(buf)         // [123 34 78 97...]
	fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}

	buf, err = json.MarshalIndent(a, "", "\t")
	if err != nil {
		panic(err)
	}
	fmt.Println(string(buf))

	var b userInfo
	err = json.Unmarshal(buf, &b)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}

时间相关

Go中使用time封装关于时间的函数

time.Now获取当前时间,可以通过.Year(), .Month()等方法得到时间的年月日等信息。

.Sub()可以获取两个时间之间的时间长度,用于求持续时间,可以使用.Minutes(), ,Seconds()来获取这个时间长度有几分钟几秒。

时间的格式化输出,并不是像其他语言中的yyyy-mm-dd,而是 "2006-01-02 15:04:05"(手册里写的,不知道为啥)

.Unix()用于返回当前时间的时间戳

package main

import (
	"fmt"
	"time"
)

func main() {
	now := time.Now()
	fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
	t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
	t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
	fmt.Println(t)                                                  // 2022-03-27 01:25:36 +0000 UTC
	fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
	fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-03-27 01:25:36
	diff := t2.Sub(t)
	fmt.Println(diff)                           // 1h5m0s
	fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
	t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
	if err != nil {
		panic(err)
	}
	fmt.Println(t3 == t)    // true
	fmt.Println(now.Unix()) // 1648738080
}

字符串与数字之间的转换

Go中使用strconv包进行转换

具体的用法见代码。

package main

import (
	"fmt"
	"strconv"
)

func main() {
    				  //数据   bitsize
	f, _ := strconv.ParseFloat("1.234", 64)
	fmt.Println(f) // 1.234
				//数据 进制
	n, _ := strconv.ParseInt("111", 10, 64)
	fmt.Println(n) // 111
				//数据  默认进制
	n, _ = strconv.ParseInt("0x1000", 0, 64)
	fmt.Println(n) // 4096

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

	n2, err := strconv.Atoi("AAA")
	fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
}

进程信息

os.Args获取进程执行时的命令行参数

os.Getenv,os.Setenv用来获取或者写入环境变量

exec.Command来快速获取子进程,并获取其输入输出

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	// go run example/20-env/main.go a b c d
	fmt.Println(os.Args)           // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
	fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
	fmt.Println(os.Setenv("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
}

三个小case实战

0x01猜数字

随机生成一个100以内正整数,用户猜数字是多少,主要学习Go语言中是如何使用输入流

Go使用math/rand包实现随机数的生成,和c/c++类似,需要设置一个随机数种子,否则生成的是伪随机数,一般以程序运行当前时间的时间戳为种子

package main
​
import (
    "fmt"
    "math/rand"
    "time"
)
​
func main() {
    maxNum := 100
    rand.Seed(time.Now().UnixNano())    //设置随机数种子
    secretNumber := rand.Intn(maxNum)
    fmt.Println("The secret number is ", secretNumber)
}

输入,两种方式得到输入

方式一

直接使用fmt.Scanf进行输入,格式与c/c++中的scanf函数类似。

package main
import (
    "fmt"
    "math/rand"
    "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")
    var guess int
    fmt.Scanf("%d", &guess)                             //标准输入
    
    fmt.Println("You guess is", guess)
}

方式二(当读入的文件过大时,避免一次读取需要消耗过多的内存,一般采用流的方式读取)

使用bufio的方式进行输入

bufio.NewReader(os.Stdin)创建一个只读输入流

.ReadString(‘\n’)表示以回车为分隔符读取流中的数据

将输入的字符串前后的空格和回车删除,并使用Atoi将string转为int

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)                         //创建读入的流
    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)                          //打印结果
}
​

最后的标准程序:

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
        }
    }
}

0x02 命令行字典

使用彩云小译的api接口实现一个在命令行查字典的case

首先测试接口的可用性,使用浏览器打开fanyi.caiyunapp.com/#/,输入要翻译的单词后摁F12打开程序员工具,之后点击翻译,再点开network,并在下面找到dict,在Headers, Payload,Preview和Response中可以看到相应的请求信息和响应的信息。

image.png

image.png

image.png

image.png

这样就表示接口正常可用。

写Go语言程序来调用这个接口来实现翻译

程序作为客户机,需要向服务器发送http请求,就要打包好一个http请求报文。其中包括创建报文,设置请求头参数,请求方式,请求路径等。

手写这些请求参数比较繁琐,而我们所有的信息在浏览器中已经存在了,所以这里就可以使用代码生成工具curlconverter.com/go/,复制下面的请求,粘贴到网站上,即可自动生成代码并设置好参数。

image.png

image.png

v1标程

package main
​
import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "strings"
)func main() {
    client := &http.Client{}
    var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
    req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
    if err != nil {
        log.Fatal(err)
    }
    req.Header.Set("Connection", "keep-alive")
    req.Header.Set("DNT", "1")
    req.Header.Set("os-version", "")
    req.Header.Set("sec-ch-ua-mobile", "?0")
    req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
    req.Header.Set("app-name", "xy")
    req.Header.Set("Content-Type", "application/json;charset=UTF-8")
    req.Header.Set("Accept", "application/json, text/plain, */*")
    req.Header.Set("device-id", "")
    req.Header.Set("os-type", "web")
    req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
    req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
    req.Header.Set("Sec-Fetch-Site", "cross-site")
    req.Header.Set("Sec-Fetch-Mode", "cors")
    req.Header.Set("Sec-Fetch-Dest", "empty")
    req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
    req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
    req.Header.Set("Cookie", "_ym_uid=16456948721020430059; _ym_d=1645694872")
    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)
}

自定义参数并接收其响应

前面是写死的单词,现在需要做到想查哪个查哪个

请求中比较重要的两个参数,一个是source,表示要查的单词,一个是trans_type表示要把什么语言翻译为什么语言。将这两个参数封装到一个结构体中,并使用json.marshal转换为字节切片,再转换为字节流,传入请求头中。这样就可以通过输入来确定要查询的单词。而不是写死的单词。

调用client.Do(req)来发送请求并接收消息,在接收完之后,一般会立即使用defer关键字来将返回的流关闭,以保证在程序执行完之后会把resp关掉。

package main
​
import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)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)
    req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
    if err != nil {
        log.Fatal(err)
    }
    req.Header.Set("Connection", "keep-alive")
    req.Header.Set("DNT", "1")
    req.Header.Set("os-version", "")
    req.Header.Set("sec-ch-ua-mobile", "?0")
    req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
    req.Header.Set("app-name", "xy")
    req.Header.Set("Content-Type", "application/json;charset=UTF-8")
    req.Header.Set("Accept", "application/json, text/plain, */*")
    req.Header.Set("device-id", "")
    req.Header.Set("os-type", "web")
    req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
    req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
    req.Header.Set("Sec-Fetch-Site", "cross-site")
    req.Header.Set("Sec-Fetch-Mode", "cors")
    req.Header.Set("Sec-Fetch-Dest", "empty")
    req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
    req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
    req.Header.Set("Cookie", "_ym_uid=16456948721020430059; _ym_d=1645694872")
    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)
}

请求已经发出,结果也已经得到,但是结果是序列化后的结果,需要对其进行反序列化。

常用的做法是,定义一个空的结构体,包括返回信息的各个字段,并使用json.unmarshal对收到的信息进行反序列化,得到json格式的数据。

但是这其中会出现一个小问题,返回的信息非常多,结构体定义起来比较繁琐,这里同样可以使用代码生成工具进行生成。oktools.net/json2go将json格式的信息转换为相应的结构体定义。

复制下面json格式的Response,粘贴到网站,即可得到对应结构体的定义。

image.png

image.png

定义好结构体,即可对收到的数据进行反序列化,那么如何从中拿出想要的信息?

例如这里,dictionary中的explanations字段,表示的是翻译的结果,pron字段,表示单词的音标。这两条是对我们很有用的消息,需要得到并输出。

在得到的反序列化后的数据中,直接采用.运算符,即可取出各个字段的信息。得到输出即可。

最后的标程

package main
​
import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
)
//用于设置请求头中的参数
type DictRequest struct {
    TransType string `json:"trans_type"`    //表示转换方式,这里是固定的en2zh,既英文转为中文
    Source    string `json:"source"`        //表示需要查询的单词。
    UserID    string `json:"user_id"`
}
//用于把接收到的序列化后的数据转化为json格式,并按照需求进行输出打印
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"`
}
//word既为要查的单词
func query(word string) {
    client := &http.Client{}
    request := DictRequest{TransType: "en2zh", Source: word}        //定义结构体并封装好信息
    buf, err := json.Marshal(request)                               //对结构体进行序列化,转化为json格式
    if err != nil {
        log.Fatal(err)
    }
    var data = bytes.NewReader(buf)                                 //生成字节流,放入请求头中
    req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
    if err != nil {
        log.Fatal(err)
    }
    //设置一系列参数
    req.Header.Set("Connection", "keep-alive")
    req.Header.Set("DNT", "1")
    req.Header.Set("os-version", "")
    req.Header.Set("sec-ch-ua-mobile", "?0")
    req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
    req.Header.Set("app-name", "xy")
    req.Header.Set("Content-Type", "application/json;charset=UTF-8")
    req.Header.Set("Accept", "application/json, text/plain, */*")
    req.Header.Set("device-id", "")
    req.Header.Set("os-type", "web")
    req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
    req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
    req.Header.Set("Sec-Fetch-Site", "cross-site")
    req.Header.Set("Sec-Fetch-Mode", "cors")
    req.Header.Set("Sec-Fetch-Dest", "empty")
    req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
    req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
    req.Header.Set("Cookie", "_ym_uid=16456948721020430059; _ym_d=1645694872")
    //调用.Do来发出请求,并接收返回值
    resp, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    //使用defer关键字,在程序执行完后将resp关闭。
    defer resp.Body.Close()
    bodyText, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    //在返回的序列中,通常包括状态码,一般会加三行这个代码,如果返回错误的状态码,就在日志中打印一下,方便定位bug的位置
    if resp.StatusCode != 200 {
        log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
    }
    //定义一个空的结构体,用来接收反序列化后的信息。
    var dictResponse DictResponse
    err = json.Unmarshal(bodyText, &dictResponse)
    if err != nil {
        log.Fatal(err)
    }
    //输出其英式发音和美式发音的音标
    fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
    //输出单词的每个意思。
    for _, item := range dictResponse.Dictionary.Explanations {
        fmt.Println(item)
    }
}
​
func main() {
    //这里使用命令行来提供参数,----------$ go run main.go hello
    //                               翻译一下hello
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
        `)
        os.Exit(1)
    }
    word := os.Args[1]
    query(word)
}
​

0x03 SOCKS5代理服务器

不能翻墙,都是明文传输的

SOCKS5基本原理

通过SOCKS5协议做代理服务器的工作流程如下:

image.png

分为四个阶段,协商阶段认证阶段请求阶段relay阶段

协商阶段

用户向SOCKS5代理服务器发送报文,包括版本号,认证方式等信息,SOCKS5返回一个报文,告诉用户自己支持什么样的认证方式,返回00代表不需要认证,返回其他认证的话,下一步会做认证流程

这里跳过认证流程,因为这次实现的是不加密的代理服务器

请求阶段

用户向代理服务器发送新的报文,包括协议版本号,请求类型等。一般是connection请求,需要发送要访问的域名或ip地址和端口等信息。

relay阶段

用户正常发送请求,代理服务器收到请求后将请求转发到真正的服务器上,并将服务器的响应转发到客户端

实现TCP echo server

你发送什么,服务器就回复什么,用来测试服务器是否可用。

Go中使用net.Listen对服务器进行监听,返回一个监听

使用.Accept()检测服务器是否接收到连接,收到就返回连接

并发环境下,使用一个新的协程处理这个连接

从连接中读取数据,并将读取到的数据写入连接中返回。这样来做到发送什么,就回复什么的效果

package main
​
import (
    "bufio"
    "fmt"
    "log"
    "net"
)
​
func main() {
    server, err := net.Listen("tcp", "127.0.0.1:1080")
    if err != nil {
        panic(err)
    }
    for {
        client, err := server.Accept()
        fmt.Println("!!")
        if err != nil {
            log.Printf("Accept failed %v", err)
            continue
        }
        go process(client)
    }
}
​
func process(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    for {
        b, err := reader.ReadByte()
        if err != nil {
            break
        }
        _, err = conn.Write([]byte{b})
        if err != nil {
            break
        }
    }
}

实现SOCKS5的第一阶段

实现一个不加密的简单SOCKS5协议。第二阶段的认证就省略了

在第一阶段,用户会向SOCKS5服务器发送协议的版本号,和支持的认证方式,如下所示:

// +----+----------+----------+
// |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

SOCKS5服务器收到协议后,会对数据进行一系列解析,并返回给客户机一个包,指明使用的协议和推荐你使用的认证方式。这里不需要认证,所以返回0x00即可

// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1  |   1    |
// +----+--------+
package main
​
import (
    "bufio"
    "fmt"
    "io"
    "log"
    "net"
)
​
const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04func main() {
    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)
    }
}
​
func process(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    err := auth(reader, conn)
    if err != nil {
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
    log.Println("auth success")
}
​
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
}
​

这一步我们实现了SOCKS5服务器接收参数并返回给客户机的阶段

实现SOCKS5代理的第三阶段

这个阶段客户机会向SOCKS5代理服务器发送请求报文,告诉代理服务器要请求的地址端口号等信息。代理服务器给予一个回包

客户机向代理服务器发送的报文格式如下:

// +----+-----+-------+------+----------+----------+
// |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个字节

代理服务器收到信息之后需要做:

  1. 检验版本号是否是SOCKS5协议
  2. 本次只实现连接请求,故CMD字段为0x01
  3. 从ATYP中检测目标地址类型,并根据类型从DST.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

很多字段都没有用,将格式拼接好写入到连接中即可。

package main
​
import (
    "bufio"
    "encoding/binary"
    "errors"
    "fmt"
    "io"
    "log"
    "net"
)
​
const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04func main() {
    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)
    }
}
​
func process(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    err := auth(reader, conn)
    if err != nil {
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
    err = connect(reader, conn)
    if err != nil {
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
}
​
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)
    }
​
    // +----+--------+
    // |VER | METHOD |
    // +----+--------+
    // | 1  |   1    |
    // +----+--------+
    _, err = conn.Write([]byte{socks5Ver, 0x00})
    if err != nil {
        return fmt.Errorf("write failed:%w", err)
    }
    return nil
}
​
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
}

这里服务器端实现了与客户机建立连接并收到来自客户机请求的地址和端口号等信息。

实现SOCKS5代理的第三 第四阶段

SOCKS5服务器与真正的服务器建立连接,双向转换数据

Go中使用net.Dial()函数实现TCP的连接,并返回一个连接

将SOCKS5服务器与真正服务器的连接与客户端与SOCKS5代理服务器的连接相互连接,使其能够实现双向的数据转换

使用io.Copy函数实现数据流之间的连接。

最终的代码:

package main
​
import (
    "bufio"
    "context"
    "encoding/binary"
    "errors"
    "fmt"
    "io"
    "log"
    "net"
)
//定义一些常量
const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04func main() {
    //监听端口,这个端口就是SOCKS5服务器的端口
    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)
    }
}
​
func process(conn net.Conn) {
    //这个连接的生命周期也就是这个函数的生命周期,确保在函数执行完之后连接关闭连接
    defer conn.Close()
    //从连接中读取数据流
    reader := bufio.NewReader(conn)
    //调用auth函数对读入的数据流和连接进行处理(包括第一阶段和第二阶段【第二阶段不做验证而已】)
    err := auth(reader, conn)
    if err != nil {
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
    //调用connect函数接收客户机发送的请求报文,并提取出报文中的请求地址或域名和端口号
    err = connect(reader, conn)
    if err != nil {
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
}
​
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)
    }
    //如果使用的协议不是SOCKS5,就错误
    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)
    }
    //创建缓冲区接收认证的方法(接受了但是没用,因为我们不认证hhh)
    method := make([]byte, methodSize)
    _, err = io.ReadFull(reader, method)
    if err != nil {
        return fmt.Errorf("read method failed:%w", err)
    }
    //回包的格式
    // +----+--------+
    // |VER | METHOD |
    // +----+--------+
    // | 1  |   1    |
    // +----+--------+
    
    //写入协议版本号和推荐使用的认证方式(00代表不认证)
    _, err = conn.Write([]byte{socks5Ver, 0x00})
    if err != nil {
        return fmt.Errorf("write failed:%w", err)
    }
    return nil
    //SOCKS5代理的第1 2阶段完成
}
​
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个字节
​
    //创建大小为4字节的缓冲区,用于读取前四个固定字长的报文数据
    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 {
    //ipv4则后面是固定字长4字节的地址
    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)
    //ipv6和其他情况不做处理
    case atypeIPV6:
        return errors.New("IPv6: no supported yet")
    default:
        return errors.New("invalid atyp")
    }
    
    //复用前面的buf缓冲区,读取后面固定的两字节端口号
    _, err = io.ReadFull(reader, buf[:2])
    if err != nil {
        return fmt.Errorf("read port failed:%w", err)
    }
    //使用大端方式
    port := binary.BigEndian.Uint16(buf[:2])
    
    //调用net.Dial函数使用tcp与读取到的地址和端口进行连接,并返回一个连接
    dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
    if err != nil {
        return fmt.Errorf("dial dst failed:%w", err)
    }
    //在函数执行结束后关闭连接
    defer dest.Close()
    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)
    }
    //现在SOCKS5代理服务器有两个连接,一个是客户机与代理服务器的连接,一个是代理服务器与真正的服务器的
    //连接,这时需要开两个写成将这两个连接连到一起,实现双向数据传输。但是开启协程之后就会这个进程就会关
    //闭,这样这两个协程也就会关闭,这样无法实现数据的传输,所以在双方都还有数据交换的时候,保证这个进程
    //不会关闭,这样就引入了context机制,context.Withcancel(),表示当cancel()函数执行完成时,ctx.Done()也会执行
    //这样就实现了当有任意一方不发送数据时,这个连接才关闭。
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    
    go func() {
        _, _ = io.Copy(dest, reader)
        cancel()                            //****
    }()
    go func() {
        _, _ = io.Copy(conn, dest)
        cancel()                            //****
    }()
​
    <-ctx.Done()
    return nil
}