1.1什么是Go语言

  • 使用标准库即可开发高性能 、高并发应用程序(标准库功能十分强大,稳定)

  • 基于C语言,且比C语言容易

  • 循环只有for,遍历循环较为容易

  • 适配多系统,树莓派等

  • 垃圾回收,无须考虑内存释放

实例:

简单的静态页面的代码

2.1开发环境

直接在golang官网安装,或者下载vsc编辑器后下载GO插件

在https://goproxy.cn网址按照教程配置第三方包,可以提高依赖的下载速度,该网站还有自托管 Go 模块代理的配置教程

golang学生可以免费申请使用,具体操作和idea免费申请类似

2.2.1基础语法--hello world!

packagemain

import (

"fmt"

)

funcmain() {

fmt.Println("hello world")

}

package main表示该文件属于main包的一部分,mian包即程序的入口包,该文件即程序的入口文件

导入标准库的fmt包:主要往屏幕输入输出字符串,格式化字符串(该操作类似C的导入头文件)

func main即主函数,调用fmt的println输出

运行该程序使用go run命令

生成二进制文件使用go build命令

鼠标放在Println上可以链接跳转包的官方文档,查看其他方法的使用方法,类似idea

2.2.2基础语法--变量

字符串是内置类型,类似string,可以直接用 + 拼接,也可以用 = 去比较两个字符串

变量的声明:

var 【name】 = 【value】

比如: var a = "initial"

会根据value自动匹配变量的类型,如果想直接确定类型,可以在【name】后直接将类型名标注

比如: var b, c int = 1, 2

或者

【变量名】 := 【value】

声明常量时,将var改为const

可以声明多个类型不同的变量(类型由初始化表达式推导):

vari, j, kint // int, int, int

varb, f, s=true, 2.3, "four"// bool, float64, string

packagemain

import (

"fmt"

"math"

)

funcmain() {

vara="initial"

varb, cint=1, 2

vard=true

varefloat64

f :=float32(e)

g :=a+"foo"

fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0

fmt.Println(g) // initialapple

constsstring="constant"

consth=500000000

consti=3e20/h

fmt.Println(s, h, i, math.Sin(h), math.Sin(i))

}

2.2.3 基础语法 -- if else

和C基本类似

不同:

if和else if后没有条件判断的括号(小括号)且必须有花括号括住

packagemain

import"fmt"

funcmain() {

if7%2==0 {

fmt.Println("7 is even")

} else {

fmt.Println("7 is odd")

}

if8%4==0 {

fmt.Println("8 is divisible by 4")

}

ifnum :=9; num<0 {

fmt.Println(num, "is negative")

} elseifnum<10 {

fmt.Println(num, "has 1 digit")

} else {

fmt.Println(num, "has multiple digits")

}

}

2.2.4 基础语法 -- 循环

go 语言只有for循环一种

for后面什么都不写即代表死循环

可以使用经典的C语言的for循环,格式相同,同样也有continue和break的跳出循环方式

以下是集中for循环的使用方法

packagemain

import"fmt"

funcmain() {

i :=1

for {

fmt.Println("loop")

break

}

forj :=7; j<9; j++ {

fmt.Println(j)

}

forn :=0; n<5; n++ {

ifn%2==0 {

continue

}

fmt.Println(n)

}

fori<=3 {

fmt.Println(i)

i=i+1

}

}

2.2.5 基础语法 -- switch

go语言中Switch后面的变量名和if一样不需要括号

和C语言不同的是,C语言case语句后没有break默认走完所有case,

而go语言默认是不走的

在go中Switch还有更加高级的用法,可以在Switch后不加任何的变量名,而 在case后添加条件判断,可以代替if语句使条件的选择更加清晰,代码更加美观

packagemain

import (

"fmt"

"time"

)

funcmain() {

a :=2

switcha {

case1:

fmt.Println("one")

case2:

fmt.Println("two")

case3:

fmt.Println("three")

case4, 5:

fmt.Println("four or five")

default:

fmt.Println("other")

}

t :=time.Now()

switch {

caset.Hour() <12:

fmt.Println("It's before noon")

default:

fmt.Println("It's after noon")

}

}

2.2.6 基础语法 -- 数组,切片

1.普通数组

和C语法和使用方法基本相同

vara [5]int

a[4] =100

b := [5]int{1, 2, 3, 4, 5}

vartwoD [2][3]int

fori :=0; i<2; i++ {

forj :=0; j<3; j++ {

twoD[i][j] =i+j

}

}

2.切片

切片是一个可变长度的数组,类似java的list集合,可以随时追加元素

切片使用make创建,用append进行追加,需要注意追加后要返回切片(会自动进行扩容并返回新的长度)

使用append函数追加元素可以一次性追加多个元素,甚至可以直接追加一个切片

同样可以用copy函数拷贝切片

go也有类似Python的切片操作

fmt.Println(s[2:5]) // [c d e],打印2~5(不包括5)

packagemain

import"fmt"

funcmain() {

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.7 基础语法 -- map

使用过程中用到的最多的数据结构

  • 同样,使用make创建

m :=make(map[string]int)

m["one"] =1

m["two"] =2

其中string是key的类型,int是value的类型

  • 初始化一些map中的值

ages :=map[string]int{

"alice": 31,

"charlie": 34,

}

  • 使用delete去删除

delete(m, "one")

  • 在使用时可以在变量后面加一个ok来获取对应key索引的value是否存在如果存在变量为value,ok为true,否则,变量为对应类型的零值,ok为false

r, ok :=m["unknow"]

fmt.Println(r, ok) // 0 false

  • map是无序的,多次迭代会得到不同的结果

2.2.8 基础语法 -- range

  • 一个迭代工具,可以快速遍历数组,切片,map等,类似java的foreach快速遍历

  • 在迭代时可以同时输出对应的key和value

  • 如果不需要索引可以用下划线free

packagemain

import"fmt"

funcmain() {

nums := []int{2, 3, 4}

sum :=0

fori, num :=rangenums {

sum+=num

ifnum==2 {

fmt.Println("index:", i, "num:", num) // index: 0 num: 2

}

}

fmt.Println(sum) // 9

m :=map[string]string{"a": "A", "b": "B"}

fork, v :=rangem {

fmt.Println(k, v) // b 8; a A

}

fork :=rangem {

fmt.Println("key", k) // key a; key b

}

}

2.2.9 基础语法 -- 函数

  • go语言的函数形参列表和参数类型与C语言位置相反,类型写在形参的后面

  • go语言函数的返回值可以是多个,并且在开发时通常返回一个本该返回的值而另一个为错误信息(类似于状态码,message)

packagemain

import"fmt"

funcadd(aint, bint) int {

returna+b

}

funcadd2(a, bint) int {

returna+b

}

funcexists(mmap[string]string, kstring) (vstring, okbool) {

v, ok=m[k]

returnv, ok

}

funcmain() {

res :=add(1, 2)

fmt.Println(res) // 3

v, ok :=exists(map[string]string{"a": "A"}, "a")

fmt.Println(v, ok) // A True

}

2.2.10 基础语法 -- 指针

  • go语言也有指针,其使用形式大致和Cpp相同,但功能相对简单,一般用于函数间传参时改变变量的值,传参时用&,解引用时用*

  • 但要注意的是,go语言是不支持指针运算的,如果想进行指针运算,需要引入特殊包,直接对内存进行操作

funcadd2ptr(n*int) {

*n+=2

}

funcmain() {

n :=5

add2(n)

fmt.Println(n) // 5

add2ptr(&n)

fmt.Println(n) // 7

2.2.11 基础语法 -- 结构体

  • 结构体的使用和C语言类似,创建结构体:

typeuserstruct {

name string

passwordstring

}

  • 结构体变量的赋值可以用key:value的方式,有时key也可以省略不写,

也可以只对结构体中的部分变量进行赋值

a :=user{name: "wang", password: "1024"}

b :=user{"wang", "1024"}

c :=user{name: "wang"}

c.password="1024"

  • 结构体也可以作为形参类型,但需要注意只有指针结构体才能改变结构体变量的值

  • 结构体还有结构体方法,类似于内成员变量,使用方法为在func后加 结构体变量名 结构体名

  • 同样在此处如果要改变变量的值,使用的函数的参数必须为指针变量

具体实例代码如下:

func (uuser) checkPassword(passwordstring) bool {

returnu.password==password

}

func (u*user) resetPassword(passwordstring) {

u.password=password

}

funcmain() {

a :=user{name: "wang", password: "1024"}

a.resetPassword("2048")

fmt.Println(a.checkPassword("2048")) // true

}

2.2.12 基础语法 -- 错误处理

错误,一种类似java中的异常却不完全相同的类型

  • go语言通常将错误作为返回值

  • 不同于java的异常,go的错误会清楚的显示错误发生的行与列,并能通过简单的if else语句初期错误

funcfindUser(users []user, namestring) (v*user, errerror) {

for_, u :=rangeusers {

ifu.name==name {

return&u, nil

}

}

returnnil, errors.New("not found")

}

例如,在参数列表中加入err类型,如果返回正常,error即返回nil,否则,new一个错误并返回

  • 在主函数中同样也要有变量来接收err的值,并主函数中对错误进行相应的处理以保证程序的稳定性

funcmain() {

u, err :=findUser([]user{{"wang", "1024"}}, "wang")

iferr!=nil {

fmt.Println(err)

return

}

fmt.Println(u.name) // wang

ifu, err :=findUser([]user{{"wang", "1024"}}, "li"); err!=nil {

fmt.Println(err) // not found

return

} else {

fmt.Println(u.name)

}

}

2.2.13 基础语法 -- 字符串操作

在go语言的strings包里包含了许多有关于字符串的操作,需要使用时导包即可

  • Contains:查找是否包含某一字符串,返回布尔值

  • Count:统计某个字符串在原字符串中出现的次数

  • Index:定位

  • Join:将一个字符串拼接到另一个字符串后面

  • Repeat:重复多次字符串

  • len:内置函数,统计字符串中的字符个数,主义中文可能一个字对应多个字符

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

2.2.14 基础语法 -- 字符串格式化

  • 在标准库fmt下就有字符串格式化函数

  • 常见的有Println,打印并换行

  • 还有熟悉的C语言中的Printf,其语法和C语言类似,比较方便的一点是,go语言只需要%v即可输出任意类型数据

  • 可以用%+v , %#v来得到更加详细的结构

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}

  • 当然,如果需要打印一定精度的浮点数,也可以用%.nf的方式控制

f :=3.141592653

fmt.Println(f) // 3.141592653

fmt.Printf("%.2f\n", f) // 3.14

2.2.15 基础语法 -- JSON处理

  • 定义结构体时,所有变量用大写

  • 给结构体赋值后,用Marshal进行序列化,或者用MarshalIndent函数产生整齐缩进的序列化

  • 输出时,用string做强制类型转换,否则输出为一串16进制编码

  • 也可以用Unmarshal函数进行反序列化,将json数据解码为字节切片

  • 如果在输出时想让输出与结构体成员名不同,需要在结构体声明时在对应结构体成员后加tag标签进行标注

funcmain() {

a :=userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}

buf, err :=json.Marshal(a)

iferr!=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")

iferr!=nil {

panic(err)

}

fmt.Println(string(buf))

varbuserInfo

err=json.Unmarshal(buf, &b)

iferr!=nil {

panic(err)

}

fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}

2.2.16 基础语法 -- 时间处理

  • 时间的处理要引入time包

  • 通过time.Now()获取当前时间

  • 通过time.Date去构造一个带时区的时间

  • 构造完成后可以用【变量名】.Year , 【变量名】.Month 等等去获取时间中的年月日时分秒

  • Sub函数将两个时间相减获得到时间差 用.Minutes(),.Seconds()可以将时间差转换为分,秒

  • 时间格式化,必须将"2006-01-02 15:04:05"放入,否则不成功

这里有两种格式化方法:

t.Format("2006-01-02 15:04:05")

t3, err :=time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")

  • 在系统交互时,可以用.Unix()去获取一个时间戳

funcmain() {

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

iferr!=nil {

panic(err)

}

fmt.Println(t3==t) // true

fmt.Println(now.Unix()) // 1648738080

}

2.2.17 基础语法 -- 数字解析

  • 对于字符串和数字的转化解析,需要引入strconv包

  • 使用ParseFloat将字符串转化为浮点数,64代表精度

f, _ :=strconv.ParseFloat("1.234", 64)

fmt.Println(f) // 1.234

  • 使用ParseInt将字符串转化为整数,第二个参数代表进制,如果为0代表自动推测,64代表精度

n, _ :=strconv.ParseInt("111", 10, 64)

fmt.Println(n) // 111

n, _=strconv.ParseInt("0x1000", 0, 64)

fmt.Println(n) // 4096

  • 使用Atoi进行快速转化,也可以Itoa转化

n2, _ :=strconv.Atoi("123")

fmt.Println(n2) // 123

  • 如果失败会返回错误

n2, err :=strconv.Atoi("AAA")

fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax

2.2.18 基础语法 -- 进程信息

要导入对应的os,os/exec包

os.Args:获取当前进程的命令行参数,命令行参数包括了程序路径本身,以及通常意义上的参数。程序中os.Args的类型是 []string

os.Getenv:检索由键命名的环境变量的值。它返回值,如果变量不存在,该值将为空。

os.Setenv: 函数可以设置名为 key 的环境变量,如果出错会返回该错误。

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

iferr!=nil {

panic(err)

}

fmt.Println(string(buf)) // 127.0.0.1 localhost

3.实战


3.1猜数字游戏

游戏介绍:每次游戏程序会自动生成一个0~100的随机数,玩家输入自己猜的数字,程序会告诉玩家你猜的数字大于或小于正确答案,直到用户输入正确的答案为止(具体流程如图)

开始实战:

1.首先去生成随机数

这里要导入math/rand包,用maxNum声明并变量赋值,并调用rand包下的Intn函数产生随机数(最大为maxNum)

funcmain() {

maxNum :=100

secretNumber :=rand.Intn(maxNum)

fmt.Println("The secret number is ", secretNumber)

}

但是运行后出现了问题,每次运行都会显示随机数为81

原因是没有设置随机数的种子,导致每次产生的随机数相同

解决方法:通常用时间戳来初始化随机数种子

修改代码如下:

funcmain() {

maxNum :=100

rand.Seed(time.Now().UnixNano())

secretNumber :=rand.Intn(maxNum)

fmt.Println("The secret number is ", secretNumber)

}

修改后每次运行都会产生不同的随机数结果

2.接收用户的输入并输出

go接收输入可以通过scanf这种比较简单的方式来实现,此处为后续项目的学习做准备,使用较麻烦的一种方式

首先new一个reader,通过reader的ReadString方法去读一个字符串

reader :=bufio.NewReader(os.Stdin)

input, err :=reader.ReadString('\n')

注意使用ReadString方法得到的字符串再结尾会有一个换行符,要通过strings包中的Trim方法消去换行

input, err :=reader.ReadString('\n')

得到一个数字字符串后,在利用前面的Atoi方法将字符串转换为数字

最后输出用户的输入进行检验

代码如下:

fmt.Println("Please input your guess")

reader :=bufio.NewReader(os.Stdin)

input, err :=reader.ReadString('\n')

iferr!=nil {

fmt.Println("An error occured while reading input. Please try again", err)

return

}

input=strings.Trim(input, "\r\n")

guess, err :=strconv.Atoi(input)

iferr!=nil {

fmt.Println("Invalid input. Please enter an integer value")

return

}

fmt.Println("You guess is", guess)

此处要注意包的引入,要包含我们用到的函数,并且每次对数据进行处理时都要进行 必要的错误校验,如果发生异常要进行相应处理。

3.逻辑的判断与循环

  • 逻辑 的判断使用if else语句比较即可

  • 需要注意的是,整个用户输入,提示语的输出应该是在大循环内的,以保证游戏是一直玩下去的

  • 当用户输入了正确答案的时候,要break退出循环

  • 用户在输入,程序处理代码时如果出现错误,不是卡死退出,而要continue跳出该回合,用户继续游戏

游戏完整代码如下:

packagemain

import (

"bufio"

"fmt"

"math/rand"

"os"

"strconv"

"strings"

"time"

)

funcmain() {

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

iferr!=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)

iferr!=nil {

fmt.Println("Invalid input. Please enter an integer value")

continue

}

fmt.Println("You guess is", guess)

ifguess>secretNumber {

fmt.Println("Your guess is bigger than the secret number. Please try again")

} elseifguess<secretNumber {

fmt.Println("Your guess is smaller than the secret number. Please try again")

} else {

fmt.Println("Correct, you Legend!")

break

}

}

}

3.2 在线命令行词典

在命令行输入一个单词会输出这个单词的音标,词性和释义,具体如下图

1.初步尝试

首先打开一个彩云小译的网页,输入要翻译的单词good,点击翻译后,打开网页开发者工具

选择network(网络),找到一个请求方法为POST的 dict,从负载和预览中可以看到一些请求的详细信息,我们在用golang开发时,也要使用对应的api

这种请求代码一般比较复杂,这里介绍一种可以代码生成的方法,首先右键dict选择复制----copy as cURL

将我们刚刚复制的一串代码输入到上方curl command(bash),会自动生成代码,我们将其复制到编辑器,代码解析参考注释

packagemain

import (

"fmt"

"io/ioutil"

"log"

"net/http"

"strings"

)

funcmain() {

client :=&http.Client{}//创建一个httpclient,此处可以指定最大请求时间

vardata=strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)

req, err :=http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)//创建http请求,参数:method,url,data(此处用了流,避免数据过大占用太多内存),因此上面用strings将字符串转换为流

iferr!=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,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")

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("sec-ch-ua", `"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";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 Edg/108.0.1462.76")

req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")

resp, err :=client.Do(req)//发起请求

iferr!=nil {

log.Fatal(err)

}

deferresp.Body.Close()//关闭返回的流

bodyText, err :=ioutil.ReadAll(resp.Body)//把流读到内存中变成数组

iferr!=nil {

log.Fatal(err)

}

fmt.Printf("%s\n", bodyText)//打印出最后的json字符串

}

client := &http.Client{}//创建一个httpclient,此处可以指定最大请求时间

var data = strings.NewReader({"trans_type":"en2zh","source":"good"})

req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)//创建http请求,参数:method,url,data(此处用了流,避免数据过大占用太多内存),因此上面用strings将字符串转换为流

resp, err := client.Do(req)//发起请求

defer resp.Body.Close()//关闭返回的流

bodyText, err := ioutil.ReadAll(resp.Body)//把流读到内存中变成byte数组

fmt.Printf("%s\n", bodyText)//打印出最后的json字符串

2.生成request body

完成上述代码可以得到一串json数据,但good是固定的,我们需要到用json序列化

首先根据上节基础语法的知识,先构建一个json结构体

typeDictRequeststruct {

TransTypestring`json:"trans_type"`

Source string`json:"source"`

UserID string`json:"user_id"`

}

然后new一个结构体变量并赋值,并用json序列化请求在将其转化为byte数组

request :=DictRequest{TransType: "en2zh", Source: "good"}

buf, err :=json.Marshal(request)

vardata=bytes.NewReader(buf)

接下来就和之前一样创建请求等等

代码如下:

typeDictRequeststruct {

TransTypestring`json:"trans_type"`

Source string`json:"source"`

UserID string`json:"user_id"`

}

funcmain() {

client :=&http.Client{}

request :=DictRequest{TransType: "en2zh", Source: "good"}

buf, err :=json.Marshal(request)

iferr!=nil {

log.Fatal(err)

}

vardata=bytes.NewReader(buf)

iferr!=nil {

log.Fatal(err)

}

resp, err :=client.Do(req)

iferr!=nil {

log.Fatal(err)

}

deferresp.Body.Close()

bodyText, err :=ioutil.ReadAll(resp.Body)

iferr!=nil {

log.Fatal(err)

}

fmt.Printf("%s\n", bodyText)

}

到此为止,修改后的代码运行结果与刚才应该是完全相同的,都生成了一串json的字符串

3.解析response body

首先打开(https://oktools.net/json2go),该网站可以实现json到go的结构体转化

将刚刚翻译出dict的响应处代码复制到网站的json框,可以得到一个超大的go语言结构体

(此处不展示)

然后去修改刚刚的代码,把原本最后的直接打印json串修改为反序列化到我们刚刚获得的结构体中,并用最详细的%#v去打印出来

vardictResponseDictResponse

err=json.Unmarshal(bodyText, &dictResponse)

iferr!=nil {

log.Fatal(err)

}

fmt.Printf("%#v\n", dictResponse)

4.打印结果

拿到结果后其实有很多返回内容是我们不需要的,只需要音标,词性和释义即可,可以从结构体中找到这几个东西把他们打印出来

fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)

for_, item :=rangedictResponse.Dictionary.Explanations {

fmt.Println(item)

}

5.代码完善

(1)对响应码进行判断,如果不为200,说明可能发生了错误,会导致后续反序列化为空

ifresp.StatusCode!=200 {

log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))

}

(2)主函数

刚刚仅仅是对一个单词的查询,可以将刚刚写好的函数作为功能函数query,再去编写主函数,将good变成一个变量传入,这样就可以任意查词了

funcmain() {

iflen(os.Args) !=2 {

fmt.Fprintf(os.Stderr, `usage: simpleDict WORD

example: simpleDict hello

`)

os.Exit(1)

}

word :=os.Args[1]

query(word)

}

3.3 Socks5 代理

什么是Socks5

socks5协议是一款广泛使用的代理协议,它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色,使得内部网中的前端机器变得能够访问Internet网中的服务器,或者使通讯更加安全。SOCKS5 服务器通过将前端发来的请求转发给真正的目标服务器, 模拟了一个前端的行为。在这里,前端和SOCKS5之间也是通过TCP/IP协议进行通讯,前端将原本要发送给真正服务器的请求发送给SOCKS5服务器,然后SOCKS5服务器将请求转发给真正的服务器。

socks5协议交互过程

第一步,客户端向代理服务器发送代理请求,其中包含了代理的版本和认证方

握手完成后,客户端要把需要执行的操作指令发给客户端,表明自己要执行代理的请求

客户端发完上面的请求连接后,服务端会发起连接到DST.ADDR:DST.PORT,然后返回响应到客户端

当连接建立后,客户端就可以和正常一样访问服务端通信了,此时通信的数据除了目的地址是发往代理程序以外,所有内容都是和普通连接一模一样。对代理程序而言,后面所有收到的来自客户端的数据都会原样转发到服务读端。

1.TCP echo server

首先写一个主函数去监听一个端口,再在一个死循环中去接受一个请求,如果成功会返回一个连接,使用go关键字去在process函数中处理这个连接,go可以类比为开启一个子线程

funcmain() {

server, err :=net.Listen("tcp", "127.0.0.1:1080")

iferr!=nil {

panic(err)

}

for {

client, err :=server.Accept()

iferr!=nil {

log.Printf("Accept failed %v", err)

continue

}

goprocess(client)

}

}

process函数的实现

首先创建一个流,再在死循环中去每次读一个字节,并写入slice,如果出错就关闭连接

funcprocess(connnet.Conn) {

deferconn.Close()

reader :=bufio.NewReader(conn)

for {

b, err :=reader.ReadByte()

iferr!=nil {

break

}

_, err=conn.Write([]byte{b})

iferr!=nil {

break

}

}

}

使用nc命令去测试

nc127.0.0.1:1080

输入什么,返回什么

2.auth

修改process死循环中的读写,改为调用auth函数去读报文获取认证方式,并返回方式

// +----+----------+----------+

// |VER | NMETHODS | METHODS |

// +----+----------+----------+

// | 1 | 1 | 1 to 255 |

// +----+----------+----------+

  • 0x00: 不需要认证

  • 0x01: GSSAPI认证

  • 0x02: 用户名和密码方式认证

  • 0x03: IANA认证

  • 0x80-0xfe: 保留的认证方式

  • 0xff: 不支持任何认证方式

funcauth(reader*bufio.Reader, connnet.Conn) (errerror) {

ver, err :=reader.ReadByte()

iferr!=nil {

returnfmt.Errorf("read ver failed:%w", err)

}

ifver!=socks5Ver {

returnfmt.Errorf("not supported ver:%v", ver)

}

methodSize, err :=reader.ReadByte()

iferr!=nil {

returnfmt.Errorf("read methodSize failed:%w", err)

}

method :=make([]byte, methodSize)

_, err=io.ReadFull(reader, method)

iferr!=nil {

returnfmt.Errorf("read method failed:%w", err)

}

log.Println("ver", ver, "method", method)

_, err=conn.Write([]byte{socks5Ver, 0x00})

iferr!=nil {

returnfmt.Errorf("write failed:%w", err)

}

returnnil

}

3.请求阶段

在proces函数中调用connection函数去读取代理请求信息

// +----+-----+-------+------+----------+----------+

// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |

// +----+-----+-------+------+----------+----------+

// | 1 | 1 | X'00' | 1 | Variable | 2 |

// +----+-----+-------+------+----------+----------+

创建缓冲区读取六个字段

  • VER: 代理版本信息

  • CMD

: 代理指令

  • 0x01: connect指令,tcp代理时使用。

  • 0x02: bind,很少使用,类似FTP协议中主动连接场景,服务端后服务端会主动连接到客户端。

  • 0x03: udp代理时使用。

  • RSV: 保留字段

  • ATYP

: 地址类型

  • 0x01: IPv4地址类型

  • 0x03: unix域socket类型代理

  • 0x04: IPv6地址类型

  • DST.ADDR: 需要连接的目的地址

  • DST.PORT: 需要连接的目的端口

获取到的六个字段,地址和端口号不用,全部填为0

具体代码如下

funcconnect(reader*bufio.Reader, connnet.Conn) (errerror) {

buf :=make([]byte, 4)

_, err=io.ReadFull(reader, buf)

iferr!=nil {

returnfmt.Errorf("read header failed:%w", err)

}

ver, cmd, atyp :=buf[0], buf[1], buf[3]

ifver!=socks5Ver {

returnfmt.Errorf("not supported ver:%v", ver)

}

ifcmd!=cmdBind {

returnfmt.Errorf("not supported cmd:%v", ver)

}

addr :=""

switchatyp {

caseatypIPV4:

_, err=io.ReadFull(reader, buf)

iferr!=nil {

returnfmt.Errorf("read atyp failed:%w", err)

}

addr=fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])

caseatypeHOST:

hostSize, err :=reader.ReadByte()

iferr!=nil {

returnfmt.Errorf("read hostSize failed:%w", err)

}

host :=make([]byte, hostSize)

_, err=io.ReadFull(reader, host)

iferr!=nil {

returnfmt.Errorf("read host failed:%w", err)

}

addr=string(host)

caseatypeIPV6:

returnerrors.New("IPv6: no supported yet")

default:

returnerrors.New("invalid atyp")

}

_, err=io.ReadFull(reader, buf[:2])

iferr!=nil {

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

iferr!=nil {

returnfmt.Errorf("write failed: %w", err)

}

returnnil

}

4.relay阶段建立TCP连接

使用net包下Dial函数去选择对应地址和端口建立TCP连接

建立连接后没有出错,关闭流

dest, err :=net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))

iferr!=nil {

returnfmt.Errorf("dial dst failed:%w", err)

}

deferdest.Close()

双向数据转化,从浏览器拷贝到底层服务器,在从底层服务器拷贝到浏览器,并且开启一个ctx,有一方关闭后返回

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

gofunc() {

_, _=io.Copy(dest, reader)

cancel()

}()

gofunc() {

_, _=io.Copy(conn, dest)

cancel()

}()

<-ctx.Done()

最终代码如下:

packagemain

import (

"bufio"

"context"

"encoding/binary"

"errors"

"fmt"

"io"

"log"

"net"

)

constsocks5Ver=0x05

constcmdBind=0x01

constatypIPV4=0x01

constatypeHOST=0x03

constatypeIPV6=0x04

funcmain() {

server, err :=net.Listen("tcp", "127.0.0.1:1080")

iferr!=nil {

panic(err)

}

for {

client, err :=server.Accept()

iferr!=nil {

log.Printf("Accept failed %v", err)

continue

}

goprocess(client)

}

}

funcprocess(connnet.Conn) {

deferconn.Close()

reader :=bufio.NewReader(conn)

err :=auth(reader, conn)

iferr!=nil {

log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)

return

}

err=connect(reader, conn)

iferr!=nil {

log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)

return

}

}

funcauth(reader*bufio.Reader, connnet.Conn) (errerror) {

ver, err :=reader.ReadByte()

iferr!=nil {

returnfmt.Errorf("read ver failed:%w", err)

}

ifver!=socks5Ver {

returnfmt.Errorf("not supported ver:%v", ver)

}

methodSize, err :=reader.ReadByte()

iferr!=nil {

returnfmt.Errorf("read methodSize failed:%w", err)

}

method :=make([]byte, methodSize)

_, err=io.ReadFull(reader, method)

iferr!=nil {

returnfmt.Errorf("read method failed:%w", err)

}

_, err=conn.Write([]byte{socks5Ver, 0x00})

iferr!=nil {

returnfmt.Errorf("write failed:%w", err)

}

returnnil

}

funcconnect(reader*bufio.Reader, connnet.Conn) (errerror) {

buf :=make([]byte, 4)

_, err=io.ReadFull(reader, buf)

iferr!=nil {

returnfmt.Errorf("read header failed:%w", err)

}

ver, cmd, atyp :=buf[0], buf[1], buf[3]

ifver!=socks5Ver {

returnfmt.Errorf("not supported ver:%v", ver)

}

ifcmd!=cmdBind {

returnfmt.Errorf("not supported cmd:%v", ver)

}

addr :=""

switchatyp {

caseatypIPV4:

_, err=io.ReadFull(reader, buf)

iferr!=nil {

returnfmt.Errorf("read atyp failed:%w", err)

}

addr=fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])

caseatypeHOST:

hostSize, err :=reader.ReadByte()

iferr!=nil {

returnfmt.Errorf("read hostSize failed:%w", err)

}

host :=make([]byte, hostSize)

_, err=io.ReadFull(reader, host)

iferr!=nil {

returnfmt.Errorf("read host failed:%w", err)

}

addr=string(host)

caseatypeIPV6:

returnerrors.New("IPv6: no supported yet")

default:

returnerrors.New("invalid atyp")

}

_, err=io.ReadFull(reader, buf[:2])

iferr!=nil {

returnfmt.Errorf("read port failed:%w", err)

}

port :=binary.BigEndian.Uint16(buf[:2])

dest, err :=net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))

iferr!=nil {

returnfmt.Errorf("dial dst failed:%w", err)

}

deferdest.Close()

log.Println("dial", addr, port)

_, err=conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})

iferr!=nil {

returnfmt.Errorf("write failed: %w", err)

}

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

defercancel()

gofunc() {

_, _=io.Copy(dest, reader)

cancel()

}()

gofunc() {

_, _=io.Copy(conn, dest)

cancel()

}()

<-ctx.Done()

returnnil

}

完成后调命令测试,会出现详细的代理信息

curl--socks5127.0.0.1:1080-vhttp: //www.qq.com