这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天。
概述
学习语言的第一步永远是熟悉基本语法。由于我对基本语法比较熟悉,因此没有必要在这上面过多赘述,只有一些值得特殊注意的问题:
- 不同于大部分面向对象的语言,go语言为struct实现方法时,需要特别区分为值实现方法或为指针实现方法。假如为值实现方法,则方法中实际操作调用对象的拷贝,对这个对象的修改在方法执行结束之后不会保留结果,同时拷贝将造成一定的性能浪费。 因此我个人倾向于为指针实现方法。
type Foo struct {
bar string
}
func (f Foo)do() {
f.bar = "foobar"
}
func main() {
f := Foo{bar: "Foobar"}
f.do()
fmt.Println(f.bar) // Foobar
}
- 常用库的使用方法,例如
fmt、json、time和strconv,每个语言都有类似的库,整体来说大同消息。没有必要硬背下来用法,可以在使用时查文档。
三个实战程序
猜数字
猜数字是经典的入门程序,它随机生成1~100的数字,用户每次猜测一个数字,程序返回猜测偏大还是偏小,直到猜中。这个程序的期望交互模式如下:
$ go run guessing-game/v5/main.go
Please input your guess
50
You guess is 50
Your guess is bigger than the secret number. Please try again
25
You guess is 25
Your guess is smaller than the secret number. Please try again
37
You guess is 37
Your guess is smaller than the secret number. Please try again
43
You guess is 43
Correct, you Legend!
特别的,当输入非整数时,程序将输出Invalid input. Please enter an integer value。
在听讲之前,我首先尝试自己编写这个较为简单的程序,实现思路如下:
- 使用随机库,以时间为种子,生成随机数,对100取模+1,提示用户游戏开始
- 进入游戏循环
- 读取用户输入,当输入不合法时提醒用户重新输入
- 复述用户输入内容
- 与生成的随机数比较,并返回给用户的提示信息
- 若输入正确,则跳出循环,否则继续循环
核心代码如下:
func main() {
target := rand.Int()%100 + 1
rand.Seed(time.Now().UnixNano())
fmt.Println("Please input your guess")
input := bufio.NewReader(os.Stdin)
for {
line, _, _ := input.ReadLine()
guess, err := strconv.ParseInt(strings.Trim(string(line), "\n"), 10, 32)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
continue
}
fmt.Println("You guess is", guess)
if int(guess) == target {
fmt.Println("Correct, you Legend!")
break
} else if int(guess) > target {
fmt.Println("Your guess is bigger than the secret number. Please try again")
} else {
fmt.Println("Your guess is smaller than the secret number. Please try again")
}
}
}
这个程序需要综合运用if、for、break和一些基础库如rand、bufio,但没有运用到struct,属于较为基础的练习。
我想到的代码与课程资源中的代码基本一致,区别在于我手写了取模,没有调用randn(),这是由于对API不熟悉,故使用了简单且显然可行的方式。
在线词典
这个程序需要读取程序参数,调用API,完成词典查询的工作,在标准输出中输出结果。
期望运行如下:
$ go run simpledict/v4/main.go hello
hello UK: [ˈheˈləu] US: [həˈlo]
int.喂;哈罗
n.引人注意的呼声
v.向人呼(喂)
这个程序涉及到的知识点有:发送http请求,处理json数据,在网页上抓包,是一个相对综合的练习程序。课程中使用的翻译API为彩云小译。
获取API
我们可以直接通过网页检查的方式获取API以及基本的用法。在网页中发送翻译请求,通过网络可以对流量抓包,在这里找到API。在载荷界面,我们可以看到请求需要的数据内容。在案例中,为一段json数据:{"trans_type":"en2zh","source":"hello"},直接阅读便可发现,前者是翻译类型,后者则是翻译内容。我们只需要使用正确的URL,方法,载荷即可调用这一API。
这一API的返回值可以在预览中看到。在我们的程序中,需要获取音标,翻译。这两个内容分别在dictionary.prons和dictionary.explanations中。我们的程序需要解析返回的json数据,获取这两条内容并打印。
转换API为Go语句
有一个小技巧可以将请求转化为curl命令内容,并随后转化为Go语句。在网络界面对应的名称右键复制,选择复制为cURL(选择一种格式),接下来到工具网站curlconverter中转化即可。
转移结果如下:
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-TW;q=0.9,zh;q=0.8")
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工具获取请求返回值
接下来我们需要生成json请求体。虽然在这个案例中可以直接手动创建,但使用json会是可扩展性更强的方案。
首先是请求对应的结构体类型
type DictRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
UserID string `json:"user_id"`
}
使用json:"trans_type"可以指明转化为json时的标签。
要将返回转化为对应的结构体,比较复杂。使用oktools可以方便地完成转化。
最后只需要调用相应的json.Unmarshal接口即可将返回请求解析为Go对象,读取需要的值并输出。
SOCKS5代理
SOCKS5是一个简单的代理协议,在这个程序中我们要实现一个代理服务器。
代理原理
建立SOCKS5代理分为4个阶段:
- 握手阶段,浏览器向代理服务器发送请求,包括协议的版本号和认证的种类,socks5服务器将从其中选择一个认证。
- 认证流程
- 请求阶段,浏览器向代理服务器发送请求,主要内容为版本号和请求的类型,主要为连接请求。
- relay阶段,此阶段为正常工作,浏览器发送请求,代理服务器进行转发。
编写程序
TCP连接
程序首先从一个简单的echo服务器开始,我们在这里完成基本的tcp连接过程
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
}
}
}
Auth认证
第二步是完成认证,握手阶段中,认证的内容分为三个部分
- 协议版本号。我们的协议只支持socks5。
- 方法数目。
- 每个方法的编码。
前两者只占一个字节,我们先使用IO的ReadByte读取这两个内容,如果不符合协议则写回error。如果前两者都正确,那么就make一个切片存放所有方法的编码。目前只有两种方法:00不认证,02用户名密码认证
最后需要返回版本和选择的方法编号。
请求阶段
请求阶段中,服务器要读取携带URL或IP地址+端口的包,并打印出来。
浏览器会发送一个包,包含:版本号,请求类型,RSV保留字段,atype目标地址类型,addr,port。我们需要逐个读取这些字段。
收到包之后,需要返回一个包,主要包括版本号,返回类型(成功时为0),保留字段,atype地址类型(1)。
relay阶段
最后我们需要实际向外部发送请求。
直接使用net.dial建立TCP连接,使用两个goroutine进行双向传输。