跳到主要内容

Go语言入门

安装

Mac M1 Pro:https://golang.google.cn/dl/

选择Arm,PKG格式的安装,默认会安装到/usr/local/go文件夹下面

配置环境变量~/.bash_profile

# 下面是配置go环境
GOPATH=/usr/local/go
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOBIN

查看版本以及是否配置环境变量成功:

go version

image-20241102102707280

基础语法

HelloWorld

可以使用Jerbrain的GoLand集成开发环境进行开发

package main

import (
"fmt"
)

func main() {
s := "cxk"
fmt.Println("Hello and welcome, %s!", s)
for i := 1; i <= 5; i++ {
fmt.Println("i =", 100/i)
}
}

变量

在 Go 语言中,变量定义遵循以下规则:

  1. 声明变量
    • 使用 var 关键字可以声明变量。
    • 可以在同一行中声明多个变量,类型可以相同或不同。
  2. 初始化变量
    • 变量可以在声明时直接赋值,也可以稍后赋值。
    • 如果没有显式初始化,变量会被赋予其类型的零值(例如,整数为 0,布尔值为 false,字符串为空等)。
  3. 简短声明
    • 使用 := 语法可以在函数内声明并初始化变量,无需使用 var 关键字。
  4. 常量
    • 使用 const 关键字定义常量,常量在编译时就确定其值,不能被修改。
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)
fmt.Println(g)

const s string = "constant"
const h = 500000000
const i = 3e20 / h
fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}

分支

if-else分支:if 后面的条件表达式必须是布尔类型,返回 truefalse

可以在 if 语句中进行短变量声明。在这种情况下,变量的作用域仅限于 if 语句块内

package main

import (
"fmt"
)

func main() {
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}

if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
}

switch分支:

switch 语句用于根据不同的条件执行不同的代码块。

可以通过指定表达式来进行匹配,如果不指定,默认匹配 true

可以在同一个 case 中列出多个条件,用逗号分隔

可以不使用表达式,直接写多个 case,通常用于范围判断或布尔条件。

switch 默认不会穿透到下一个 case,如果需要,可以使用 fallthrough 关键字显式地继续执行下一个 case

package main

import (
"fmt"
"time"
)

func main() {
a := 2
switch a {
case 1:
fmt.Println("a is 1")
case 2:
fmt.Println("a is 2")
case 3:
fmt.Println("a is 3")
case 4, 5:
fmt.Println("a is 4 or 5")
default:
fmt.Println("a is not 1, 2, 3, 4 or 5")
}
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
default:
fmt.Println("Good afternoon!")
}
}

循环

无限循环:可以在 for 后面不写条件,形成一个无限循环,通常与 break 配合使用。

计数循环:通过初始化语句、条件和迭代语句来实现传统的计数循环,适合已知次数的操作。

条件循环:可以直接在 for 后写条件,循环将持续执行直到条件不再满足,类似于 while 循环。

continue 语句:使用 continue 可以跳过当前迭代的剩余部分,直接进入下一次迭代。

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

数组

用法:

数组声明:使用 var 关键字声明数组,可以指定长度和元素类型。例如,可以声明一个固定长度的整型数组。

元素赋值:可以通过索引直接对数组的元素进行赋值,索引从 0 开始。

数组长度:使用 len() 函数可以获取数组的长度,数组长度是固定的。

数组初始化:可以在声明时通过字面量初始化数组,直接为每个元素赋值。

多维数组:Go 支持多维数组,可以通过嵌套数组的方式定义,例如二维数组,并且可以通过嵌套循环对其进行赋值。

输出:可以使用 fmt.Println() 输出数组的元素或整个数组,方便查看数组的内容和结构。

更多情况下会使用切片而不是数组

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

切片

可以理解为可变数组,类似于Java的ArrayList

切片声明:可以使用 make() 函数创建切片,指定其类型和初始长度。切片是动态大小的数据结构,适合存储可变长度的数据。

元素赋值:可以通过索引直接对切片的元素进行赋值,索引从 0 开始,切片的长度可以在运行时变化。

获取长度:使用 len() 函数可以获取切片的长度,便于在处理数据时进行控制。

追加元素:可以使用 append() 函数向切片追加元素。append() 函数可以一次追加一个或多个元素,并返回新的切片。

复制切片:可以使用 copy() 函数将一个切片的元素复制到另一个切片,确保数据的独立性。

切片操作:通过切片的索引可以获取子切片,使用 s[start:end] 语法来截取切片的一部分。

切片字面量:可以直接声明并初始化切片,通过字面量的方式指定元素,简洁方便。

package main

import "fmt"

func main() {
s := make([]string, 3)
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println("get[2]:", s[2])
fmt.Println("len:", len(s))

s = append(s, "d")
s = append(s, "e", "f")
fmt.Println("apd:", s)

c := make([]string, len(s))
copy(c, s)
fmt.Println("cpy:", c)

fmt.Println(s[2:5])
fmt.Println(s[:5])
fmt.Println(s[2:])

good := []string{"g", "o", "o", "d"}
fmt.Println("dcl:", good)
}

map

类似于Java中的HashMap

映射声明:可以使用 make() 函数创建映射,指定键值对的类型。映射是一种无序的键值对集合,适合快速查找和存储数据。

元素赋值:通过键直接对映射的元素进行赋值,如果键不存在,会自动添加。

获取长度:使用 len() 函数可以获取映射中键值对的数量。

获取元素:通过键可以获取对应的值,使用 r, ok := mp[key] 语法可以检查键是否存在。

删除元素:可以使用 delete() 函数从映射中删除指定的键及其对应的值。

映射字面量:可以通过字面量的方式直接声明并初始化映射,简洁方便。

package main

import "fmt"

func main() {
mp := make(map[string]int)
mp["one"] = 1
mp["two"] = 2

fmt.Println(mp)
fmt.Println(len(mp))
fmt.Println(mp["one"])
fmt.Println(mp["two"])

r, ok := mp["unknown"]
fmt.Println(r, ok)

delete(mp, "one")

m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
fmt.Println(m2, m3)
}

range

range 关键字的用法主要用于遍历切片、映射和其他数据结构,具体包括以下几个方面:

  1. 遍历切片:使用 for i, num := range nums 可以遍历切片,i 是当前元素的索引,num 是对应的值。这样可以在遍历的同时获取每个元素的索引和值。
  2. 计算和条件判断:在遍历切片的过程中,可以对元素进行计算(如求和)和条件判断(如查找特定元素的索引)。
  3. 遍历映射:使用 for k, v := range m 可以遍历映射,k 是键,v 是对应的值,这使得访问映射中的每个键值对变得简单。
  4. 仅遍历键:如果只需要遍历映射的键,可以使用 for k := range m,这样只会获取键而忽略值。
  5. 简洁性range 提供了一种简洁的方式来遍历各种数据结构,避免了手动管理索引或键的复杂性。
package main

import "fmt"

func main() {
nums := []int{2, 7, 11, 15}
sum := 0
for i, num := range nums {
sum += num
if num == 7 {
println("index of 7 is ", i)
}
}
fmt.Println("sum:", sum)

m := map[string]string{"name": "zhangsan", "age": "20"}
for k, v := range m {
fmt.Println(k, v)
}

for k := range m {
fmt.Println(k) //只打印key
}
}

函数

函数定义:使用 func 关键字定义函数,后接函数名、参数列表和返回值类型。函数可以接收一个或多个参数,并返回一个或多个值。

参数类型:在参数列表中,可以为每个参数单独指定类型,也可以在同一行中为多个参数指定相同类型,例如 add2(a, b int)

返回值:函数可以直接返回值,也可以使用命名返回值。在命名返回值的情况下,可以在函数体内直接赋值,最后使用 return 返回即可。

调用函数:在 main 函数中可以调用定义的函数,传入参数并接收返回值。

映射作为参数:函数可以接收映射作为参数,并在函数内部访问映射的元素,这使得函数更加灵活。

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] //v is the value, ok is a boolean
return v, ok
}

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

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

}

指针

在 Go 语言中,函数参数的传递方式主要有值传递和指针传递:

  1. 值传递:默认情况下,函数参数是通过值传递的。在函数内部对参数的修改不会影响到外部变量。例如,在 add 函数中对 n 的修改不会改变 main 函数中的 n 的值。
  2. 指针传递:通过传递变量的指针,可以在函数内部修改外部变量的值。在 add2ptr 函数中,通过传递 &nn 的地址),可以直接修改外部变量的值。
  3. 指针解引用:在指针参数中,使用 *n 来解引用,从而访问和修改指针指向的值。
  4. 函数调用:在 main 函数中,通过调用函数并传入相应的参数,可以观察到值传递和指针传递的效果。
package main

import "fmt"

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

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

func main() {
n := 5
add(n)
fmt.Println(n) // 5

add2ptr(&n)
fmt.Println(n) // 7
}

结构体

在 Go 语言中,结构体的用法包括以下几个方面:

  1. 结构体定义:使用 type 关键字定义结构体,结构体由一组字段组成,可以包含不同类型的数据。字段可以指定名称和类型。
  2. 结构体实例化:可以通过字段名或按顺序初始化结构体。例如,可以使用 user{name: "admin", password: "admin"}user{"admin", "admin"} 进行实例化。
  3. 字段访问和修改:可以通过点(.)操作符访问和修改结构体的字段。例如,c.password = "123456" 修改了 cpassword 字段。
  4. 方法定义:可以为结构体定义方法,使用接收者(receiver)来关联方法与结构体。通过方法可以对结构体进行操作或提供功能。
  5. 值传递与指针传递:在函数中可以通过值传递(传递结构体副本)或指针传递(传递结构体的地址)来访问和修改结构体。
  6. 方法调用:可以通过点操作符调用结构体的方法,如 a.checkPassword("admin")
package main

import "fmt"

type user struct {
name string
password string
}

func main() {

a := user{name: "admin", password: "admin"}
b := user{"admin", "admin"}
c := user{name: "cxk"}
c.password = "123456"
var d user
d.name = "cxk"
d.password = "1024"
fmt.Println(a, b, c, d)
fmt.Println(checkPassword(a, "admin"))
fmt.Println(checkPassword2(&a, "admin"))

fmt.Println(a.checkPassword("admin"))

}

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
}

错误处理

在 Go 语言中,错误处理和指针的用法包括以下几个方面:

  1. 错误处理:Go 语言通过返回值来处理错误,通常在函数返回多个值时,最后一个值是一个 error 类型,用于指示函数是否成功执行。例如,findUser 函数返回一个指向 user 的指针和一个错误值。
  2. 条件检查:在调用可能返回错误的函数后,通常使用 if err != nil 来检查是否发生了错误。如果发生错误,可以通过打印错误信息或采取其他处理措施。
  3. 返回指针:在 findUser 函数中,返回的是指向用户结构体的指针。在遍历用户切片时,通过 return &u 返回找到用户的地址。注意,直接返回结构体的地址可能会导致问题,因为循环中的 u 是局部变量。
  4. 局部变量作用域:在 main 函数中,使用 if u, err := findUser(...); 的形式可以在条件语句内定义局部变量 uerr,该变量在 if 语句块内有效,有助于避免变量冲突。
  5. 简洁性:通过错误处理机制,Go 提供了一种清晰且直观的方式来管理和响应运行时错误,增强了代码的可读性和可维护性。
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("user not found")
}

func main() {
u, err := findUser([]user{{"a", "b"}}, "a")
if err != nil {
fmt.Println(err)
return
}

fmt.Println(u.name)

if u, err := findUser([]user{{"a", "b"}}, "b"); err != nil {
fmt.Println(err)
} else {
fmt.Println(u.name)
}

}

实战案例

猜数字游戏

输入处理的方式如下:

  1. 使用 bufio 包:通过 bufio.NewReader(os.Stdin) 创建一个新的读取器,专门用于从标准输入(键盘)读取数据。
  2. 读取输入:使用 reader.ReadString('\n') 方法读取用户输入,直到遇到换行符为止。这将返回用户输入的字符串,包括换行符。
  3. 处理换行符:使用 strings.TrimSuffix(input, "\n") 去除字符串末尾的换行符,以便后续处理。
  4. 字符串转换为整数:通过 strconv.Atoi(input) 将输入的字符串转换为整数。如果转换失败,会返回一个错误。
  5. 错误处理:在读取和转换过程中,程序通过检查返回的错误,判断用户输入是否合法,并给予相应提示。
package main

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

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

fmt.Println("Please input a number between 0 and", maxNum)
reader := bufio.NewReader(os.Stdin)
for {
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occurred while reading input. Please try again", err)
continue
}
input = strings.TrimSuffix(input, "\n")
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer")
continue
}
fmt.Println("You guessed:", guess)
if guess > secretNum {
fmt.Println("Your guess is too high")
} else if guess < secretNum {
fmt.Println("Your guess is too low")
} else {
fmt.Println("Congratulations! You guessed the secret number ")
break
}
}
}

命令行词典

curl转go:https://curlconverter.com/

json转go:https://oktools.iokde.com/json2go

结构体后面的部分是结构体标签,用于指定在 JSON 编码和解码时字段的名称。例如,TransType 将被序列化为 "trans_type"Source 将被序列化为 "source"

  1. 序列化(将结构体转换为 JSON):
    • 使用 json.Marshal 函数将 DictRequest 结构体实例 request 转换为 JSON 格式的字节切片。
    • buf, err := json.Marshal(request)request 序列化为 JSON 字符串,存储在 buf 中。如果发生错误,会记录并终止程序。
  2. 反序列化(将 JSON 转换为结构体):
    • 使用 json.Unmarshal 函数将从 HTTP 响应中读取的 JSON 数据转换为 DictResponse 结构体。
    • err = json.Unmarshal(bodyText, &dictResponse) 将响应的 JSON 数据解析为 dictResponse,如果发生错误,会记录并终止程序。
package main

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

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

type DictResponse struct {
Rc int `json:"rc"`
Wiki struct {
} `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: "boy",
}
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("accept", "application/json, text/plain, */*")
req.Header.Set("accept-language", "zh")
req.Header.Set("app-name", "xiaoyi")
req.Header.Set("authorization", "bearer")
req.Header.Set("content-type", "application/json;charset=UTF-8")
req.Header.Set("device-id", "40fc51033a5ba5e96b5fc956ca6175a5")
req.Header.Set("origin", "https://fanyi.caiyunapp.com")
req.Header.Set("os-type", "web")
req.Header.Set("os-version", "")
req.Header.Set("priority", "u=1, i")
req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
req.Header.Set("sec-ch-ua", `"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("sec-ch-ua-platform", `"macOS"`)
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 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36")
req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
bodyText, err := io.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)
}
log.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)
}
}

func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s word\n", os.Args[0])
os.Exit(1)
}
word := os.Args[1]
query(word)
}

运行结果:

image-20241102133244536

Sockets5代理

echo server

具体步骤:

  1. 创建监听器

    • 使用 net.Listen("tcp", "127.0.0.1:1080") 创建一个 TCP 监听器,绑定到本地的 1080 端口。如果创建失败,程序会调用 panic 终止运行。
  2. 接受连接

    • 在无限循环中,使用 server.Accept() 接受客户端的连接请求。如果发生错误,则记录错误并继续下一次循环。
  3. 并发处理

    • 每当接受到一个客户端连接,使用 go process(client) 启动一个新的 Goroutine 来处理该连接。这样可以同时处理多个客户端请求。
  4. 处理客户端连接

    • process 函数中,使用 defer conn.Close() 确保在函数结束时关闭连接。

    • 创建一个 bufio.NewReader 以从连接中读取数据。

    • 使用 reader.ReadByte() 循环读取字节。如果读取成功,将读取到的字节通过 conn.Write 写回给客户端,实现回显功能。如果读取或写入过程中发生错误,循环将终止。

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 error: %v\n", 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
}
}
}

auth

认证函数: auth 函数,该函数负责处理客户端的认证请求。认证过程遵循 SOCKS5 协议的规范:

  • 读取版本号并验证是否为 SOCKS5 协议(0x05)。
  • 读取支持的认证方法的数量和具体方法。
  • 打印协议版本和方法信息以供调试。
  • 向客户端发送认证成功的响应(socks5Ver, 0x00,表示无认证)。
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 error: %v\n", err)
continue
}
go process(client)
}
}

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
/**
格式
ver | nmethods | methods
1 | 1 | 1-255
ver:协议版本,socket5默认为0x05
nmethods:M支持认证的方法数量
methods:支持的nmethods的值为多少,methods就有多少个字节
*/
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read ver error: %v", err)
}
if ver != socks5Ver {
return fmt.Errorf("not supported ver %v", ver)
}
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read method size error: %v", err)
}
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("read method error: %v", err)
}
log.Println("ver", ver, "method", method)
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write response error: %v", err)
}
return nil
}

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

使用curl去请求

image-20241102135632906

打印结果:

image-20241102135644041

请求阶段

在这个 Go 程序中,添加了 SOCKS5 协议的连接处理功能,具体做了以下几点:

  1. 处理连接请求
    • process 函数中,调用 connect 函数来处理客户端的连接请求。
  2. 连接请求的格式
    • connect 函数解析客户端发送的连接请求,根据 SOCKS5 协议的格式读取请求头信息,包括版本号(VER)、命令(CMD)、保留字段(RSV)和目标地址类型(ATYP)。
  3. 解析目标地址和端口
    • 根据 ATYP 的值,处理不同类型的目标地址:
      • IPv4 地址:读取 4 字节的 IP 地址并格式化为字符串。
      • 域名地址:首先读取域名的长度,然后读取对应长度的字节作为域名。
      • IPv6 地址:当前不支持,直接返回错误。
    • 读取目标端口,使用大端字节序转换为 16 位无符号整数。
  4. 建立连接
    • 打印出要连接的地址和端口。
  5. 构造连接响应
    • 构造并发送响应给客户端,表示连接成功。响应的格式包括 SOCKS5 版本号、命令返回码(成功为 0x00)、保留字段、地址类型(这里固定为 0x01 表示 IPv4)、绑定地址(这里为 0.0.0.0)和绑定端口(通常为 0,表示未使用)。
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 connect 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 服务绑定的地址
// 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
}

可以拿到对应的ip和端口

image-20241102140331580

relay阶段

connect 函数中,添加了以下功能来处理 SOCKS5 连接请求:

  1. 建立与目标主机的 TCP 连接
    • 使用 net.Dial 方法与客户端请求的目标地址(addr)和端口(port)建立 TCP 连接,并处理连接失败的情况。
  2. 发送成功响应
    • 构造并发送一个成功的 SOCKS5 响应给客户端,表示连接已经建立。这包括 SOCKS5 版本号、返回码(0x00 表示成功)、保留字段、地址类型(固定为 0x01),以及绑定地址和端口。
  3. 数据转发
    • 使用两个 goroutine 实现数据转发:
      • 第一个 goroutine 从客户端的连接(reader)读取数据,并将其写入到目标服务器(dest)。
      • 第二个 goroutine 从目标服务器读取数据,并将其写入到客户端的连接(conn)。
  4. 等待连接关闭
    • 使用 context 来管理 goroutine 的生命周期,并在其中一个方向的数据传输完成后关闭连接。
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)
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()

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

测试:

image-20241102141657400

image-20241102141708754

参考资料

Go 入门指南

Golang发行版