这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天
Go Day01
1.1什么是Go语言
- 使用标准库即可开发高性能 、高并发应用程序(标准库功能十分强大,稳定)
- 基于C语言,且比C语言容易
- 循环只有for,遍历循环较为容易
- 适配多系统,树莓派等
- 垃圾回收,无须考虑内存释放
实例:
简单的静态页面的代码
2.1开发环境
直接在golang官网安装,或者下载vsc编辑器后下载GO插件
在goproxy.cn网址按照教程配置第三方包,可以提高依赖的下载速度,该网站还有自托管 Go 模块代理的配置教程
golang学生可以免费申请使用,具体操作和idea免费申请类似
2.2.1基础语法--hello world!
package main
import (
"fmt"
)
func main() {
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
可以声明多个类型不同的变量(类型由初始化表达式推导):
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
package main
import (
"fmt"
"math"
)
func main() {
var a = "initial"
var b, c int = 1, 2
var d = true
var e float64
f := float32(e)
g := a + "foo"
fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0
fmt.Println(g) // initialapple
const s string = "constant"
const h = 500000000
const i = 3e20 / h
fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}
2.2.3 基础语法 -- if else
和C基本类似
不同:
if和else if后没有条件判断的括号(小括号)且必须有花括号括住
package main
import "fmt"
func main() {
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 语言只有for循环一种
for后面什么都不写即代表死循环
可以使用经典的C语言的for循环,格式相同,同样也有continue和break的跳出循环方式
以下是集中for循环的使用方法
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
go语言中Switch后面的变量名和if一样不需要括号
和C语言不同的是,C语言case语句后没有break默认走完所有case,
而go语言默认是不走的
在go中Switch还有更加高级的用法,可以在Switch后不加任何的变量名,而 在case后添加条件判断,可以代替if语句使条件的选择更加清晰,代码更加美观
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 基础语法 -- 数组,切片
1.普通数组
和C语法和使用方法基本相同
var a [5]int
a[4] = 100
b := [5]int{1, 2, 3, 4, 5}
var twoD [2][3]int
for i := 0; i < 2; i++ {
for j := 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)
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.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
package main import "fmt" func main() { nums := []int{2, 3, 4} sum := 0 for i, num := range nums { sum += num if num == 2 { fmt.Println("index:", i, "num:", num) // index: 0 num: 2 } } fmt.Println(sum) // 9 m := map[string]string{"a": "A", "b": "B"} for k, v := range m { fmt.Println(k, v) // b 8; a A } for k := range m { fmt.Println("key", k) // key a; key b } }
2.2.9 基础语法 -- 函数
- go语言的函数形参列表和参数类型与C语言位置相反,类型写在形参的后面
- go语言函数的返回值可以是多个,并且在开发时通常返回一个本该返回的值而另一个为错误信息(类似于状态码,message)
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func add2(a, b int) int {
return a + b
}
func exists(m map[string]string, k string) (v string, ok bool) {
v, ok = m[k]
return v, ok
}
func main() {
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语言是不支持指针运算的,如果想进行指针运算,需要引入特殊包,直接对内存进行操作
func add2ptr(n *int) {
*n += 2
}
func main() {
n := 5
add2(n)
fmt.Println(n) // 5
add2ptr(&n)
fmt.Println(n) // 7
2.2.11 基础语法 -- 结构体
- 结构体的使用和C语言类似,创建结构体:
type user struct {
name string
password string
}
- 结构体变量的赋值可以用key:value的方式,有时key也可以省略不写,
也可以只对结构体中的部分变量进行赋值
a := user{name: "wang", password: "1024"}
b := user{"wang", "1024"}
c := user{name: "wang"}
c.password = "1024"
- 结构体也可以作为形参类型,但需要注意只有指针结构体才能改变结构体变量的值
- 结构体还有结构体方法,类似于内成员变量,使用方法为在func后加 结构体变量名 结构体名
- 同样在此处如果要改变变量的值,使用的函数的参数必须为指针变量
具体实例代码如下:
func (u user) checkPassword(password string) bool {
return u.password == password
}
func (u *user) resetPassword(password string) {
u.password = password
}
func main() {
a := user{name: "wang", password: "1024"}
a.resetPassword("2048")
fmt.Println(a.checkPassword("2048")) // true
}
2.2.12 基础语法 -- 错误处理
错误,一种类似java中的异常却不完全相同的类型
- go语言通常将错误作为返回值
- 不同于java的异常,go的错误会清楚的显示错误发生的行与列,并能通过简单的if else语句初期错误
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")
}
例如,在参数列表中加入err类型,如果返回正常,error即返回nil,否则,new一个错误并返回
- 在主函数中同样也要有变量来接收err的值,并主函数中对错误进行相应的处理以保证程序的稳定性
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)
}
}
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标签进行标注
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"}}
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()去获取一个时间戳
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 }
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()
if err != nil {
panic(err)
}
fmt.Println(string(buf)) // 127.0.0.1 localhost
3.实战
3.1猜数字游戏
游戏介绍:每次游戏程序会自动生成一个0~100的随机数,玩家输入自己猜的数字,程序会告诉玩家你猜的数字大于或小于正确答案,直到用户输入正确的答案为止(具体流程如图)
开始实战:
1.首先去生成随机数
这里要导入math/rand包,用maxNum声明并变量赋值,并调用rand包下的Intn函数产生随机数(最大为maxNum)
func main() {
maxNum := 100
secretNumber := rand.Intn(maxNum)
fmt.Println("The secret number is ", secretNumber)
}
但是运行后出现了问题,每次运行都会显示随机数为81
原因是没有设置随机数的种子,导致每次产生的随机数相同
解决方法:通常用时间戳来初始化随机数种子
修改代码如下:
func main() {
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')
if err != 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)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
return
}
fmt.Println("You guess is", guess)
此处要注意包的引入,要包含我们用到的函数,并且每次对数据进行处理时都要进行 必要的错误校验,如果发生异常要进行相应处理。
3.逻辑的判断与循环
- 逻辑 的判断使用if else语句比较即可
- 需要注意的是,整个用户输入,提示语的输出应该是在大循环内的,以保证游戏是一直玩下去的
- 当用户输入了正确答案的时候,要break退出循环
- 用户在输入,程序处理代码时如果出现错误,不是卡死退出,而要continue跳出该回合,用户继续游戏
游戏完整代码如下:
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 在线命令行词典
在命令行输入一个单词会输出这个单词的音标,词性和释义,具体如下图
1.初步尝试
首先打开一个彩云小译的网页,输入要翻译的单词good,点击翻译后,打开网页开发者工具
选择network(网络),找到一个请求方法为POST的 dict,从负载和预览中可以看到一些请求的详细信息,我们在用golang开发时,也要使用对应的api
这种请求代码一般比较复杂,这里介绍一种可以代码生成的方法,首先右键dict选择复制----copy as cURL
打开代码生成网址:curlconverter.com/#go
将我们刚刚复制的一串代码输入到上方curl command(bash),会自动生成代码,我们将其复制到编辑器,代码解析参考注释
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
func main() {
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将字符串转换为流
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,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("referer", "https://fanyi.caiyunapp.com/")
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)//发起请求
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字符串
}
client := &http.Client{}//创建一个httpclient,此处可以指定最大请求时间
var data = strings.NewReader({"trans_type":"en2zh","source":"good"}
)
req, err := http.NewRequest("POST", "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结构体
type DictRequest struct {
TransType string `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)
var data = bytes.NewReader(buf)
接下来就和之前一样创建请求等等
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
代码如下:
type DictRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
UserID string `json:"user_id"`
}
func main() {
client := &http.Client{}
request := DictRequest{TransType: "en2zh", Source: "good"}
buf, err := json.Marshal(request)
if err != nil {
log.Fatal(err)
}
var data = bytes.NewReader(buf)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
if err != nil {
log.Fatal(err)
}
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的字符串
3.解析response body
首先打开(oktools.net/json2go),该网站可以实现json到go的结构体转化
将刚刚翻译出dict的响应处代码复制到网站的json框,可以得到一个超大的go语言结构体
(此处不展示)
然后去修改刚刚的代码,把原本最后的直接打印json串修改为反序列化到我们刚刚获得的结构体中,并用最详细的%#v去打印出来
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != 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 := range dictResponse.Dictionary.Explanations {
fmt.Println(item)
}
5.代码完善
(1)对响应码进行判断,如果不为200,说明可能发生了错误,会导致后续反序列化为空
if resp.StatusCode != 200 {
log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
}
(2)主函数
刚刚仅仅是对一个单词的查询,可以将刚刚写好的函数作为功能函数query,再去编写主函数,将good变成一个变量传入,这样就可以任意查词了
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)
}
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可以类比为开启一个子线程
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)
}
}
process函数的实现
首先创建一个流,再在死循环中去每次读一个字节,并写入slice,如果出错就关闭连接
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命令去测试
nc 127.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
: 不支持任何认证方式
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
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)
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
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
具体代码如下
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
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
}
4.relay阶段建立TCP连接
使用net包下Dial函数去选择对应地址和端口建立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()
双向数据转化,从浏览器拷贝到底层服务器,在从底层服务器拷贝到浏览器,并且开启一个ctx,有一方关闭后返回
ctx, cancel := context.WithCancel(context.Background())
go func() {
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
最终代码如下:
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, 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)
}
_, 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) {
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)
_, 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
}
完成后调命令测试,会出现详细的代理信息
curl --socks5 127.0.0.1:1080 -v http: //www.qq.com