go语言基础+代码实践+作业|青训营笔记

259 阅读8分钟

这是我参与「第三届青训营-后端场」笔记创作活动的第1篇笔记

0.课前准备

代码地址 github.com/wangkechun/…

1.语言基础

go语言的特点

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

基础语法

  1. package main 表示文件属于main包。main包为程序的入口。
  2. go语言是强类型语言;字符串为内置类型,可以用“+”拼接或用“==”比较;var或:=定义变量,const定义常量,常量无确定类型,使用时编译器根据上下文推导。
  3. if没有小括号,但必须有大括号。
  4. 所有循环都用关键字for实现。
  5. switch无需小括号,无需break。switch的类型灵活,case里判断种类丰富,可替代连续if-else。
  6. 数据结构用make定义。切片slice,与python的list相似,但不支持负数索引。slice底层存储容量、数据以及指向数据的指针。
  7. goloang的函数通常返回多个值,包括函数返回值+错误信息。
  8. 支持指针、结构体。在关键字func和函数名之间加入小括号(this struct)或(this *struct)可以定义struct的方法。
  9. 函数返回值一般设为(val,err),成功时返回(val,nil),错误时返回(nil,errors.New("报错信息"))。
  10. 若结构体中所有属性命名都为首字母大写的形式,则结构体可以用 buf,err:=json.Marshal(对象) 进行序列化,将对象转化为16进制编码存储在buf中。 err=json.Unmarshal(buf,&对象)可进行反序列化。属性定义行后加上 `json:"新属性名"` 即可在输出时将属性名更改为新属性名,一般通过这种方式把首字母大写形式改为全小写模式(也可以改成中文)。
  11. 时间解析不能用"yyyy-MM-dd hh:mm:ss"而是用"2006-01-02 15:04:05"。
  12. os.Args获取进程运行时的命令行参数,例如go run main.go arg1 arg2,则os.Args包含3个参数:main.go的绝对路径、arg1和arg2。os.Getenv("PATH")获取环境变量。os.Setenv("key","val")设置环境变量(只能用字符串给字符串赋值)。exec.Command("命令","参数").CombinedOutput()快速启动子进程并获取其输出。

2.代码实践

我使用的是ubuntu20.04+vscode的编译环境,谷歌浏览器。

2.1 猜谜游戏

一个简单的小程序

课后作业:将输入用fmt.Scanf简化

fmt.Scanf与C++的scanf相似。以案例的v5版本为基础,进行后续操作即可完成修改:

  1. 将第20行改为变量声明 reader := bufio.NewReader(os.Stdin)->var input string

由于scanf需要指定往某一个变量里写值,所以必须先声明变量reader

  1. 将第22行改为scanf输入 input, err := reader.ReadString('\n')->_, err := fmt.Scanf("%s",&input)

如果改为%d读取整形也可以实现,但后续代码也需要更改(v5后面有将字符串转化为整形的代码)。由于v5版本读取的就是字符串,这里我追求最少更改,所以也读取字符串就好了。函数内部不能写%v,因为这样会让编译器以为要读整形导致后面类型转换的时候报错。

  1. 将第27行注释掉 //input = strings.TrimSuffix(input, "\n")

  2. 删除不需要的包

结果对比: v5版本对于任何非法输入都会报错,而改为scanf输入之后,若输入“num1 num2”不会报错,而是将num1和num2视为两次独立的输入。

2.2 命令行词典

相关网页:

彩云翻译:fanyi.caiyunapp.com/

json代码生成网址:curlconverter.com/#go

json代码转化网址:oktools.net/json2go

操作步骤

  1. 打开在彩云翻译的网页打开开发者工具(F12打开)。
  2. 输入一个单词(例如输入“good”),点击翻译,找到方法为"POST"名字为"dict"的数据包,右键点击->复制->复制为cURL命令。
  3. 打开json代码生成网址,在方框中粘贴cURL,下面语言选择go,复制下面生成的代码。一定要用谷歌浏览器,如果使用火狐浏览器,则设置请求头部分(req.Header.Set)差别很大,且生成的代码运行只会输出乱码。
  4. 运行生成的代码,可以得到结果。
  5. 添加数据包
"bytes"
"encoding/json"
//“strings”
  1. 添加结构体
type DictRequest struct{
    TransType string `json:"trans_type"`
    Source string `json:"source"`
    UserID string `json:"user_id"`
}

7.将main函数第二行data的赋值方式修改为如下代码:

//var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
request := DictRequest{TransType:"en2zh",Source:"good"}
buf,err := json.Marshal(request)
if err != nil{
    log.Fatal(err)
}
var data = bytes.NewReader(buf)

此时代码的输出与第4步相同。得到的结果是一个json文件。

  1. 为了解析json文件,将其转化为我们能看懂的信息,我们需要在json代码转化网址中将之前的输出转化为一个结构体,方便我们从结构体中读取数据。具体方法为将之前的输出复制到json代码转化网址的json输入框,点击转换-嵌套。复制生成的结构体,将其名称改为DictResponse,将main函数最后的输出改为如下代码:
if resp.StatusCode != 200 {
    log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
}
//fmt.Printf("%s\n", bodyText)
var dictResponse DictResponse
err = json.Unmarshal(bodyText,&dictResponse)
if err != nil{
    log.Fatal(err)
}
fmt.Printf("%#v\n",dictResponse)

其中,注释行之前的代码是为了防止服务端的回应并不是正确状态(有可能返回403、404,这样之后反序列化的结果会为空),注释行之后的代码是反序列化和详细输出。

  1. 再将最后一行改为如下代码:
//fmt.Printf("%#v\n",dictResponse)
fmt.Println(word,"UK",dictResponse.Dictionary.Prons.En,"US:",dictResponse.Dictionary.Prons.EnUs)
for _,item := range dictResponse.Dictionary.Explanations{
    fmt.Println(item)
}
  1. 将原来的main()函数改为query(word string),请求的赋值中将单词“good”改为变量word。增加新的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)
}

同时调用包添加"os"。报错输出中的换行以及删除缩进可以增加输出的可读性,且换行无法用"\n"代替。

课后作业:

增加另一种翻译引擎的支持

支持另一种引擎可按之前流程再做一遍。使用火山翻译,找到前缀为?msToken的数据包。有多个,找到只包含language和text的包,然后按照之前的流程继续就可以了。

一些翻译引擎可能因为返回数据包太多找不到目标数据包,在过滤中选择Fetch/XHR可减少大量无用的数据包。

其他翻译引擎遇到的问题:

谷歌翻译是一个前缀为batchexecute的数据包,其preview中能找到查找的关键字。但其数据的格式不是json格式,因此难以解析。

百度翻译会有一个v2transapi为前缀的数据包,为我们的目标数据包。数据是用json传输的,但转化出来的格式是一个长字符串,难以确定请求结构体。若直接更改长字符串,则会出现只能查一个词的情况(用单词good转化代码则只能查good,查其他单词出现报错,报错代码998)。

有道翻译遇到的问题与百度翻译相似。

并行请求两个翻译引擎以提高响应速度

var wg sync.WaitGroup
wg.Add(2)
go func(w string){
    defer wg.Done()
    querycaiyun(word)
}(word)
go func(w string){
    defer wg.Done()
    queryhuoshan(word)
}(word)
wg.Wait()

使用go关键字,通过协程并行。可能出现还没有输出则程序结束的情况,因此加入"sync"包,设置wg==2,每个go函数执行完会调用wg.Done(),每调用一次wg减少1,wg.Wait()会等待指导wg==0。query中的变量写word或w都能通过。

2.3 SOCKS5代理

2.3.1 原理

  1. 客户端与socks5服务器建立连接。
  2. 客户端与socks5服务器保持连接,socks5服务器与目标主机建立tcp连接。
  3. 客户端与目标主机通过socks5服务器延迟通信。

2.3.2 操作步骤

  1. 运行v1,之后另开一个终端,输入nc 127.0.0.1 1080(连接本机的1080端口)。新的终端中输入任意字符串,会返回输入字符串。
  2. 运行v2,另一个终端输入curl --socks5 127.0.0.1:1080 -v www.qq.com 。v2的代码中auth为认证函数,它读取一个只读流和一个tcp连接。此时的输出应为“ver 5 method [0 1]”,对应只读流中包含的版本信息以及建立连接的方法数量。
  3. v3对应socks5的第一步,socks5服务器对客户端进行认证+连接。v4为socks5服务器连接目标主机。net.Dial("tcp",addr,port)函数可以快速对指定ip和端口建立tcp连接。之后使用io.Copy函数帮助客户和目标主机传递信息,此函数是单向的,因此需要有两个go func(){}。由于go func(){}执行很快,为了等待双方的数据传输完成,需要在之前用ctx,cancel:=context.WithCanel(context.Background())建立ctx,在之后用<-ctx.Done()关闭,以保持连接持续。此时只有一方终端链接程序才会结束。