#1 Go语言基础和实战 | 青训营笔记

121 阅读8分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天
本次编程学习的基本环境配置如下
OS: macOS 13.1
IDE: Goland 2022.3
Go Version: 1.18

作为Go语言小白, 出现错误实在难免。如果您发现文章有错误, 或者对部分语句有疑问, 欢迎在评论区指出。

重点内容

  1. 配置Go语言的开发环境
  2. Go语言的变量和作用域规则
  3. Go语言的流程控制, 包括条件选择、循环语法以及Switch语法
  4. Go语言静态数组、切片、map
  5. Go语言函数、结构体、结构体方法
  6. Go语言错误处理机制
  7. Go语言字符串操作和与其他数据类型的转换(strconv包、json包等)
  8. Go语言接受Shell输入

Go语言的其他内容

  1. Go语言文件I/O
  2. Go语言的defer语法
  3. Go语言interface机制
  4. Go语言模块管理
  5. channel和goroutine
  6. Go语言单元测试
  7. Go对C的接口

详细介绍

Go语言的特点

  1. 高性能, 高并发
  2. 语法简单, 学习曲线平缓
  3. 丰富的标准库
  4. 完善的工具链
  5. 静态链接
  6. 快速编译 go build/go run, 但会付出性能代价
  7. 跨平台, 在各种设备上都可以运行, 还可以很方便的交叉编译
  8. 垃圾回收

配置开发环境

  1. 下载Golang
  2. 下载喜欢的IDE
  3. 在终端输入go version, 输出为go version go1.18.4 darwin/arm64

Hello World

// helloworld.go
// 指定文件所属的包, main是程序入口包
package main

// 导入标准库的fmt包
import (
   "fmt"
)

// main函数
func main(){
   fmt.Println("Hello World")
}

运行

go run helloworld.go
# Hello World

编译

除了直接运行, 还可以用go build helloworld.go编译, 编译会生成一个名为helloworld的可执行文件, 直接执行也会输出Hello World

Go中的数据类型

Go语言是一种强类型语言, 可以用关键字var声明或用海象表达式快速定义, 支持类型推断。

package main

import "fmt"

func main() {
   // 1. 变量声明后需要使用
   // 2. 类型推断系统, 如果不指定类型, go会根据所给的初始值推断类型
   // 3. 强类型, 静态类型
   // a declared but not used
   var a, c int
   a = 1
   b := 3
   fmt.Println(a, c)
   fmt.Println(b)
   var d, e = true, "this is function "
   g := e + "Main"
   fmt.Println(d, e, g)
   const (
      q = 1
      w = 2
   )
   fmt.Println(q, w)
}

作用域

  1. 变量不能在同一作用域下重复定义
  2. 子作用域允许定义与父作用域相同的变量, 且变量值的修改仅在子作用域有效
  3. 子作用域可以捕获父作用域的变量, 子作用域可以读取和修改该变量

// scope.go
package main

import "fmt"

func main() {
   // 4. 变量不能在同一作用域下重复定义
   // 5. 子作用域允许定义与父作用域相同的变量, 且变量值的修改仅在子作用域有效 [暂定]
   // 6. 子作用域可以捕获父作用域的变量, 子作用域可以读取和修改该变量
   a := 2
   b := 3
   // 捕获a
   if a > 3 {
      fmt.Println("a > 3")
   } else {
      fmt.Println("a <= 3")
   }
   // 在子作用域中重新定义a
   if a := 3; a > 3 {
      fmt.Println("a > 3")
      a = 2
   } else {
      // 在if作用中定义的a仅在if作用域中有效, 因此, 该行输出3
      fmt.Println("a <= 3", a)
      b = 1
   }
   // 输出 2(3失效) 1 
   fmt.Println(a, b)
   
}

image.png

简单的条件选择和循环

  1. if关键字可用于进行条件判定,判定条件必须是布尔表达式, 不支持隐式类型转换
  2. if内可以增加一条赋值语句, 被定义的变量仅在if-else if-else语句块内作用
  3. Go语言的for关键字十分强大, 可以用来构造各种类型的循环.
  4. 随手一记: Go语言有goto关键字, 应该也可以用来做循环吧
// process_control.go
package main

import "fmt"

func main() {
   // 一个简单的控制台输入
   var (
      input   int
      inFloat float64
   )
   fmt.Println("请输入一个整数:")
   _, err := fmt.Scanf("%d\n", &input)
   if err != nil {
      fmt.Println("Input Error: ", err)
   } else {
      fmt.Println("输入的整数是: ", input)
   }
   fmt.Println("请输入一个浮点数:")
   // err 不会在作用域的其他位置使用, 最好定义在if语句的作用域之内
   if _, errNew := fmt.Scanf("%f\n", &inFloat); errNew != nil {
      fmt.Println("Input Error: ", errNew)
   } else {
      fmt.Println("输入的浮点数是: ", inFloat)
   }
   // Go的for循环是类C的, 完整形式:(不能写++i)
   for i := 1; i < 10; i++ {
      fmt.Print(i, " ")
   }
   fmt.Println()
   // 可以只有 循环判断条件
   i := 1
   for i < 10 {
      fmt.Print(i, " ")
      i++
   }
   fmt.Println()
   // 支持 while-true-break
   // 支持 continue, 本例不演示了
   for {
      fmt.Print(i, " ")
      i++
      if i > 20 {
         break
      }
   }
   fmt.Println()
}

image.png

Switch分支结构

  1. 可以如C语言那样, 用switch关键字后跟一个变量, 然后在block中用case做相等关系的条件判定
  2. switch关键字后也可以不跟变量, 直接在block内做任何关系的条件判定. 程序选择匹配到的最先的一个分支执行, 等价语法是 if-else if-if else ...-else, 而不是if-if-if...if
  3. 和C不同的是, switch的block中, 当遇到下一个case的时候自动结束switch而不是继续执行.
  4. 由于规则3的存在, 在多数时候goswitch要更好用, 它更符合直觉, 减少了未定义行为出现的风险, 但很多trick就没法用了
package main

import "fmt"

func main() {
   var (
      input int
   )
   fmt.Println("请输入一个整数")
   if _, err := fmt.Scanf("%d", &input); err != nil {
      fmt.Println("Input Error: ", err)
   } else {
      fmt.Println(input)
   }
   // c-style 但是不用写break, 默认带break
   switch input {
   case 1:
      fmt.Println("输入的是 1")
   case 2:
      fmt.Println("输入的是 2")
   default:
      fmt.Println("输入的是 ", 3)
   }
   fmt.Println("------------------")
   // python3.10 match-case be-like
   switch {
   case input > 2:
      fmt.Println("输入的值大于2")
   // 输入是4, 这条分支会不会触发? ==> 不会, 等价写法是 if-else if-else if-...-else
   case input > 3:
      fmt.Println("输入的值大于3")
   // 注意Golang的强类型特性, input 和 bool没有隐式类型转换
   //case input:
   default:
      fmt.Println("输入的值为", input)
   }
}

image.png

数组

  1. 本节介绍的是静态数组, 支持长度推断
  2. 静态数组不支持append函数
  3. 静态数组支持for-range语法快速访问(例子中没有)
  4. 静态数组支持切片索引语法(例子中也没有), 这样操作会得到一个Slice对象, 见下节
package main

import (
   "fmt"
)

func main() {
   var (
      // 数组的字面量初始化和长度推断
      arrayA = []int{1, 2, 3, 4, 5}
      // 支持直接定义数组,
      arrayB [10]int
      // 使用make创建切片, 切片有类似数组行为, 比如支持索引访问
      // 下一个例子有更多关于切片的用法
      arrayC = make([]int, 10)
      // 这样写会创建长度为0的数组
      arrayD []int
      // 二维数组, 自动的长度推断
      array2D = [][]int{{1, 2, 3}, {4, 5, 6}}
      // 二维数组, 直接定义
      array2Dc [5][2]int
   )
   fmt.Println(arrayA)
   for i := 0; i < len(arrayB); i++ {
      arrayB[i] = i + 1
   }
   fmt.Println(arrayB)
   // 访问数组arrayC
   arrayC[9] = 1
   fmt.Println(len(arrayC), cap(arrayC))
   fmt.Println(arrayC[9])
   // ... 输出 0, 0
   fmt.Println(len(arrayD), cap(arrayD))
   // 访问数组D, 会出现一个panic
   // arrayD[0] = 1
   fmt.Println(array2D)
   fmt.Println(len(array2D), cap(array2D))
   for i := 0; i < 5; i++ {
      for j := 0; j < 2; j++ {
         array2Dc[i][j] = i + j
      }
   }
   fmt.Println(array2Dc)
}

image.png

数组切片

  1. 数组切片可以通过静态数组的切片操作得到, 更常见的做法是通过make内置函数得到(三个参数: 类型、初始长度,容量; 两个参数: 类型, 初始长度). 合理设置容量可以减少切片的扩容操作, 加快append的速度, 而且还节省内存.
  2. 切片支持append函数, 可以将一个元素push到切片尾部, 由于push操作可能引起切片底层的数组扩容, 因此需要将append函数的返回值接收, 通常赋值给原切片, 这一操作可被称为appendAssign(Goland是这么做的). 切片每次扩容会将容量扩大为原来的两倍.
  3. 切片的删除操作比较复杂, 是通过appendAssign操作达成的, 这其实是数组的特性, push快而pop很慢, 涉及到大量数据的移动
fmt.Println("arrayB:", arrayB)
// [1 2 3 4 5 6 7 8 9 10]
arrayE := arrayB[1:2]
// arrayE [2]
arrayE = append(arrayE, 3, 100, 200, 400, 500)
// arrayE [2 3 100 200 400 500]
fmt.Println("arrayE:", arrayE)
// delete arrayE[2]
arrayF := append(arrayE[:2], arrayE[3:]...)
fmt.Println("arrayF:", arrayF)
// arrayF [2 3 200 400 500]

image.png

package main

import (
   "fmt"
   "strconv"
)

func main() {
   var (
      sliceA = make([]string, 3)
      // len=0, cap=10
      sliceB = make([]string, 0, 10)
   )
   // append的结果需要重新赋值回去
   sliceA = append(sliceA, "hello")
   sliceA = append(sliceA, "world")

   fmt.Println(sliceA)
   // range语法
   for i, s := range sliceA {
      fmt.Println(i, s)
   }
   fmt.Println(len(sliceB), cap(sliceB))
   for i := 0; i < 10; i++ {
      sliceB = append(sliceB, strconv.Itoa(i))
   }
   // 切片区间访问
   fmt.Println(sliceB)
   fmt.Println(len(sliceB), cap(sliceB))
   fmt.Println(sliceB[:])
   fmt.Println(sliceB[3])
   fmt.Println(sliceB[5:10])
   // 切片的浅拷贝
   sliceC := sliceB[:]
   // proof of slice shallow copy
   fmt.Println(sliceC)
   sliceC[0] = "revised"
   fmt.Println("B:", sliceB)
   fmt.Println("--------------------")
   // 切片的深拷贝 copy
   var sliceD = make([]string, len(sliceB))
   copy(sliceD, sliceB)
   fmt.Println("D:", sliceD)
   sliceD[0] = "0"
   fmt.Println("B:", sliceB)
   fmt.Println("D:", sliceD)
}

image.png

map

  1. 用关键字var声明的map, 在赋值后才可以操作
  2. 可以通过make操作, 或通过:=得到一个可操作的map
  3. map支持[]增加键值对和range语法遍历键值对, 使用delete函数删除键值对
  4. map支持[]访问键值对, 但返回的是两个值resexist, 通过后者判定值是否存在, 存在则通过前者获取
package main

import "fmt"

func main() {
   // 无法进行以下操作, mapA事实上没有创建
   //var mapA map[string]int
   //mapA["string"] = 2
   //for s, i := range mapA {
   // fmt.Println(s, i)
   //}
   // 以下操作可以进行
   mapB := map[string]int{"string": 2}
   mapB["string"] = 3
   mapB["string2"] = 3
   for s, i := range mapB {
      fmt.Println(s, i)
   }
   // 使用make创建
   fmt.Println("-----------------")
   mapC := make(map[string]int, 10)
   mapC["string"] = 4
   mapC["string2"] = 5
   for s, i := range mapC {
      fmt.Println(s, i)
   }
   fmt.Println("-----------------")
   // 删除项
   delete(mapC, "string2")
   for s, i := range mapC {
      fmt.Println(s, i)
   }
   // 注意 mapC["string"]的返回值是两项

   if r, exist := mapC["string"]; exist {
      fmt.Println(r)
   } else {
      fmt.Println("没有该项")
   }

}

image.png

函数

  1. func关键字声明函数, Go语言的函数是比较符合我的习惯的,比如任意数量参数的传递, 后置的返回值声明, 返回值声明中可以指定变量名, 相同类型的变量的类型可以只写一个等等
  2. 要是再来一个最后一行表达式的值为返回值, 就更棒啦
  3. 函数reviseAnInteger使用了指针, 指针意味着变量所有权的隐式转移, 在实际编程中要注意.
package main

import "fmt"

func countAndAdd(a, b int, c ...int) (ans, count int) {
   ans = a + b
   count = 2
   for _, num := range c {
      ans += num
      count++
   }
   return ans, count
}

func reviseAnInteger(a, b *int) {
   *a += 1
   *b += 1
}

func main() {
   ans, count := countAndAdd(1, 2, 3, 4, 5, 6)
   fmt.Printf("ans=%d, count=%d\n", ans, count)
   var (
      a, b = 1, 1
   )
   reviseAnInteger(&a, &b)
   fmt.Println(a, b)
}

image.png

结构体和结构体方法

  1. struct关键字可以用来配合type关键字建立结构体类型
  2. 结构体类型block中有字段、类型声明以及tag三部分组成
  3. 结构体可以嵌套
  4. 结构体方法是一种特别的函数, 可以理解为将函数变量中的结构体形参提到函数名之前. 结构体方法带来了很多好处, 如:检查接口类型的实现等
  5. fmt.Printf()函数支持使用"%+v"%#v的格式化模式, 能输出结构体的更详细的信息
package main

import "fmt"

// user 用户结构体
// 类型后可用反引号加上tag
type user struct {
   id   int64  `json:"id"`
   name string `json:"name"`
}

type vipUser struct {
   u   user
   vip bool
}

// getId 结构体方法, 如果有一个方法传指针, 最好都用指针
func (u *user) getId() int64 {
   return u.id
}

func (u *user) setName(newName string) {
   u.name = newName
}

func main() {
   u := user{
      id:   0,
      name: "",
   }
   u1 := user{1, "2"}
   u2 := user{id: 2}
   fmt.Println(u.getId(), u1.getId(), u2.getId())
   u2.setName("hello")
   // 结构体有默认的控制台输出
   fmt.Println(u2)
   fmt.Println("--------------------")
   // 结构体的嵌套
   vip := vipUser{
      u:   u,
      vip: true,
   }
   // 嵌套结构体的默认输出
   fmt.Println(vip)
   fmt.Printf("%+v\n", vip)
   fmt.Printf("%#v\n", vip)
}

image.png

错误传递机制

  1. Go语言的错误传递机制让其饱受批评, 但笔者认为, 虽然if err != nil看着让人心烦, 但在结合IDE的错误处理检查功能后, 确实能起到减少程序崩溃的风险。
  2. 在Go语言中,错误会一层一层向上传递, 直到无法处理, 使用panic触发异常,程序崩溃。
package main

import (
   "errors"
   "fmt"
)

type User struct {
   name     string
   password string
}

// findUser 在`users`找名字为`name`的用户
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)
   }
}

image.png

字符串操作和解析

  1. 本节主要介绍了一些针对字符串处理的函数
  2. 在实际开发中,字符串处理占很大比重。使用这些函数可以让开发工作事半功倍
  3. 还有字符串与其他类型的转换, 本节主要是字符串和数字的相互转换, JSON一节主要讲自定义类型的序列和反序列化, 可以视作字符串和其他类型的相互转换
// strings.go

package main

import (
   "fmt"
   "strconv"
   "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("h", 2))                   // hh
   fmt.Println(strings.Replace("eWo", "e", "E", -1))     // EWo
   fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]
   fmt.Println(strings.ToLower("HELLO"))                 // hello
   fmt.Println(strings.ToUpper(a))                       // HELLO
   fmt.Println(len(a))                                   // 5
   b := "你好"
   fmt.Println(len(b)) // 6
   // 多行文本
   c := "第一行\n第二行\n第三行"
   fmt.Println(c)
   // 解析数字
   if f, err := strconv.ParseFloat("1.234", 64); err != nil {
      fmt.Println(err)
   } else {
      fmt.Println(f)
   }

   if d, err := strconv.ParseInt("111", 10, 64); err != nil {
      fmt.Println(err)
   } else {
      fmt.Println(d)
   }
   // 整数转字符串
   q := strconv.Itoa(1)
   fmt.Println(q)
   // 浮点数转字符串
   fmt.Println(strconv.FormatFloat(1.234, 'e', 10, 64))
   fmt.Println(fmt.Sprintf("%.2f", 1.234))

}

image.png

结构体与Json

  1. 使用JSON包中的MarshalUnmarshal函数实现结构体类型的序列和反序列化
  2. 使用MarshalIndent函数在序列化时加上缩进
package main

import (
   "encoding/json"
   "fmt"
)

// userInfo 使用JSON库, 请注意要保证字段是公开的, 可以用结构体Tag对字段重命名
type userInfo struct {
   Name  string
   Age   int `json:"age"`
   Hobby []string
}

func main() {
   // 将a做JSON序列化, 得到buf
   a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
   // ? 为啥叫Marshal这个名字呢?
   // Marshal 返回 ([]byte, err)
   // 可将byte转换成字符串
   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"]}

   // MarshalIndent 使用indent的序列化, 方便打印
   buf, err = json.MarshalIndent(a, "", "\t")
   if err != nil {
      panic(err)
   }
   fmt.Println(string(buf))

   var b userInfo
   // Unmarshal 将字符串反序列化到结构体b
   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"}}
}

image.png

时间处理

  1. 时间字符串的解析和处理
  2. 获取当前时间戳的方法:time.Now().Unix()
package main

import (
   "fmt"
   "time"
)

func main() {
   // 构造时间, 返回Time结构体
   t := time.Date(2023, 1, 15, 18, 32, 10, 0, time.UTC)
   // 可以分项打印时间
   fmt.Println(t.Year(), t.Month())
   // 还必须用这个时间, 算是彩蛋吗
   fmt.Println(t.Format("2006-01-02 15:04:05"))
   // 获取当前时间, Time结构体
   now := time.Now() 
   // 类似: 2023-01-15 18:39:56.184533 +0800 CST m=+0.000338709
   fmt.Println(now)
   // Sub 返回一个 Duration 结构体 : 7h52m13.815467s
   fmt.Println(t.Sub(now))
   // 解析时间串
   t2, err := time.Parse("2006-01-02 15:04:05", "2023-01-15 18:32:10")
   if err != nil {
      panic(t2)
   }
   // 是否一致: 返回true
   fmt.Println(t2 == t)
   // 返回时间戳
   fmt.Println(now.Unix())

}

image.png

进程信息和命令行

  1. 获取命令行参数和环境变量
  2. 执行命令并获取输出
package main

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

func main() {
   // go run example/20-env/main.go a b c d
   // 打印所有输入的命令行参数
   fmt.Println(os.Args)
   // 打印 环境变量 PATH中的第一项
   fmt.Println(strings.Split(os.Getenv("PATH"), ":")[0])
   // 设置环境变量
   if err := os.Setenv("AA", "BB"); err != nil {
      panic(err)
   } else {
      // 设置成功, 打印
      fmt.Println(os.Getenv("AA"))
   }
   fmt.Println("文件 exec_info.go 的最后10行是: ")
   // 执行指令 grep 127.0.0.1 /etc/hosts
   buf, err := exec.Command("tail", "-n", "10", "exec_info.go").CombinedOutput()
   if err != nil {
      panic(err)
   }
   // 打印当前文件内容
   fmt.Println(string(buf))
}

image.png

例子

HTTP服务器

package main

import (
   "fmt"
   "net/http"
   "os"
)

func main() {
   // 为模式 "/" 指定Handler
   http.Handle("/", http.FileServer(http.Dir(".")))
   // 启动HTTP服务器, 监听本机所有IP的8080端口
   if err := http.ListenAndServe(":8080", nil); err != nil {
      fmt.Println("Internal Error")
      os.Exit(-1)
   }
}

image.png

猜数游戏

经典编程实例, 代码可从文末的代码仓库获取, 后文实例同理.

  1. 随机数生成: rand.Seed()设置随机数种子, rand.Intn(n int)获取一个随机整数(最大为n)
  2. bufio.NewReader(os.Stdin)获取标准输入文件句柄, 通过ReadString(\n)方法从标准输入中读取一行, 通过strings.Trim方法去除多余的space字符, 在通过strconv.Atoi(input)将输入字符串转换成整数, 与生成的随机数比较, 并输出提示信息
  3. 循环2, 直到猜对退出.

image.png

字典服务

本实例展示了Go语言的网络协议能力, 该实例可以理解为是一个爬虫程序
主函数main从命令行中获取到要查的词, 然后交给query函数, 重点在于query函数的处理,以下为该函数流程

  1. 初始化HTTP客户端client, 用于以POST方式向URL: "https://api.interpreter.caiyunai.com/v1/dict"发送HTTP请求, 该API是彩云小译的字典服务, 可用来查询词汇.
  2. 源文件中定义了DictRequestDictResponse两个结构体类型, 用来快速序列化生成HTTP请求体和反序列化HTTP响应体.
  3. 建立DictRequest结构体对象, 序列化为JSON字节流(Marshal)并作为POST请求体, 然后设置Header, 应该是从现实请求中获取到的
  4. 通过client发送到服务器, 并等待响应, 收到响应后, 通过UnMarshal反序列化为DictResponse结构体对象, 取出词义信息, 打印到终端
  5. 注意defer resp.Body.Close()是Go语言中的defer(延迟调用)语法, 他会将之后的语句封装成可调用对象, 压入延迟调用栈(注意这个), 在当前函数退出(panic或者return)时执行.

image.png

代理服务

本程序会实现SOCKS5代理服务器, socks5是基于传输层的协议,客户端和服务器经过两次握手协商之后服务端为客户端建立一条到目标服务器的通道,在传输层转发TCP/UDP流量。具体可以查看rfc1928

  1. main函数通过net.Listen()启动服务器, 然后在一个死循环中Accept客户端请求. 当收到客户端请求后, 通过go关键字启动一个goroutine, 为该客户端提供服务, 即执行process
  2. process函数会先后执行auth()函数和connect函数, 分别完成认证协商和网络服务过程.
  3. auth()从连接中读取数据. 该函数希望从TCP的字节流中读到版本信息完成鉴权. 字节流的第一个字节应当是SOCKS5的版本信息, 默认为0x05, 然后是NMETHODMETHODS标识了一个字节数组, 表示支持认证的方法. 我们建立的代理服务器仅为自己服务, 一般无需鉴权。
  4. 服务器从支持认证的方法中选择一个, 返回到客户端, 这里选择的就是0, 即不需要鉴权
  5. 接下来服务器等待转入process函数, 阻塞在io.ReadFull方法(暂时认为是BIO)上, 等待客户端发送新的TCP数据.
  6. 服务器收到客户端发来的数据, 从中解析VER, CMD 和 ATYPE 并判定合法性, 从中解析出要连接的服务器IP地址或者域名保存到addr, 端口保存到port, 并代替客户端与目标服务器通信net.Dial()
  7. 服务器从目标服务器上接收数据, 并将数据发送到客户端, 完成本次服务, 连接关闭. 这一操作使用两个goroutine以及context.WithCancel完成的。
  8. 函数会阻塞在<-ctx.Done(), 等待cancel()函数被调用. 两个goroutine都执行数据流的拷贝, io.Copy()是一个死循环, 只有当拷贝出错的时候, 才会退出, 退出就会调用cancel(), 然后<-ctx.Done()不再阻塞, 函数退出, 执行延迟的cancel()方法, 向另外的goroutine发送取消信号, 另一goroutine也退出, 最后中断连接, 服务就完成啦
// 上下文机制在 goroutine 之间传递 deadline、取消信号(cancellation signals)或者其他请求相关的信息
// 其中context.WithCancel 函数能够从 context.Context 中衍生出一个新的子上下文并返回用于取消该上下文的函数。
// 一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号。
ctx, cancel := context.WithCancel(context.Background())
// 函数退出时, 父上下文关闭, 子上下文关闭, 向所有的goroutine发送取消信号
defer cancel()

go func() {
   // 将客户端的数据发到目标服务器, 这是一个死循环, 出错的时候, 才会退出...
   _, _ = io.Copy(dest, reader)
   // 执行cancel(), ctx.Done()将会接收到值, 然后本函数退出, 执行延迟的cancel()
   // 另一个goroutine也会收到信号而退出
   cancel()
}()
go func() {
   // 将目标服务器发来的数据发回客户端
   _, _ = io.Copy(conn, dest)
   cancel()
}()
// 调用cancel()方法可以从ctx.Done()中得到值, 从而函数退出
<-ctx.Done()

启动服务器

服务器运行在127.0.0.1:1088

image.png

在浏览器中配置代理

新建一个新的SOCKS5代理,将代理服务器地址设置为127.0.0.1, 代理端口设置为1088, 并设置为当前代理模式

image.png

开一个掘金试试吧

image.png

服务器这边的输出

image.png

总结

今天学习了很多Go语言的基本语法知识, 内容很多, 需要在实践中不断强化
SOCKS5代理服务器实例有点神奇, Go语言这个goroutine和channel真的太好用了!!

引用

  1. 课程:走进Go语言基础:juejin.cn/course/byte…
  2. 课程:Go语言实战实例:juejin.cn/course/byte…
  3. 代码仓库:github.com/wangkechun/…
  4. rfc1928: www.ietf.org/rfc/rfc1928…