Go 语言基础| 青训营笔记

119 阅读14分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

简介

Go 语言的特点

  • 高性能、高并发

  • 语法简单、学习曲线平缓

    // 一个简单的 HTTP 服务器
    package main
    import (
      "net/http"
    )
    
    func main() {
      http.Handle("/", http.FileServer(http.Dir("."))) // 路由实现
      http.ListenAndServe(":8080", nil) // 监听端口并启动服务器
    }
    
  • 丰富的标准库

  • 完善的工具链

  • 静态链接

  • 快速编译

  • 跨平台

  • 垃圾回收

开发环境

  1. 安装 Golang
  2. 配置集成开发环境:VS Code / Goland

基础语法与常用标准库

Hello World
package main // 表示文件是程序入口 main 包的一部分

import (
  "fmt" // 用于屏幕输入输出、格式化字符串
)

func main() {
  fmt.Println("hello world")
}
> go run example/01-hello/main.go # 运行
hello world
> go build example/01-hello/main.go # 编译成二进制
> ./main
hello world
变量
  • 强类型语言:字符串型、整型、浮点型、布尔型等
  • 变量声明:=:=
  • 常量:没有确定的类型,会根据使用的上下文自动确定类型
package main

import (
  "fmt"
  "math"
)

func main() {
  
  var a = "initial" // 自动推断类型
  
  var b, c int = 1, 2 // 显式指定
  
  var d = true
  
  var 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) // initialapple
  
  const s string = "constant"
  const h = 500000000
  const i = 3e20 / h
  fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
if else
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")
  }
} 
循环
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
  }
} 
switch分支结构
package main

import (
  "fmt"
  "time"
)

func main() {
  a := 2
  switch a { // 可使用任意变量类型,如字符串、结构体
    case 1:
    	fmt.Println("one") // 不需要加 break
    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 中写条件分支,较多 if else 嵌套更清晰可读
    case t.Hour() < 12:
    	fmt.Println("It's before noon")
    default:
    	fmt.Println("It's after noon")
  }
}
数组
package main

import "fmt"

func main() {
  
  var a [5]int
  a[4] = 100
  fmt.Println(a[4], 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)
}
切片
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") // slice 的原理:存储长度+容量+指向数组的指针,append 容量不够时,会产生扩容并返回一个新的 slice
  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 (hash / dict)
package main

import "fmt"

func main() {
  m := make(map[string]int) // map[key type]value type
  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"] // ok 获取 key 的存在性
  fmt.Println(r, ok) // 0 false
  
  delete(m, "one")
  
  m2 := map[string]int{"one": 1, "two": 2}
  var m3 = map[string]int{"one": 1, "two": 2}
  fmt.Println(m2, m3) // map 完全无序
range:快速遍历slice/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 { // 返回 key、value
    fmt.Println(k, v) // b 8; a A
  }
  for k:= range m {
    fmt.Println("key", k) // key a; key b
  }
}
函数
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
} 
指针
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
}
结构体
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 // 用指针实现对结构体的修改,避免大结构体拷贝的开销
} 
结构体方法
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
}
错误处理
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)
  }
}
字符串操作
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 一个中文对应多个字符
}
字符串格式化
  • %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=%vn", 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 处理
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("%#vn", b) // main.userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "Typescript"}}
}
时间处理
  • 特定的时间字符串示例:"2006-01-02 15:04:05"
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) // 1h5mOs
  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
}
数字解析
package main

import (
	"fmt"
  "strconv" // 字符串数字转换包
)

func main() {
  f, _ := strconv.ParseFloat("1.234", 64) // 64位精度
  fmt.Println(f) // 1.234
  
  n, _ := strconv.ParseInt("111", 10, 64)
  fmt.Println(n) // 111
  
  n, _ := strconv.ParseInt("0x1000", 0, 64) // 0 代表自动推测进制
  fmt.Println(n) // 4096
  
  n2, _ := strconv.Atoi("123") // 快速将十进制字符串转成数字;Itoa相反
  fmt.Println(n2) // 123
  
  n2, _ := strconv.Atoi("AAA")
  fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
}
进程信息
  • os.Args:二进制自身的目录和名字、参数
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
}

实战

猜谜游戏

生成随机数

package main

import (
	"fmt"
  "math/rand"
)

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

效果:一直是同一个数字

> go run guessing-game/v1/main.go
The secret number is 81
> go run guessing-game/v1/main.go
The secret number is 81
> go run guessing-game/v1/main.go
The secret number is 81

原因:未设置随机数种子

Random numbers are generated by a Source. Top-level functions, such as Float64 and Int, use a default shared Source that produces a deterministic sequence of values each time a program is run. Use the Seed function to initialize the default Source if different behavior is required for each run. The default Source is safe for concurrent use by multiple goroutines, but Sources created by NewSource are not.

用时间戳初始化随机数种子

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

读取用户输入

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.TrimSuffix(input, "\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)
}

实现判断逻辑

  guess, err := strconv.Atoi(input) // 转换成数字
  if err != nil {
  	fmt.Println("Invalid input. Please enter an integer value")
  	return
  }
  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!")
+ }
}

实现游戏循环

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)
-			return
+			continue
		}
		input = strings.TrimSuffix(input, "\n")

		guess, err := strconv.Atoi(input)
		if err != nil {
			fmt.Println("Invalid input. Please enter an integer value")
-			return
+			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
+		}
	}
}
命令行字典
  • 发送 HTTP 请求
  • 解析 JSON
  • 使用代码生成提升开发效率

抓包彩云小译

image.png

代码生成

  1. Copy as cURL (bash)

    curl 'https://api.interpreter.caiyunai.com/v1/dict' \
      -H 'Accept: application/json, text/plain, */*' \
      -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6' \
      -H 'Connection: keep-alive' \
      -H 'Content-Type: application/json;charset=UTF-8' \
      -H 'DNT: 1' \
      -H 'Origin: https://fanyi.caiyunapp.com' \
      -H 'Referer: https://fanyi.caiyunapp.com/' \
      -H 'Sec-Fetch-Dest: empty' \
      -H 'Sec-Fetch-Mode: cors' \
      -H 'Sec-Fetch-Site: cross-site' \
      -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36 Edg/101.0.1210.32' \
      -H 'X-Authorization: token:qgemv4jr1y38jyq6vhvi' \
      -H 'app-name: xy' \
      -H 'device-id: ' \
      -H 'os-type: web' \
      -H 'os-version: ' \
      -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="101", "Microsoft Edge";v="101"' \
      -H 'sec-ch-ua-mobile: ?0' \
      -H 'sec-ch-ua-platform: "Windows"' \
      --data-raw '{"trans_type":"en2zh","source":"dictionary"}' \
      --compressed
    
  2. Convert curl commands to Python, JavaScript, PHP, R, Go, Rust, Elixir, Java, MATLAB, Dart, CFML, Ansible URI, Strest or JSON

    package main
    
    import (
    	"fmt"
    	"io/ioutil"
    	"log"
    	"net/http"
    	"strings"
    )
    
    func main() {
    	client := &http.Client{}
    	var data = strings.NewReader(`{"trans_type":"en2zh","source":"dictionary"}`) // 将data 字符串转换成流,流式请求减少内存占用
    	req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data) // 创建请求(method, url, data)
    	if err != nil {
    		log.Fatal(err)
    	}
    	req.Header.Set("Accept", "application/json, text/plain, */*") // 设置请求头
    	req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
    	req.Header.Set("Connection", "keep-alive")
    	req.Header.Set("Content-Type", "application/json;charset=UTF-8")
    	req.Header.Set("DNT", "1")
    	req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
    	req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
    	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/101.0.4951.41 Safari/537.36 Edg/101.0.1210.32")
    	req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
    	req.Header.Set("app-name", "xy")
    	req.Header.Set("os-type", "web")
    	req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="101", "Microsoft Edge";v="101"`)
    	req.Header.Set("sec-ch-ua-mobile", "?0")
    	req.Header.Set("sec-ch-ua-platform", `"Windows"`)
    	resp, err := client.Do(req) // 发起请求
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer resp.Body.Close() // defer 在函数结束后从下往上触发;response 的 Body 为流,为避免资源泄漏手动关闭该流
    	bodyText, err := ioutil.ReadAll(resp.Body) // 读取响应流为 JSON 数组
    	if err != nil {
    		log.Fatal(err)
    	}
    	fmt.Printf("%s\n", bodyText)
    }
    

    效果:

    > go run simpledict/v1/main.go hello
    {"rc":0,"wiki":{"known_in_laguages":63,"description":{"source":"tangible and intangible thing, except labor tied services, that satisfies human wants and provides utility","target":null},"id":"Q28877","item":{"source":"good","target":"\u5546\u54c1"},"image_url":"http:\/\/www.caiyunapp.com\/imgs\/link_default_img.png","is_subject":"true","sitelink":"https:\/\/www.caiyunapp.com\/read_mode\/?id=625b4b949c0120504d1e7b69"},"dictionary":{"prons":{"en-us":"[g\u028ad]","en":"[gud]"},"explanations":["a.\u597d\u7684;\u5584\u826f\u7684;\u5feb\u4e50\u7684;\u771f\u6b63\u7684;\u5bbd\u5927\u7684;\u6709\u76ca\u7684;\u8001\u7ec3\u7684;\u5e78\u798f\u7684;\u5fe0\u5b9e\u7684;\u4f18\u79c0\u7684;\u5b8c\u6574\u7684;\u5f7b\u5e95\u7684;\u4e30\u5bcc\u7684","n.\u5229\u76ca;\u597d\u5904;\u5584\u826f;\u597d\u4eba","ad.=well"],"synonym":["excellent","fine","nice","splendid","proper"],"antonym":["bad","wrong","evil","harmful","poor"],"wqx_example":[["to the good","\u6709\u5229,\u6709\u597d\u5904"],["good, bad and indifferent","\u597d\u7684,\u574f\u7684\u548c\u4e00\u822c\u7684"],["good innings","\u957f\u5bff"],["good and ...","\u5f88,\u9887;\u5b8c\u5168,\u5f7b\u5e95"],["do somebody's heart good","\u5bf9\u67d0\u4eba\u7684\u5fc3\u810f\u6709\u76ca,\u4f7f\u67d0\u4eba\u611f\u5230\u6109\u5feb"],["do somebody good","\u5bf9\u67d0\u4eba\u6709\u76ca"],["be good for","\u5bf9\u2026\u6709\u6548,\u9002\u5408,\u80dc\u4efb"],["be good at","\u5728\u2026\u65b9\u9762(\u5b66\u5f97,\u505a\u5f97)\u597d;\u5584\u4e8e"],["as good as one's word","\u4fe1\u5b88\u8bfa\u8a00,\u503c\u5f97\u4fe1\u8d56"],["as good as","\u5b9e\u9645\u4e0a,\u51e0\u4e4e\u7b49\u4e8e"],["all well and good","\u4e5f\u597d,\u8fd8\u597d,\u5f88\u4e0d\u9519"],["a good","\u76f8\u5f53,\u8db3\u8db3"],["He is good at figures . ","\u4ed6\u5584\u4e8e\u8ba1\u7b97\u3002"]],"entry":"good","type":"word","related":[],"source":"wenquxing"}}
    

生成 request body

package main

import (
+	"bytes"
+ "encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
-	"strings"
)

+type DictRequest struct {
+	TransType string `json:"transtype"`
+	Source    string `json:"source"`
+	UserID    string `json:"user_id"`
+}

func main() {
	client := &http.Client{}
-	var data = strings.NewReader(`{"trans_type":"en2zh","source":"dictionary"}`)
+	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)
	}
	...

解析 request body

Python / JavaScriptGo(强类型语言)
返回的 response body字典 / map 结构JSON 字符串
解析方法[] / . 取值[] / . 取值(可行但非最佳实践)
⭕反序列化到字段一一对应的结构体代码生成繁琐且易出错\xrightarrow[\text{代码生成}]{\text{繁琐且易出错}}JSON 转 Golang Struct
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"`
}
	...
	bodyText, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
-	fmt.Printf("%s\n", bodyText)
+ var dictResponse DictResponse
+ err = json.Unmarshal(bodyText, &dictResponse)
+ if err != nil {
+ 	log.Fatal(err)
+ }
+ fmt.Printf("%#v\n", dictResponse)
}

效果:

> go run simpledict/v3/main.go hello
main.DictResponse{Rc:0, 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\"" }{KnownInLaguages:63, Description:struct { Source string "json:\"source\""; Target interface {} "json:\"target\"" }{Source:"tangible and intangible thing, except labor tied services, that satisfies human wants and provides utility", Target:interface {}(nil)}, ID:"Q28877", Item:struct { Source string "json:\"source\""; Target string "json:\"target\"" }{Source:"good", Target:"商品"}, ImageURL:"http://www.caiyunapp.com/imgs/link_default_img.png", IsSubject:"true", Sitelink:"https://www.caiyunapp.com/read_mode/?id=625b4b949c0120504d1e7b69"}, 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\"" }{Prons:struct { EnUs string "json:\"en-us\""; En string "json:\"en\"" }{EnUs:"[gʊd]", En:"[gud]"}, Explanations:[]string{"a.好的;善良的;快乐的;真正的;宽大的;有益的;老练的;幸福的;忠实的;优秀的;完整的;彻底的;丰富的", "n.利益;好处;善良;好人", "ad.=well"}, Synonym:[]string{"excellent", "fine", "nice", "splendid", "proper"}, Antonym:[]string{"bad", "wrong", "evil", "harmful", "poor"}, WqxExample:[][]string{[]string{"to the good", "有利,有好处"}, []string{"good, bad and indifferent", "好的,坏的和一般的"}, []string{"good innings", "长寿"}, []string{"good and ...", "很,颇;完全,彻底"}, []string{"do somebody's heart good", "对某人 的心脏有益,使某人感到愉快"}, []string{"do somebody good", "对某人有益"}, []string{"be good for", "对…有效,适合,胜任"}, []string{"be good at", "在…方面(学得,做得)好;善于"}, []string{"as good as one's word", "信守诺言,值得信赖"}, []string{"as good as", "实际上,几乎等于"}, []string{"all well and good", "也好,还好,很不错"}, []string{"a good", "相当,足足"}, []string{"He is good at figures . ", "他善于计算。"}}, Entry:"good", Type:"word", Related:[]interface {}{}, Source:"wenquxing"}}

打印结果

	...
	bodyText, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
+	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.Printf("%#v\n", dictResponse)
+	fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
+	for _, item := range dictResponse.Dictionary.Explanations {
+		fmt.Println(item)
+	}
}

完善代码

  • 代码的主体改成一个 query 函数,查询的单词作为参数传递进来
  • main 函数判断命令和参数的个数:如果不是两个,则打印出错误信息,退出程序;否则获取到用户输入的单词,然后执行 query 函数
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
)

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

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

func query(word string) {
	client := &http.Client{}
	request := DictRequest{TransType: "en2zh", Source: word}
	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)
	}
	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() {
	if len(os.Args) != 2 {
		fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
		`)
		os.Exit(1)
	}
	word := os.Args[1]
	query(word)
}
SOCKS5代理

SOCKS5 代理协议

  • 诞生于互联网早期
  • 明文传输
  • 用途
    • 某些企业的内网为了确保安全性设置了严格的防火墙策略,导致访问某些资源会很麻烦,SOCKS5 相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源
    • 很多翻墙软件最终暴露的是一个 SOCKS5 协议的端口
    • 爬虫在爬取过程中很容易会遇到 IP 访问频率超过限制,网上代理 IP 池里面的很多代理的协议就是 SOCKS5

效果:

> go run proxy/v4/main.go
2022/05/08 17:05:44 client 127.0.0.1:29559 auth failed:IPv6: no supported yet
2022/05/08 17:06:18 dial 198.18.2.70 80
>curl --socks5 127.0.0.1:1080 -v http://www.qq.com
*   Trying 127.0.0.1:1080...
* SOCKS5 connect to IPv6 2402:4e00:1430:1301:0:9227:79d3:ffd1:80 (locally resolved)
* connection to proxy closed
* Closing connection 0
curl: (97) connection to proxy closed

>curl --socks5 127.0.0.1:1080 -v http://www.baidu.com
*   Trying 127.0.0.1:1080...
* SOCKS5 connect to IPv4 198.18.2.70:80 (locally resolved)
* SOCKS5 request granted.
* Connected to 127.0.0.1 (127.0.0.1) port 1080 (#0)
> GET / HTTP/1.1
> Host: www.baidu.com
> User-Agent: curl/7.80.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
< Connection: keep-alive
< Content-Length: 2381
< Content-Type: text/html
< Date: Sun, 08 May 2022 09:06:18 GMT
< Etag: "588604c8-94d"
< Last-Modified: Mon, 23 Jan 2017 13:27:36 GMT
< Pragma: no-cache
< Server: bfe/1.0.8.18
< Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
<
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=http://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
* Connection #0 to host 127.0.0.1 left intact

原理

image.png

不经代理服务器经过代理服务器
和对方网站建立 TCP 连接

三次握手

发起 HTTP 请求

服务返回 HTTP 响应
浏览器和 SOCKS5 代理建立 TCP 连接→代理和真正的服务器建立 TCP 连接
↓↓↓

握手阶段
浏览器向 SOCKS5 代理发送请求,包的内容包括协议的版本号、支持的认证的种类

SOCKS5 服务器选中一个认证方式,返回给浏览器
返回 00 代表不需要认证,返回其他类型则进入认证阶段

请求阶段
认证通过后浏览器向 SOCKS5 服务器发起请求,主要信息包括版本号、请求类型(一般是 connection 请求,代表代理服务器要和某个域名或者某个 IP 地址某个端口建立 TCP 连接)

代理服务器收到响应后,与真正的后端服务器建立连接,返回响应

relay 阶段
浏览器正常发送请求→代理服务器接收请求后直接把请求转发到真正的服务器上→代理服务器将真正的服务器返回的响应直接转发到浏览器
⚠ 代理服务器并不关心流量的细节,可以是 HTTP 流量,也可以是其它 TCP 流量

TCP echo server

package main

import (
	"bufio"
	"log"
	"net"
)

func 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) // 新建 goroutine(开销小于子进程,可处理上万并发)
	}
}

func process(conn net.Conn) {
	defer conn.Close() // 函数结束时关闭连接
	reader := bufio.NewReader(conn) // 创建只读、带缓冲的流
	for {
		b, err := reader.ReadByte() // 底层实现会将按 Byte 读取合并
		if err != nil {
			break
		}
		_, err = conn.Write([]byte{b}) // 一般用 slice,此处仅做类型转换
		if err != nil {
			break
		}
	}
}

效果:

> go run proxy/v1/main.go
> ncat 127.0.0.1 1080 # 可直接建立 TCP l
hello
hello

auth

package main

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func 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) { // 只读流, 原始 TCP 连接
	// +----+----------+----------+
	// |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) // 创建 method 的缓冲区
	_, err = io.ReadFull(reader, method) // 用 io.ReadFull 填充满
	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
2022/03/20 02:39:03 ver 5 method [0 1]
2022/03/20 02:39:03 auth success
>curl --socks5 127.0.0.1:1080 -v http://www.qq.com
*   Trying 127.0.0.1...
* TCP_NODELAY set
* SOCKS5 communication to www.qq.com:80
* SOCKS5 connect to IPV4 116.128.170.212 (locally resolved)
* Closing connection 0
curl: (7) Faild to receive SOCKS5 connect request ack.

请求阶段

...
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")
+	err = connect(reader, conn)
+	if err != nil {
+		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
+		return
+	}
}
...
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) // 复用 4 个字节的缓冲区
		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
2022/03/20 02:46:08 dial 116.128.170.212 80
>curl --socks5 127.0.0.1:1080 -v http://www.qq.com
*   Trying 127.0.0.1...
* TCP_NODELAY set
* SOCKS5 communication to www.qq.com:80
* SOCKS5 connect to IPV4 116.128.170.212 (locally resolved)
* SOCKS5 request granted.
* Connected to 127.0.0.1 (127.0.0.1) port 1080 (#0)
> GET / HTTP/1.1
> HOST: www.qq.com
> User-Agent: curl/7.64/1
> Accept: */*
>
* Recv failure: Connection reset by peer
* Closing connection 0
curl: (56) Revc failure: Connection reset by peer

relay 阶段

  • net.dial 建立 TCP 连接

  • io.Copy 实现单向数据转发

    func Copy

    func Copy(dst Writer, src Reader) (written int64, err error)

    Copy copies from src to dst until either EOF is reached on src or an error occurs. It returns the number of bytes copied and the first error encountered while copying, if any.

    A successful Copy returns err == nil, not err == EOF. Because Copy is defined to read from src until EOF, it does not treat an EOF from Read as an error to be reported.

    If src implements the WriterTo interface, the copyis implemented by calling src.WriteTo(dst). Otherwise, if dst implements the ReaderFrom interface, the copy is implemented by calling dst.ReadFrom(src).

  • 启动两个 goroutine 实现双向数据转发

  ...
  port := binary.BigEndian.Uint16(buf[:2]) // 解析整型数值

+	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)
	...
	...
	_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
	if err != nil {
		return fmt.Errorf("write failed: %w", err)
	}
+	ctx, cancel := context.WithCancel(context.Background()) // 等待任何一个方向的 Copy 失败进而关闭连接
+	defer cancel() // 无意义
+
+	go func() {
+		_, _ = io.Copy(dest, reader)
+		cancel() // 出错时调用 cancel
+	}()
+	go func() {
+		_, _ = io.Copy(conn, dest)
+		cancel()
+	}()
+
+	<-ctx.Done() // 等待 context 执行完时关闭连接
	return nil
}