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