Go 语言入门指南:基础语法和常用特性解析 |豆包MarsCode AI 刷题

110 阅读23分钟

GO语言介绍

什么是Go语言(特性)

  1. 高性能、高并发
  2. 语法简单、学习曲线平缓(类似于C语言,且循环只有for)
  3. 丰富的标准库(和python一样,拥有丰富标准库,不需要第三方库;稳定性高,可以持续享受语言迭代带来的持续优化)
  4. 完善的工具链(内置了完整的测试框架)
  5. 静态链接(部署方便快捷)
  6. 快速编译
  7. 跨平台(可以用来开发ios、安卓等软件)
  8. 垃圾回收(和java类似,写代码的时候只要专注于代码就可以)

入门

开发环境

安装Golang

Golang官网

image.png download后按照教程提示下载即可

配置集成开发环境

image.png 👆这个是vscode中下载golang插件 image.png

这边不多说,我是用AI练中学,更方便一些。

基础语法

Hello World

package main

import (
	"fmt"
)

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

这是一个简单的Hello World代码。

  • main包就是程序的入口包。
  • 第3行开始导入了标准库里的fmt包,这个包用来向屏幕输出字符串、格式化字符串。
  • 第7行开始是main函数,使用fmt.Println打印hello world。

变量 and 常量

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

go中变量有字符串、整型、boolean类型、浮点型等。字符串之间可以用+进行连接,如代码中第20行。

变量的声明有两种方式:

  1. var a = "initial",用var可以自动推测输入的变量的类型。也可以显式地进行声明,如第12行中的int
  2. f := float32(e)也可以用变量:=值的方式进行声明。

常量的声明(第24行开始):在golang里面常量没有确定的类型,会根据上下文自动确定类型。

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

写法和c很像,需要注意的是,if后面没有括号!!! 如果写了括号,编译器在保存的时候会自动去掉括号。

第17行代码的意思是,声明一个变量num = 9,然后看它是否满足num < 0,如果满足则执行if内的语句。

for循环

golang只有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
	}
}

如果for里面什么都不写,就相当于一个死循环。就会像下面这张图片一样,一直这样。

image.png 代码第22行到25行像极了java中的while循环语句。

switch

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

需要注意,代码第11行,switch后面也是不加括号,以及每个case分支中没有break就可以只执行符合条件的分支,这个是和java有区别的地方。

switch后也可以不加任何东西,这样避免了多个switch嵌套的问题。

数组

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

长度是固定的,比较少用。

这边与java的区别是,第10行,数组的长度用len(数组)来表示。fmt.Println(数组)打印出的是[数组元素(空格隔开)]而不是数组地址值。

切片 slice

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

新东西!

切片是一个可变长度的数组,我们可以任意时刻去更改长度。

第7行,用make([]string, 3)来创建切片;第14行和第15行,用append(s, "d")来增添元素,注意append必须将结果赋值给原数组;可以用copy在两个slice之间拷贝数据。

拥有和python一样的切片操作。例如,第22行s[2:5]表示取s中从下标2到下表4的元素。取值范围是[startIndex, endIndex)。

map

golang中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 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:make(map[string]int)

第7行和第8行读取key-value对

第11行和12行用m[key]读取value

第14行,用ok来获取这个map里面是否存在当前的key

range

可以用range来遍历数组

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

当遍历map时,for后两个参数分别代表key和value。如果只有一个参数,则代表key

函数

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
}

函数返回的变量类型是后置的func add(a int, b int) int {

第13行到16行同时返回了两个值。用map的v, ok特性,可以判断是否exist问题。

指针

指针的用途:对传入的参数进行修改。

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
}

第5行到第7行,这样的相加操作是不起作用的,因为函数内相加后出函数时变量销毁,所以相加操作不成立。这时就要用到指针。第9行到第11行,通过加星号*,将其加上指针,对应的代码第17行加上&符号,于是相加操作成立。

结构体

我觉得这个结构体很像java中的类

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
}

type user struct创建结构体 第11行到第17行为赋值。没有赋值的都会设置为空值。注意,像第12行b := user{"wang", "1024"}这种写法必须把结构体里面出现的全部赋值,否则会报错。

第24行到第26行和第28行到第30行的区别是后者运用指针,可以对原始数值进行修改,但是这边貌似没体现。

结构体方法

类似其他语言中的类成员函数。

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
}

和之前的区别是:(u user)位置变了。这样实现了user.xxxxx的方法调用。而它就从一个普通函数变成类成员函数。

带指针的可以对类结构成员进行修改。如第14行到第16行。

错误处理

使用一个单独的返回值来传递错误信息。

不同于java里面使用的错误异常,golang可以很清晰地知道哪个函数发生错误,并且用if-else去处理错误。

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

在函数里面,我们在函数返回值中加一个err error,这代表这个函数可能返回错误。第13到第20行,如果要return,就同时return两个值。如果没有错误就返回&变量, nil;如果有错误,就返回nil, errors.New(错误信息)

在调用有返回错误的函数时,接收需要有两个变量,如第23行。

err != nil,即有发生错误、错误信息。

字符串操作

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
}

Contains(字符串, 某字符串):判断字符串中是否包含某字符串

Count(字符串, 某字符串):字符串计数

HasPrefix(字符串, 某字符串):是否有某字符串前缀

HasSuffix(字符串, 某字符串):是否有某字符串后缀

Index(字符串, 某字符串):获取某字符串下标

Join(字符串, 某字符串):将一个字符串中的元素以某字符串进行连接

Repeat(字符串, 次数):重复字符串

Replace(字符串, 原字符串, 新字符串, -1):替换字符。这边的-1如果不指定,则只替换第一个匹配字符。如果有-1,则替换所有匹配字符。

Split(字符串, 某字符串):遇见某字符串就将其分开,返回类型为数组

ToLower(字符串):变成小写

ToUpper(字符串):变成大写

对于中文,一个字可能对应多个字符。

字符串格式化

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
}

%v可打印任意类型的变量

%+v可以得到更加详细的结构

%#v更加详细

%.2f打印保留两位小数的浮点数 这里是四舍五入的保留两位小数!

JSON处理

结构体字段名首字大写即可变成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("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}

json.Marshal(a)数列化

json.Unmarshal(buf, &b)反数列化

输出的字段名默认为首字母大写的字段名。如果要使得字段名为小写,可以如第10行代码,在结构体中的字段名后面加上tag

时间处理

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
}

time.Now():快速获取当前时间

time.Date():构造一个带时区的时间

第14行中有很多方法来获取时间的信息。

t2.Sub(t):对两个时间做减法,得到一个时间段,可以通过diff.Minutes(), diff.Second()得到有多少分钟、多少秒。

now.Unix():获取时间戳

数字解析

都在strconv这个包下

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
}

strconv.ParseInt(字符串, 进制, 精度整数):如果进制为0,则自动推测

strconv.Atoi("123"):快速把一个十进制字符串转化为数值

strconv.Itoa(123):将数字转为字符串

获取进程相关信息

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
}

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

os.Getenv("PATH"):获取环境变量

os.Setenv():写入环境变量

实战

猜谜游戏

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
		}
	}
}
  1. 第14行到16行,首先生成随机数:使用rand.Intn(n),记得要先导入math/rand包。表示随机生成一个范围在[0, n)内的随机整数。第15行生成时间戳,才能确保随机数生成成功。BUT不知道为什么,我这里不写也可以生成随机数==

  2. 第20行,读取用户输入:

    • 记得导入bufioosstrconvstrings包。
    • 第22行,bufio.NewReader从标准输入读取一行文本,直到遇到换行符为止。
    • 第23行到第26行,如果读取输入时发生错误,打印错误信息并继续循环,等待玩家重新输入。
    • 第27行,strings.Trim函数去除输入字符串中的回车符和换行符,以确保输入的是一个整数。
    • 第29行到第33行,strconv.Atoi 将输入的字符串转换为整数,如果转换失败,打印错误信息并继续循环。
  3. 第35行到42行,实现判断逻辑:如果猜测的数字大于或小于随机数字,输出消息提示;如果猜测的数字等于随机数字,则输出消息提示并break循环。

游戏效果:

image.png

在线词典

实现效果:用户输入一个单词,程序会返回这个单词的音标和中文注释。

原理:调用第三方API去查询到单词的翻译,并且打印出来。

这个例子中,我们将学习go语言如何发送http请求、解析json,以及学习如何使用代码生成以提高开发效率

抓包演示

彩云小译官网

image.png 以这个彩云小译为例,先输入想要翻译的单词;然后鼠标右键,点击检查;找到Network里面的dict,其中带有post的语句就是它发送的http请求。

代码生成

代码生成器网址

image.png 找到Network里面的dict,鼠标右键找到Copy并找到Copy as cURL,这样在终端就可以看见代码了。

image.png 把刚刚复制的cURL复制到curl command中,就会生成下面那一大串代码。

生成代码解读
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)
}

strings.NewReader() 将字符串转换成流。

第14行:创建请求。http.NewRequest(请求类型, url, data是一个流)

第18行到35行:设置请求头

第36行:发起请求

第40行:在golang中为了避免资源泄漏,需要加defer手动关闭流。defer会在函数结束后从下往上触发。这里只有一个defer,那就是在函数结束后调用Close()函数

第41行:读取响应。把整个流读到内存里面并变成一个数组

生成 request body

构造请求结构体

type DictRequest struct{
    TransType string `json:"trans_type"`
    Source string `json:"source"`
    UserID string `json:"user_id"`
}
func main(){
    ...
    request := DictRequest{TransType: "en2zh", Source: "good"}
    buf, err := json.Marshal(request)
    if err != nil {
        log.Fatal(err)
    }
    ...
}

其他与上面一个模块的代码差不多。

解析 response body

JSON转Golang Struct

把刚刚网页里面preview中的json字符串复制到这个网站对应的JSON框里面,然后点击转化就可以实现JSON转Golang Struct

sadness,我这个网站打不开,所以体验不了o.O

更新!这个链接可以!!

出错了!

image.png 按照AI的提示进行修改还是运行不了,真的是难过了...有同学知道解决方法的可以在评论区留下言,我去捞捞!!!

所以这个实例我只能先跳过了呜呜呜

SOCKS5 代理介绍

SOCKS5相当于在防火墙开了一个口子,让授权的用户可以通过单个端口访问内部的所有资源。实际上很多翻墙的软件最终暴露的也是一个SOCKS5协议的端口。

原理

image.png

分为三个阶段:

  1. 握手阶段:浏览器会向SOCKS5代理发送请求,包的内容包括一个协议版本号以及支持的认证种类,SOCKS5服务器会选中一个认证方式,返回给浏览器。
  2. 认证阶段
  3. 请求阶段:认证通过后浏览器会给SOCKS5服务器发请求,主要信息包括版本号、请求类型等,一般是connecttion请求,也就是建立TCP连接
  4. relay阶段:此时浏览器会发请求,然后代理服务器接收到请求后会直接把请求转换到真正的服务器上。如果真正服务器返回响应,那么也会把请求转发到浏览器。

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

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

这段代码实现的就是我们给它发什么,它就返回什么。

  • 第10行:增添端口,返回一个server
  • 第15行:用server.Accept()接受一个请求。 如果成功则返回一个成功连接的消息提示。 接下来在process()函数中处理连接 第20行的go可以理解为启动一个子线程来处理这个连接
  • 第25行:在函数结束的时候一定要把这个连接关掉
  • 第26行:创建一个只读的带缓冲的流
  • 第28行:每次读一个字节
  • 第33行:conn.Write()写入字节

认证 auth

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

...

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
}
  • 第32行:先读到了版本号,所以第33行到38行先判断版本号
  • 第39行:读methodSize,同样是单个字节
  • 第43行:读到了methodSize之后用methodSize创建一个缓冲区;用io.ReadFull()进行填充
  • 第54行:构造包

运行代码,返回结果: image.png

image.png

可以看到,返回了版本号

这里有一个坑,大家启动需要开一个窗口,输入命令行要再开一个窗口,否则没有反应。

请求阶段

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 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", cmd)
	}
	addr := ""
	switch atyp {
	case atypeIPV4:
		_, 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 服务绑定的地址    4个字节
	// BND.PORT 服务绑定的端口DST.PORT    2个字节
	_, 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
}
  • 第31行:创建一个长度为4的缓冲区,用io.ReadFull把它填充满,接下来对于每个字段都验证它的合法性。
  • 第44行到第66行:对atyp类型进行分类处理。
  • 第67行:用切片语法将前面长度为4的缓冲区裁剪为长度为2的缓冲区并将其填充满。
  • 第71行:用binary.BigEndian.Uint16()解析出数字。

效果:打印出ip和端口

image.png

relay 阶段

在上一模块代码第72行处添加:

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

在上一模块代码第89行和90行之间添加:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go func() {
	_, _ = io.Copy(dest, reader)
	cancel()
}()
go func() {
	_, _ = io.Copy(conn, dest)
	cancel()
}()

<-ctx.Done()

先用go启动goroutine,然后分别调用io.Copy

两个拷贝的方向不一样。第一个是从用户的浏览器拷贝数据到底层的服务器;另一个是从底层的服务器拷贝数据到用户的浏览器。两个方向刚好相反。

context.WithCancel()来创建一个context,最后等待ctx.Done()。只要cancel()被调用,ctx.Done()就会立刻返回。这样实现了任何一个方向的copy失败,那么就返回此时的函数并且把双方的函数都关闭掉。

完整代码

package main

import (
	"bufio"
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 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", cmd)
	}
	addr := ""
	switch atyp {
	case atypeIPV4:
		_, 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
}

大家可以用SwitchyOmega进行测试

小结

对于我一个有java基础的go小白来说,基础语言的入门还是比较好理解的,但是在实战小项目的模块还是很懵的,42分钟的课听了3个小时,而且也没办法完全弄懂,有一种在搭建空中楼阁的感觉。

这篇笔记写了两天,对go语言还算是有了比较基础的认知。

参考资料