Go语言上手 - 基础语言|青训营笔记

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

一、本堂课内容重点

  • Go 语言基本语法
  • 猜数字游戏
  • 简易字典
  • SOCKS 5 代理

二、详细知识点介绍

1. Go 语言基本语法

(1)基本结构

  • main 包中的 main 函数是程序开始执行的地方。
  • import 导入函数包。
1
2
3
4
5
6
7
8
9
package main

import (
"fmt"
)

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

(2)变量

  • go 语言可以自动识别变量的数据类型。
  • var 变量名 数据类型 = 值
  • var 变量名 = 值
  • 变量名 := 值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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))
}

(3)循环

  • go 语言只有 for 这一种循环。
  • for 后面不用加 ()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
}
}

(4)判断

  • if 后面不用加 ()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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")
}
}

(5)选择

  • go 语言的 switch 不需要 break,执行完某一分支后默认会跳出 switch,而不会继续往下执行。
  • 可以使用 switch 来代替 if-else 的多重判断,效率更高。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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")
}
}

(6)数组

  • 数组长度固定,无法动态扩展。
  • var 数组名 [长度]数据类型
  • 数组名 := [长度]数据类型{元素1,元素2,……}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

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

(7)切片

  • 长度不固定,比数组更灵活。
  • 变量名 := make([]数据类型,初始长度)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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]
}

(8)Map

  • 存储的是键值对,map[key]value,使用 make 创建。
  • len(map):返回 map 的大小。
  • ok:判断某个键是否存在。
  • delete(map, key):删除键值对。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 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)
}

(9)遍历

  • for 下标, 元素值 := range 数组{……}。
  • for key, value := range map{……}。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
}
}

(10)函数

  • 使用 func 关键字定义函数。
  • 与大多数语言不同,参数列表中是先写变量名,再写变量类型。
  • 与大多数语言不同,函数先定义参数列表,再定义返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
}

(11)指针

  • 使用 * 声明一个指针。
  • 函数的形参为值传递,若想修改传入参数原本的值,应传入指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
}

(12)结构体

  • 使用 struct 定义一个结构体。
  • 结构体不仅有成员变量,还有成员方法,类似于 Java 中类的概念。
  • 通过 . 运算符获取成员。
  • 在函数名前加上 (形参名 结构体) 来指定函数为某个结构体的成员方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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

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

// 普通函数
func checkPassword(u user, password string) bool {
return u.password == password
}

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

// 结构体成员函数
func (u user) checkPassword(password string) bool {
return u.password == password
}

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

(13)异常处理

  • 函数返回的参数列表中可以加上一个 error,用于生成出错后的异常信息,用于排查问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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)
}
}

(14)字符串

常用函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
}

字符串转为其它数据类型:

  • strconv.ParseInt(“111”, 10, 64):10 表示进制数,64 表示输出 bit 长度。
  • strconv.Atoi(“123”):自动识别并转换为 int 类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"strconv"
)

func main() {
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
}

(15)fmt

  • %v:可以自动识别变量的数据类型。
  • %+v:打印详细信息。
  • %#v:打印更详细的信息。
  • %.2f:保留小数点后两位。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
}

(16)json

  • 结构体要想能被 json 序列化,需要保证每一个属性名的首字母大写。
  • buf, err := json.Marshal(结构体变量):将结构体序列化。
  • err = json.Unmarshal(buf, &结构体变量):将数据反序列化到结构体中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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"}}
}

(17)time

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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,这个时间是API中固定的
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
}

(18)env

  • os.Args:操作系统进程信息,获取命令行输入参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}

2. 猜数字游戏

(1)知识点总结

  • 使用 rand 生成随机数时,应先设置随机数种子,否则每次都会生成相同的随机数。这里用时间戳来初始化随机数种子。
  • 可以使用 input, err := reader.ReadString(‘\n’) 来读取一行输入,使用这种方式会读取到字符串最后的换行符(Windows 系统下为:”\r\n”),可以使用 input = strings.TrimSuffix(input, “\r\n”) 去掉最后的换行符,最后再使用 strconv.Atoi(input) 将输入字符串装换为 int 类型的数字。
  • 可以直接使用 _, err := fmt.Scanf(“%d”, &int类型的变量) 读取输入的数字。

(2)代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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")
reader := bufio.NewReader(os.Stdin)
for {
// 方式一:使用 reader 读取输入
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
continue
}
去掉末尾的换行符,Windows 环境下为 "\r\n"
input = strings.TrimSuffix(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
}
}
}

3. 简易字典

(1)实现过程

  1. 找一个字典网站,右键点击网页,打开检查,进行一次翻译。
  2. 选择:网络 -> dict -> POST 200 OK。
  3. 右键点击 dict 复制为 cURL(bash),打开 Convert curl commands to code (curlconverter.com),自动转为客户端的代码,选择 POST 的模板,用于向服务器发送请求。
  4. 选择右边的预览,右键选择复制值(不要直接全部选中并复制!),打开 JSON转Golang Struct - 在线工具 - OKTools,转为对应的结构体。
  5. 编写代码实现单词的查询。

(2)代码

  • 请求结构体:CaiYunDictRequest。
  • 响应结构体:CaiYunDictResponse。
  • 发送请求,并打印查询结果:CaiYunQuery(word string)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package main

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

// 彩云小译
type CaiYunDictRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
UserID string `json:"user_id"`
}

// 彩云小译
type CaiYunDictResponse 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 CaiYunQuery(word string) {
fmt.Println("彩云翻译:")
client := &http.Client{}
request := CaiYunDictRequest{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 CaiYunDictResponse
// 将 json 数据反序列化到结构体中
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 simpledict\v4\main.go hello,不能直接点击运行

// os.Args: 进程信息
// 如:xxx.main.go hello
// os.Args[0] = xxx.main.go
// os.Args[1] = hello
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
`)
os.Exit(1)
}
word := os.Args[1]
CaiYunQuery(word)
}

4. SOCKS 5 代理

(1)代理流程

  1. 客户端与代理进行协商。
  2. 认证(这一步可以跳过,不加密时不需要认证)。
  3. 建立 TCP 连接。
  4. 传输数据。

Image

(2)代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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 = 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
}
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])

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

三、实践练习例子

1. 修改猜数字里的代码,用 fmt.Scanf 来实现对输入的读取。

1
2
3
// 方式二:使用 scanf 读取输入
var guess int
_, err := fmt.Scanf("%d", &guess)

2. 修改字典的代码,增加一种翻译引擎的支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// 火山翻译
type HuoShanDictResponse struct {
Words []struct {
Source int `json:"source"`
Text string `json:"text"`
PosList []struct {
Type int `json:"type"`
Phonetics []struct {
Type int `json:"type"`
Text string `json:"text"`
} `json:"phonetics"`
Explanations []struct {
Text string `json:"text"`
Examples []struct {
Type int `json:"type"`
Sentences []struct {
Text string `json:"text"`
TransText string `json:"trans_text"`
} `json:"sentences"`
} `json:"examples"`
Synonyms []interface{} `json:"synonyms"`
} `json:"explanations"`
Relevancys []interface{} `json:"relevancys"`
} `json:"pos_list"`
} `json:"words"`
Phrases []interface{} `json:"phrases"`
BaseResp struct {
StatusCode int `json:"status_code"`
StatusMessage string `json:"status_message"`
} `json:"base_resp"`
}

// 属性名应以大写字母开头,这样才能被 json 序列化
type HuoShanDictRequest struct {
Text string `json:"text"`
Language string `json:"language"`
}

// 火山翻译
func HuoShanQuery(word string) {
fmt.Println("火山翻译:")
client := &http.Client{}
request := HuoShanDictRequest{Text: word, Language: "en"}
buf, err := json.Marshal(request)
if err != nil {
log.Fatal(err)
}
var data = bytes.NewReader(buf)
req, err := http.NewRequest("POST", "https://translate.volcengine.com/web/dict/match/v1/?msToken=&X-Bogus=DFSzswVLQDc7ENhSSWQozKXAIQ5U&_signature=_02B4Z6wo00001GGK6hwAAIDBAoAqdvxNqbRhiu6AAHocmoUrJAQ8U5hOjU1IijwafuIAQ8rez.ZVof4AguPq4onq7EHjcpXm7dkiN1zIz1rnT1gUoVH0ijSZwYmCO6OOM5Lt825xxeck4pGc13", data)
if err != nil {
log.Fatal(err)
}
req.Header.Set("authority", "translate.volcengine.com")
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("content-type", "application/json")
req.Header.Set("cookie", "x-jupiter-uuid=16519255681333224; ttcid=21a3425bd9a448209377efd18a0dce7b65; i18next=translate; s_v_web_id=verify_5157f95d93aaa92c44457671ecad7b03; _tea_utm_cache_2018=undefined; tt_scid=ZQCyNtbQAV2ppnMXc6BD8hn8-zTGAAMs9PdrZ1UaYI0DM5F3BHvnmb-NWjBBlUaB7715")
req.Header.Set("origin", "https://translate.volcengine.com")
req.Header.Set("referer", "https://translate.volcengine.com/translate?category=&home_language=zh&source_language=detect&target_language=zh&text=good")
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"`)
req.Header.Set("sec-fetch-dest", "empty")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-site", "same-origin")
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")
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 HuoShanDictResponse
// 将 json 数据反序列化到结构体中
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
for _, word := range dictResponse.Words {
for _, pos := range word.PosList {
for _, explanation := range pos.Explanations {
text := explanation.Text
fmt.Println(text)
}
}
}
}

3. 在 2 的基础上实现并行请求两个翻译引擎。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
// 应在终端输入:go run simpledict\v4\main.go hello,不能直接点击运行

// os.Args: 进程信息
// 如:xxx.main.go hello
// os.Args[0] = xxx.main.go
// os.Args[1] = hello
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
`)
os.Exit(1)
}
word := os.Args[1]
go CaiYunQuery(word)
time.Sleep(time.Second)
HuoShanQuery(word)
time.Sleep(time.Second)
}

四、课后个人总结

通过本节课程的学习,让我对 go 语言的基本语法有了一个大致的了解,并且在练习的过程中进一步熟悉了 go 代码的编写。另外,在学习项目代码的过程中,我还学习到了许多以前不太了解的计算机网络以及 web 相关的知识,扩展了我的知识面,感觉受益良多,非常期待下一次课程的学习。