这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天
这一章总结了Go语言基础语法,内容包括变量定义与使用,数据类型,循环与判断,异常处理,几种常见的包函数。
最后列举了三个实际编程的例子。
基础语法
- 项目结构:文件所在包。导入库。main函数。
package main
import (
"fmt"
)
func main() {
fmt.Println("hello world")
}
- 变量的定义
一共有四种方式
var a = "initial"
var b, c int = 1, 2
var d = true
var e float64
f := float32(e)
前加const表示静态变量,定义的变量必须被使用
- nil解析
nil 本质上是一个 Type 类型的变量,Type 类型是基于 int 定义出来的一个新类型。
nil 适用于 指针,函数,interface,map,slice,channel 这 6 种类型。
Go 分配内存是置 0 分配的,确保分配出来的内存块里面是全 0 数据。
nil 这个概念是更高一层的概念,是由编译器带来的,只有这 6 种类型的变量才能和 nil 值比较。
同样的,编译器不允许赋值一个 nil 变量给这6中类型以外的任一类型。
- for循环的使用方法
for一共有三种使用方法
for {
}
for j := 7; j < 9; j++ {
}
for i <= 3 {
}
- if的使用方法
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 a:=2; 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")
}
也可以不传参,直接进入比较
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
- 数组的定义与使用
var a [5]int
a[4] = 100
b := [5]int{1, 2, 3, 4, 5}
var c = [5]int{1,2,3,4,5}
不管是一维数组还是二维数组都可以直接打印
fmt.Println(b)
fmt.Println(b[0:5])
fmt.Println("len:", len(b))
var twoD [2][3]int
fmt.Println("2d: ", twoD)
- 切片的使用
切片可以理解为长度可变的数组,在数组创建时不写长度参数即可
切片用关键字make来定义
s := make([]string, 3)
切片的使用和数组类似
s[0] = "a"
len(s)
s = append(s, "d")
使用copy可以复制切片
c := make([]string, len(s))
copy(c, s)
打印c得到[a b c d e f]
d := make([]string, len(s)-1)
copy(d, s)
此时切片d长度小于要复制的切片c,打印d得到[a b c d e]
如果长度过长,多余部分置""
- 哈希表map的创建与使用
m := make(map[string]int)
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
向表中添加或更改元素
m["one"] = 1
m["two"] = 2
获取元素
value, isExist := map["key"]
r, ok := m["unknow"] // 0 false
删除元素
delete(m, "one")
打印map
fmt.Println(m) // map[one:1 two:2]
fmt.Println(len(m)) // 2
fmt.Println(m["one"]) // 1
fmt.Println(m["unknow"]) // 0
- range的使用 对于数组和map,返回一个可以遍历的元素
nums := []int{2, 3, 4}
for idx, num := range nums {
}
m := map[string]string{"a": "A", "b": "B"}
for key, value := range m {
}
for key, _ := range m {
}
- 函数function的创建
使用关键字func创建函数
func exists(m map[string]string, k string) (v string, ok bool) {
v, ok = m[k]
return v, ok
}
- 指针的使用
获得地址&n,获得指向的值*(&n)==n
n := 5
func add2ptr(n *int) {
*n += 2
}
add2ptr(&n)
- 结构体struct的创建与使用
使用关键字type和struct定义结构体
在创建结构体实例时最好注明name: value1, password: value2
结构体可以当做指针传入函数时,不需要取值操作*struct,但在传参时,仍需&struct
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) // {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
}
结构体的方法
func main() {
a := user{name: "wang", password: "1024"}
a.resetPassword("2048")
}
func (u *user) resetPassword(password string) {
u.password = password
}
- 异常处理error
需要抛出异常时,只需要errors.New("错误信息")
这里返回一个指针类型变量,是因为nil适用于指针,但不适用于结构体。
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")
}
- defer、panic、recover的使用
Go语言不支持传统的 try…catch…finally 。
Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常。
func main(){
fmt.Println("a")
defer func(){ // 必须要先声明defer,否则不能捕获到panic异常
fmt.Println("d")
if err:=recover();err!=nil{
fmt.Println(err) // 这里的err其实就是panic传入的内容,55
}
fmt.Println("e")
}()
f() //开始调用f
fmt.Println("f") //这里开始下面代码不会再执行
}
func f(){
fmt.Println("b")
panic("异常信息")
fmt.Println("c") //这里开始下面代码不会再执行
}
输出:a、b、d、异常信息、e。
- 字符串的操作
需要导入包“strings”。
HasPrefix判断字符串是否以既定字符开头,HasSuffix判断字符串是否以既定字符结尾。
strings.Join([]string{"a", "b"}, "-")合并字符串,等效于"a" + "-" + "b"
strings.Replace(a, "e", "E", n)将a中前n个e换成E,如果n<0,就是全换
Split分割字符串,返回一个切片类型
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
}
- fmt的使用
对于结构体的输出
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}
%v适用于字符串、整形和结构体的输出
fmt.Printf("%.2f\n", 3.141592653) // 3.14
fmt.Println(a, b, c, d, e, f)
- json的使用
Marshal: 用于编码JSON
MarshalIndent: 编码JSON后,带格式化的输出
Unmarshal: 用于解码JSON
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"}}
}
分别输出
[123 34 78 97 109 101 34 58 34 119 97 110 103 34 44 34 97 103 101 34 58 49 56 44 34 72 111 98 98 121 34 58 91 34 71 111 108 97 110 103 34 44 34 84 121 112 101 83 99 114 105 112 116 34 93 125]
{"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
{
"Name": "wang",
"age": 18,
"Hobby": [
"Golang",
"TypeScript"
]
}
main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
还有一些不常用的方法:
Compact: 用于JSON字符串的拼接, 拼接时会校验后面的字符串是否是合法json, 如果不是会报错, 但对字符串中的特殊字符(html不安全字符,比如上面提到的”<” “>”等)不进行转义
HTMLEscape: 和Compact相对, 拼接JSON字符串时会进行特殊字符转义, 转义成web安全的字符
Valid: 校验数据是否是合法的JSON编码数据, 往往用于数据格式校验
Indent: 用于JSON的格式化输出, 最常见的用法是定义JSON的缩进,比如2个空格的缩进
- time的使用
t.Format("2006-01-02 15:04:05")是把t格式化成特定的格式
t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")作用同Format
t.Unix()返回一个从1970年一月一日开始到现在的秒数
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
}
- strconv的使用
strconv.ParseFloat("1.234", 64)将字符串转化成浮点数;32指float32,64指float64。
strconv.ParseInt("111", 10, 64)将字符串转化成整数;第二位表示字符串的进制,分别有2、8、10、16,如果传0,就按照字符串里0b、0o、0x来处理,默认是十进制;第三位0, 8, 16, 32, 64分别是int, int8, int16, int32, int64。
strconv.Atoi("123")相当于strconv.ParseInt("123", 10, 0)
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("111", 2, 64)
fmt.Println(n) // 7
n, _ = strconv.ParseInt("111", 0, 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) // 0
fmt.Println(err) // strconv.Atoi: parsing "AAA": invalid syntax
}
- os/exec的使用
os.Args返回程序运行时所在的地址(大概率在C盘的AppData)
os.Getenv("PATH")返回环境变量中PATH的值
func main() {
fmt.Println("---------------")
// 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("---------------")
fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
fmt.Println("---------------")
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("---------------")
fmt.Println(string(buf)) // 127.0.0.1 localhost
}
编程示例
猜数游戏
设计一个猜数小游戏,返回输入的数字和答案的大小关系。
rand.Seed(time.Now().UnixNano())设置随机数种子,加上这行代码,可以保证每次随机都是随机的。
rand.Intn(maxNum)产生一个随机数。
bufio.NewReader(os.Stdin)创建读入流。
reader.ReadString('\n')从命令行读取一个数。
strings.Trim(input, "\r\n")去掉字符串首尾匹配到的字符。
strconv.Atoi(input)将字符串转化成整数
代码如下:
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
}
}
}
在线字典
做一个翻译字典,从命令行输入英文,返回中文翻译。
- 第一步我们对翻译服务器发送请求。
首先获取API,打开翻译网页,右键检查,选中dict,右键copy,copy as cURL (bash),得到如下内容。
curl 'https://api.interpreter.caiyunai.com/v1/dict' \
-H 'authority: api.interpreter.caiyunai.com' \
-H 'accept: application/json, text/plain, */*' \
-H 'accept-language: zh-CN,zh;q=0.9' \
-H 'app-name: xy' \
-H 'content-type: application/json;charset=UTF-8' \
-H 'device-id;' \
-H 'origin: https://fanyi.caiyunapp.com' \
-H 'os-type: web' \
-H 'os-version;' \
-H 'referer: https://fanyi.caiyunapp.com/' \
-H 'sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "Windows"' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: cross-site' \
-H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36' \
-H 'x-authorization: token:qgemv4jr1y38jyq6vhvi' \
--data-raw '{"trans_type":"en2zh","source":"hello"}' \
--compressed
其次,打开网页请求代码生成网站,粘贴以上内容,得到如下代码。
该代码先是创建请求http.NewRequest,然后设置请求头req.Header.Set,有的请求头内容可能会转义错误,即是删除也可以正常运行,接下来发起请求client.Do(req),读取响应ioutil.ReadAll(resp.Body),得到json。读取响应之前会创建defer resp.Body.Close(),以便于释放资源。
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
func main() {
client := &http.Client{}
var data = strings.NewReader(`{"trans_type":"en2zh","source":"hello"}`)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
if err != nil {
log.Fatal(err)
}
req.Header.Set("authority", "api.interpreter.caiyunai.com")
req.Header.Set("accept", "application/json, text/plain, */*")
req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
req.Header.Set("app-name", "xy")
req.Header.Set("content-type", "application/json;charset=UTF-8")
req.Header.Set("device-id", "")
req.Header.Set("origin", "https://fanyi.caiyunapp.com")
req.Header.Set("os-type", "web")
req.Header.Set("os-version", "")
req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
req.Header.Set("sec-ch-ua", `"Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"`)
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", "cross-site")
req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.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 := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", bodyText)
}
打印得到的json,如下所示。
{"rc":0,"wiki":{"known_in_laguages":19,"description":{"source":"salutation or greeting","target":null},"id":"Q12068060","item":{"source":"hello","target":"Hello"},"image_url":"http:\/\/www.caiyunapp.com\/imgs\/link_default_img.png","is_subject":"true","sitelink":"https:\/\/www.caiyunapp.com\/read_mode\/?id=5c1654ca4faac90001a6f17a"},"dictionary":{"prons":{"en-us":"[h\u0259\u02c8lo]","en":"[\u02c8he\u02c8l\u0259u]"},"explanations":["int.\u5582;\u54c8\u7f57","n.\u5f15\u4eba\u6ce8\u610f\u7684\u547c\u58f0","v.\u5411\u4eba\u547c(\u5582)"],"synonym":["greetings","salutations"],"antonym":[],"wqx_example":[["say hello to","\u5411\u67d0\u4eba\u95ee\u5019,\u548c\u67d0\u4eba\u6253\u62db\u547c"],["Say hello to him for me . ","\u4ee3\u6211\u95ee\u5019\u4ed6\u3002"]],"entry":"hello","type":"word","related":[],"source":"wenquxing"}}
上示代码中data携带了我们要翻译的内容,为了扩展应用,data应该由一个struct得出,而不是一个写死的字符串。
type DictRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
UserID string `json:"user_id"`
}
{
request := DictRequest{TransType: "en2zh", Source: "good"}
buf, err := json.Marshal(request)
if err != nil {
log.Fatal(err)
}
var data = bytes.NewReader(buf)
}
- 第二步我们返回响应。
打开翻译网页,右键检查,复制dict的preview内容,如下所示。
{
"rc": 0,
"wiki": {
"known_in_laguages": 19,
"description": {
"source": "salutation or greeting",
"target": null
},
"id": "Q12068060",
"item": {
"source": "hello",
"target": "Hello"
},
"image_url": "http://www.caiyunapp.com/imgs/link_default_img.png",
"is_subject": "true",
"sitelink": "https://www.caiyunapp.com/read_mode/?id=5c1654ca4faac90001a6f17a"
},
"dictionary": {
"prons": {
"en-us": "[həˈlo]",
"en": "[ˈheˈləu]"
},
"explanations": [
"int.喂;哈罗",
"n.引人注意的呼声",
"v.向人呼(喂)"
],
"synonym": [
"greetings",
"salutations"
],
"antonym": [],
"wqx_example": [
[
"say hello to",
"向某人问候,和某人打招呼"
],
[
"Say hello to him for me . ",
"代我问候他。"
]
],
"entry": "hello",
"type": "word",
"related": [],
"source": "wenquxing"
}
}
打开结构体生成网站,粘贴上述内容,点击转换嵌套得到Go的struct代码,重命名为DictResponse,如下所示。
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 []interface{} `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"`
}
用上述结构体对服务器返回的json内容进行解码。
首先判断服务器的状态,如果不是200,说明请求异常,直接返回错误信息。
然后用结构体对json进行解码json.Unmarshal(bodyText, &dictResponse)。
最后打印dictResponse.Dictionary.Prons中的En和EnUs信息,以及dictResponse.Dictionary中的Explanations信息
{
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)
}
}
对翻译服务器请求和返回响应的代码封装成一个query函数,传入一个string类型的参数,重写main函数,判断是否有输入,并传入参数。
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)
}
运行代码,在命令行输入go run main.go hello,可以得到。
PS D:\desktop\Go\GoLearning\go-by-example-master> go run testDict\exe1\main.go hello
hello UK: [ˈheˈləu] US: [həˈlo]
int.喂;哈罗
n.引人注意的呼声
v.向人呼(喂)
代理服务器
创建一个代理服务器,浏览器发送的请求经过代理服务器到达服务器,再将返回内容传回浏览器。
浏览器,代理服务器和服务器的连接流程如下所示:
client向socks5 server进行协商,socks5 server通过协商。
client向socks5 server发送请求,socks5 server向host建立TCP连接,host返回响应,socks5 server返回状态。
client向socks5 server发送数据,socks5 server向hostrelay数据,host返回结果,socks5 server返回结果。
- 第一步创建一个server,原封不动返回输入的值,以检测服务器的正确性。
net.Listen("tcp", "127.0.0.1:1080")监听端口。
这里写一个死循环,表示一直监听端口。
server.Accept()获得TCP连接。
go process(client)创建一个子线程。
process中获得一个输入流bufio.NewReader(conn),在一个死循环中不断读入一个字节reader.ReadByte(),输出一个字节conn.Write([]byte{b})。
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
}
}
}
运行代码,然后新打开一个命令行,发送指令nc 127.0.0.1 1080,表示连接该IP的端口,输入任意内容,如果原封不动地返回输入的内容就代表服务器运行成功。
如果不能识别nc指令,就需要安装netcat,下载安装netcat 1.12,在系统环境PATH变量下新增即可。
- 第二步实现认证阶段。
首先将process中的死循环改为运行auth函数。
在auth函数中实现读取reader的报文,并返回conn的响应。
client向socks5 server发送一个报文,该报文包含1字段的版本协议,1字段的支持认证的方法数量,n字段的方法,n=支持认证的方法数量。
// +----+----------+----------+
// |VER | NMETHODS | METHODS |
// +----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +----+----------+----------+
通过reader.ReadByte()读取版本协议号ver和支持认证的方法数量methodSize。
新建methodSize个方法make([]byte, methodSize)并进行读取io.ReadFull(reader, method)。
新建并返回TCP连接的报文conn.Write([]byte{socks5Ver, 0x00})。
{
err := auth(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
retur
}
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
}
运行程序,命令行输入
C:\Users\user>curl --socks5 127.0.0.1:1080 -v http://www.qq.com
* Trying 127.0.0.1:1080...
* SOCKS5 connect to IPv6 2402:4e00:1900:1400:0:9227:71ef:f0b1:80 (locally resolved)
* Failed to receive SOCKS5 connect request ack.
* Closing connection 0
curl: (7) Failed to receive SOCKS5 connect request ack.
编译器打印出报文信息。
2023/01/17 12:19:26 ver 5 method [0 1]
2023/01/17 12:19:26 auth success
输出0 1就说明服务器工作正常
- 第三步读取浏览器发送的报文
在上一步auth函数结束后运行connect函数。
报文包括1字段的版本号,1字段的CMD(只支持connect请求),保留字段RSV,1字段的目标地址类型ATYP,可变长度DST.ADDR,2字段的目标端口号DST.PORT。
// +----+-----+-------+------+----------+----------+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
和auth函数类似,创建一个长度为4的byte切片,依次读入io.ReadFull(reader, buf),ver, cmd, atyp := buf[0], buf[1], buf[3]。
根据ATYP的不同进行不同操作,如果atyp是0x01,即IPV4,就再读四个字段io.ReadFull(reader, buf),打印成IP地址的形式fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3]);如果atyp是host,就先读取host的长度reader.ReadByte(),创建hostSize长的切片,然后读取数据io.ReadFull(reader, host);如果atyp是IPV6,暂时不做这一部分,直接报错就可以了。
然后读取端口号io.ReadFull(reader, buf[:2]),并进行格式处理binary.BigEndian.Uint16(buf[:2])。
最后我们要返回一个报文,这个报文内容比较多,但是connection需要用到的部分比较少,我们直接返回conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})即可
{
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", 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])
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
}
运行程序,命令行输入
C:\Users\user>curl --socks5 127.0.0.1:1080 -v http://www.qq.com
* Trying 127.0.0.1:1080...
* SOCKS5 connect to IPv6 2402:4e00:1900:1400:0:9227:71e8:2ccc:80 (locally resolved)
* connection to proxy closed
* Closing connection 0
curl: (7) connection to proxy closed
打印出IP端口号就证明正常运行。
- 第四步,获得端口号之后建立TCP连接。
{
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()
}
在返回报文之后,从用户浏览器reader向底层数据dest传送数据,再从底层数据dest向用户浏览器conn传送数据。
由于Go创建子线程的时间较短,需要等数据传送结束后再终止线程,所以创建context.WithCancel(context.Background()),在传送数据完成后调用cancel(),最后线程结束调用<-ctx.Done()。
{
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
}
运行程序,命令行输入
C:\Users\user>curl --socks5 127.0.0.1:1080 -v https://juejin.cn/
* Trying 127.0.0.1:1080...
* SOCKS5 connect to IPv4 183.204.200.228:443 (locally resolved)
* SOCKS5 request granted.
* Connected to 127.0.0.1 (127.0.0.1) port 1080 (#0)
* schannel: ALPN, offering http/1.1
* schannel: ALPN, server accepted to use http/1.1
> GET / HTTP/1.1
> Host: juejin.cn
> User-Agent: curl/7.71.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: Tengine
< Content-Type: text/html; charset=utf-8
< Content-Length: 60506
< Connection: keep-alive
< Date: Tue, 17 Jan 2023 08:12:20 GMT
< X-Powered-By: Express
< x-tt-logid: 20230117161220B00838AE0C1A251AB459
< ETag: "ec5a-qVwV1rCXipxGCydCWv5klLTnpd4"
< Accept-Ranges: none
< Server-Timing: inner; dur=95, pp;dur=10, total;dur=93;desc="Nuxt Server Time"
< Vary: Accept-Encoding, Accept-Encoding
< x-tt-trace-host: 01f9280880aeadc7dfd365ace415a46c7b595f2aa2f9d4cf47846dd43c1a59fbefa374c89e36a34515b1a0908c48a7fad459a45cc79cd089dc64f1fabe934d7fd72e5aba37ec629709352466d55b430e1f
< x-tt-trace-tag: id=3;cdn-cache=miss
< X-TT-TIMESTAMP: 1673943140.406
< Via: cache13.cn4110[110,0]
< Timing-Allow-Origin: *
< EagleId: b7ccc82116739431403042883e
<
<!doctype html>//剩下是网页信息,就是掘金主页
同时编译器输出时间和端口号2023/01/17 16:12:21 dial 183.204.200.228 443
除了命令行,还可以用代理服务器,打开SwitchyOmega插件,新建情景模式,代理协议选择SOCKS5,代理服务器选择127.0.0.1,代理端口选择1080,点击应用选项,最后点击插件图标,选择对应的情景模式。 此时运行代码,浏览器中打开的网页都会输出在编译器中。
课后作业
1、修改第一个例子猜谜游戏里面的最终代码,使用 fmt.Scanf 来简化代码实现
2、修改第二个例子命令行词典里面的最终代码,增加另一种翻译引擎的支持
3、在上一步骤的基础上,修改代码实现并行请求两个翻译引擎来提高响应速度