Go 语言基础 | 青训营笔记

155 阅读15分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

今天介绍了青训营go语言课程的内容纲要、go的基础语法和几个实战demo。

image.png

image.png

1、简介

1.1 什么是Go语言

Go语言特点

  1. 高性能、高并发
  2. 语法简单、学习曲线平缓
  3. 丰富的标准库
  4. 完善的工具链
  5. 静态链接
  6. 快速编译
  7. 跨平台
  8. 垃圾回收

1.2 哪些公司用Go语言

首先字节跳动已经全面拥抱了go语言,公司内部有上万个微服务使用golang来编写,不久前也开源了GO RPC框架KiteX。

根据拉勾的招聘数据,腾讯、百度、美团、滴滴、深信服、平安、OPPO、知乎、去哪儿、360、 金山、微博、哔哩哔哩、七牛、PingCAP 等公司也在大量使用Go语言。 国外Google Facebook等公司也在大量使用Go语言。

从业务维度看过语言已经在云计算、微服务、大数据、区块链、物联网等领域蓬勃发展。然后在云计算、微服务等领域已经有非常高的市场占有率 Docker、Kubernetes、 Istio、 etcd、 prometheus 几乎所有的云原生组件全是用Go实现的。 image.png

1.3字节跳动为什么全面拥抱GO语言

  1. 最初使用的Python, 由于性能问题换成了Go
  2. C++ 不太适合在线Web业务
  3. 早期团队非Java背景
  4. 性能比较好
  5. 部署简单、学习成本低
  6. 内部RPC和HTTP框架的推广

2、入门

2.1 开发环境

配置go语言的开发环境分两步,

  • 第一步是安装go语言;
  • 第二步的话是配置go的集成开发环境

集成开发环境又分为本地IDE和在线编程环境。

2.2 基础语法

2.2.1 Hello World

每种编程语言的入门教程中Hello World是不可或缺的。go中的Hello World写法:

package main // package main代表这个文件属于main包的一部分,main 包也就是程序的入口包。

import (
	"fmt" // 标准库里面的FMT包。这个包主要是用来往屏幕输入输出字符串、格式化字符串。
)

func main() {
	fmt.Println("hello world") // main 函数的话里面调用了fmt.Println 输出helloword
}

2.2.2 变量

Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。

go语言是一门强类型语言,每一个变量都有它自己的变量类型。

常见的变量类型包括字符串、整数、浮点型、布尔型等。

go语言的字符串是内置类型,可以直接通过加号拼接,也能够直接用等于号去比较两个字符串。

在go语言里面,大部分运算符的使用和优先级都和C或者C++类似。

在go语言里面变量的声明有两种方式,

  • 一种是通过 var name string = ""这种方式来声明变量,声明变量的时候,- 般会自动去推导变量的类型。 如果有需要,你也可以显示写出变量类型。
  • 另一种声明变量的方式是:使用变量冒号:=等于值。

常量的话就是把var改成const,值在一提的是go语言里面的常量,它没有确定的类型,会根据使用的上下文来自动确定类型。

2.2.3 if-else

go语言里面的if else写法和C或者C++类似。

不同点1是if后面没有括号。如果你写括号的话,那么在保存的时候你的编辑器会自动把你去掉。

第二个不同点是Golang里面的if,它必须后面接大括号,就是你不能像C或者C++ 一样,直接把if里面的语句同一行。

func main() {
	if true {
		fmt.Println("哈哈哈")
	}

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

2.2.4 循环

go语言里面的循环,在go里面没有while循环、do while循环,只有唯一的一种for循环。最简单的for循环就是在for后面什么都不写,代表一个死循环。循环途中你可以用break跳出。在循环里面,你可以用break或者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
	}
}

2.2.5 switch

switch 语句用于基于不同条件执行不同动作。

go语言里面的switch分支结构。看起来也C或者C++比较类似。同样的在switch后面的那个变量名,并不是要括号。

这里有个很大的一点不同的是,在c++里面,switch case如果不显示加break的话会然后会继续往下跑完所有的case,在go语言里面的话是不需要加break的。

相比C或者C++,go语言里面的switch功能更强大。可以使用任意的变量类型,甚至可以用来取代任意的if else语句。你可以在switch后面不加任何的变量,然后在case里面写条件分支。这样代码相比你用多个if else代码逻辑会更为清晰。

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

2.2.6 数组

数组就是一个具有编号且长度固定的元素序列。

比如这里的话是一个可以存放5个int元素 对于一个数组,可以很方便地取特定索引的值或者往特定索引取存储值,然后也能够直接去打印一个数组。

不过,在真实业务代码里面,我们很少直接使用数组,因为它长度是固定的,我们用的更多的是切片。

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

2.2.7 切片

我们可以用make来创建一个切片, 可以像数组一样去取值,使用append来追加元素。

注意append的用法的话,你必须把append的结果赋值为原数组。 因为slice的原理实际上是它有一个它存储了一个长度和一个容量,加一个指向一个数组的指针,在你执行append操作的时候,如果容量不够的话,会扩容并且返回新的slice。

slice 初始化的时候也可以指定长度。slice 拥有像python 一样的切片操作,比如这个代表取出第二个到第五个位置的元素, 不包括第五个元素。不过不同于python,这里不支持负数索引。

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

2.2.8基础语法-map

Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。

map,在其他编程语言里面, 它可能可以叫做哈希或者字典。map 是实际使用过程中最频繁用到的数据结构。我们可以用make来创建一个空 map,这里会需要两个类型,第一个是那个key的类型,这里是string另一个是value 的类型,这里是int。可以从里面去存储或者取出键值对。可以用delete从里面删除键值对。

golang的map是完全无序的,遍历的时候不会按照字母顺序,也不会按照插入顺序输出,而是随机顺序。

2.2.9 range

Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。

用range来快速遍历,这样代码能够更加简洁。 range遍历的时候,对于数组会返回两个值,第一个是索引,第二个是对应位置的值。如果我们不需要索引的话,我们可以用下划线来忽略。

2.2.10 函数

Go 语言最少有个 main() 函数(java叫做方法)。你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务。函数声明告诉了编译器函数的名称,返回类型,和参数。

Go 语言标准库提供了多种可动用的内置的函数。例如,len() 函数可以接受不同类型参数并返回该类型的长度。如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。

2.2.11 指针

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。

go里面也支持指针。指针的一个主要用途就是对于传入参数进行修改。  

我们来看这个函数。这个函数试图把一个变量+2。 但是单纯像上面这种写法其实是无效的。因为传入函数的参数实际上是一个拷贝, 那也说这个+2,是对那个拷贝进行了+2,并不起作用。 如果我们需要起作用的话,那么我们需要把那个类型写成指针类型,那么为了类型匹配,调用的时候会加一个&符号。

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
}

3、项目实战

3.1 猜谜游戏

3.1.1 题目

用Golang来构建一个猜数字游戏。

在这个游戏里面,程序首先会生成一个介于 1到100之间的随机整数,然后提示玩家进行猜测。玩家每次输入一个数字,程序会告诉玩家这个猜测的值是高于还是低于那个秘密的值。如果猜对了,就告诉玩家胜利并且退出程序。

3.1.2 解题步骤

  1. 当程序运行的时候会生成一个0到100之间的随机数字。 我们先来生成这个随机数。为了生成随机数,我们需要用到math/rand包。我们的第一个版本的代码是这样子的,我们先导入fmt包和math/rand包, 定义一个变量,maxNum是100。下面用rand.Intn来生成一个随机数, 再打印出这个随机数。
  2. 我们发现每次都会打印相同的数字到屏幕上。这个不是我们想要的,我们用time.Now().UnixNano()来初始化随机种子。
  3. 实现用户输入输出,并成数字。 我们可以用它的ReadString 方法来读取一行。如果失败了的话,我们会打印错误并能退出。ReadString 返回的结果包含结尾的换行符,我们把它去掉,再转换成数字。如果转换失败,我们同样打印错误,退出。
  4. 现在我们有了一个秘密的值,然后也从用户的输入里面读到了一个值, 我们来比较这两个值的大小。如果是用户输入的值比那个秘密的值要大的话,就告诉用户你猜的值太大了,请再试一次。如果是小了也同理,如果是相等的话,那么我们就告诉用户你赢了。
  5. 此时我们的程序大致可以正常工作了,但是玩家只能输入一次猜测,无论猜测是否正确,程序都会突退出。为了改变这种行为,让游戏可以正常玩下去,我们需要加一个循环。我们把刚刚的代码挪到一个for循环里面,再把return改成continue以便于在出错的时候能够继续循环。在用户输入正确的时候break,这样才能够在用户胜利的时候退出游戏。

3.1.3 代码

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

3.2 在线词典

3.2.1 题目

用户可以在命令行里面查询一个单词。我们能通过调用第三方的API查询到单词的翻译并打印出来。 这个例子里面,我们会学习如何用go语言来来发送HTTP请求、解析json 过来,还会学习如何使用代码生成来提高开发效率。

3.2.2 代码

package main

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

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

type DictResponse struct {
	Rc   int `json:"rc"`
	Wiki struct {
		KnownInLaguages int `json:"known_in_laguages"`
		Description     struct {
			Source string      `json:"source"`
			Target interface{} `json:"target"`
		} `json:"description"`
		ID   string `json:"id"`
		Item struct {
			Source string `json:"source"`
			Target string `json:"target"`
		} `json:"item"`
		ImageURL  string `json:"image_url"`
		IsSubject string `json:"is_subject"`
		Sitelink  string `json:"sitelink"`
	} `json:"wiki"`
	Dictionary struct {
		Prons struct {
			EnUs string `json:"en-us"`
			En   string `json:"en"`
		} `json:"prons"`
		Explanations []string      `json:"explanations"`
		Synonym      []string      `json:"synonym"`
		Antonym      []string      `json:"antonym"`
		WqxExample   [][]string    `json:"wqx_example"`
		Entry        string        `json:"entry"`
		Type         string        `json:"type"`
		Related      []interface{} `json:"related"`
		Source       string        `json:"source"`
	} `json:"dictionary"`
}

func query(word string) {
	client := &http.Client{}
	request := DictRequest{TransType: "en2zh", Source: word}
	buf, err := json.Marshal(request)
	if err != nil {
		log.Fatal(err)
	}
	var data = bytes.NewReader(buf)
	// https://api.interpreter.caiyunai.com/v1/dict
	req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Connection", "keep-alive")
	req.Header.Set("DNT", "1")
	req.Header.Set("os-version", "")
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
	req.Header.Set("app-name", "xy")
	req.Header.Set("Content-Type", "application/json;charset=UTF-8")
	req.Header.Set("Accept", "application/json, text/plain, */*")
	req.Header.Set("device-id", "")
	req.Header.Set("os-type", "web")
	req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
	req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
	req.Header.Set("Sec-Fetch-Site", "cross-site")
	req.Header.Set("Sec-Fetch-Mode", "cors")
	req.Header.Set("Sec-Fetch-Dest", "empty")
	req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
	req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
	req.Header.Set("Cookie", "_ym_uid=16456948721020430059; _ym_d=1645694872")
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	if resp.StatusCode != 200 {
		log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
	}
	var dictResponse DictResponse
	err = json.Unmarshal(bodyText, &dictResponse)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
	for _, item := range dictResponse.Dictionary.Explanations {
		fmt.Println(item)
	}
}

func main() {
	if len(os.Args) != 2 {
		fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
		`)
		os.Exit(1)
	}
	word := os.Args[1]
	query(word)
}

image.png

3.3 SOCKS5代理

3.3.1 题目

写一个socks5代理服务器,对于大家来说,一提到代理服务器, 第一想到的是翻墙。 不过很遗憾的是,socks5 协议它虽然是代理协议,但它并不能用来翻墙,它的协议都是明文传输。

这个协议历史比较久远,诞生于互联网早期。它的用途是,比如某些企业的内网为了确保安全性,有很严格的防火墙策略,但是带来的副作用就是访问某些资源会很麻烦。

socks5相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源。实际上很多翻墙软件,最终暴露的也是一个 socks5协议的端口。如果有同学开发过爬虫的话,就知道,在爬取过程中很容易会遇到IP访问频率超过限制。这个时候很多人就会去网上找一些代理IP池,这些代理IP池里面的很多代理的协议就是socks5。

3.3.2 socks5协议的工作原理

接下来我们来了解一下 socks5协议的工作原理。正常浏览器访问一个网站,如果不经过代理服务器的话,就是先和对方的网站建立TCP连接,然后三次握手,握手完之后发起HTTP请求,然后服务返回HTTP响应。如果设置代理服务器之后,流程会变得复杂一些

  1. 首先是浏览器和socks5代理建立TCP连接,代理再和真正的服务器建立TCP连接。这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、relay 阶段。
  2. 第一个握手阶段,浏览器会向socks5代理发送请求,包的内容包括一个协议的版本号 ,还有支持的认证的种类,socks5 服务器会选中一个认证方式,返回给浏览器。如果返回的是00的话就代表不需要认证,返回其他类型的话会开始认证流程,这里我们就不对认证流程进行概述了。
  3. 第三个阶段是请求阶段,认证通过之后浏览器会socks5服务器发起请求。主要信息包括版本号,请求的类型,一般主要是connection请求,就代表代理服务器要和某个域名或者某个IP地址某个端口建立TCP连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应
  4. 第四个阶段是relay阶段。此时浏览器会发送正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边。然后实际上代理服务器并不关心流量的细节,可以是HTTP流量,也可以是其它 TCP流星。这个就是 socks5协议的工作原理,接下来我们就会试图去简单地实现它。

3.3.3 代码

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
}

4、总结

今天是青训营正式学习的第一天,学了许多关于go的知识,由于有其他语言的基础,所以学起来不是很吃力。

这三个实战项目后边两个还挺有实用价值的,建议认真看一看。