这是我参与「第五届青训营」伴学笔记创作活动的第 5 天
本文是对Go基础语法实战案例的整理与补充
Go基础语法实战案例——在线词典
在本案例中,用户可以在命令行查询一个单词,然后我们通过调用第三方的API查询到单词的翻译并打印出来。在这个例子里,我们会学习如何用Go来发送HTTP请求、解析json,还会学习如何使用代码生成从而提高效率。
抓包
以彩云科技提供的在线翻译为例
打开我们要用的网站,右键检查进入浏览器的开发者模式。在网页左侧框中输入你要翻译的英文,点击翻译(实际上你输入以后网页会自动发送请求进行翻译),然后在开发者工具找到network(网络),然后从下往上找名为dict的请求,注意要找请求方法为POST的。
对这个查询单词的请求进行分析后,发现它的请求标头非常复杂,有十来个。在Payload(载荷)中,有一个json里面有两个字段——source和trans_type,前者是你要查询的单词,后者是你要从什么语言转换成什么语言。
在Preview(预览)中可以看到API的返回结果,有dictionary和wike两个字段,我们需要用的结果主要在dictionary.Explanations字段里面。其他有些字段好包括音标等信息。
代码生成
分析完请求后,我们要想办法在Golang里发送这个请求。
因为这个请求很复杂,用代码构造很麻烦,实际上我们有一种非常简单的方式来生成代码,右键要构造的请求,选择copy(复制)里的cURL(bash)格式复制。copy完成之后在终端粘贴一下curl命令,应该可以成功返回一大串json。然后我们打开一个代码生成网站,粘贴刚刚复制的cURL请求。选择POST,语言选择Go,会自动生成请求代码,其中可能有转义导致的编译错误,删掉这几行即可。下面的代码是我这边生成的和样例给的有点不一样,但不影响结果。
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
func main() {
client := &http.Client{}
var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
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;q=0.9")
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)
}
代码解析
下面我们来分析一下生成的代码
client := &http.Client{}创建一个HTTP client,创建的时候可以指定很多参数,包括请求超时的设置,是否使用cookie。req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)创建请求,第一个参数是HTTP方法,第二个参数是URL,最后一个参数是Body,Body因为可能很大,为了支持流式发送,是一个只读流。strings.NewReader把字符串转换成一个流。req.Header.Set()设置了很多header(请求头),实际上很多是可以删掉的,不影响结果。resp, err := client.Do(req)发送请求,如果出现断网、DNS解析等原因请求失败,就会打印错误并且退出进程。defer resp.Body.Close()上面发送请求成功后,我们会拿到response,按照Golang的编程习惯,我们会加一个defer,因为返回的resp.Body也是一个流,为了避免资源泄漏,我们通过defer来手动关闭这个流,defer会在函数运行结束后从下往上执行。bodyText, err := ioutil.ReadAll(resp.Body)读取流,得到整个Body然后打印。
生成Request Body
通过上面生成的代码,我们已经能够成功地发出请求,并把返回的JSON打印出来,但是输入的英文单词是固定的,所以我们需要一个变量来输入,我们需要用到JSON序列化。
JSON序列化
在Golang里面,我们需要生成一段JSON,常用的方式是先构造一个结构体,这个结构和我们需要生成的JSON的结果是一一对应的。比如dict请求头里有两个字段——source和transtype,那我们的结构体也应该包含这两个字段。下面是样例给的结构体:
type DictRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
UserID string `json:"user_id"`
}
定义好结构体后,我们定义一个结构体变量request := DictRequest{TransType: "en2zh", Source: "good"},再通过buf, err := json.Marshal(request)来得到序列化之后的字符串。需要注意的是,json.Marshal返回的是[]byte而不是strings,所以我们将strings.newReader改为bytes.newReader得到请求需要的Body,之后的步骤不变。
解析response body
上面的生成request body给出了如何将输入的变量转为JOSN格式发送请求,下面我们需要把请求得到的response body解析出来。
在Go中,最常用的方式是和request一样,写一个结构体,把返回的JSON反序列化到结构体里面。但是我们可以看到这个API返回的结果非常复杂,如果我们人为定义结构体,非常繁琐且容易出错。
这里我们使用网上现有的代码生成工具,只需要把JSON字符串粘贴进去,就能生成对应的结构体。在某些时刻,我们如果不需要对这个返回结果做很多精细的操作,可以选择转换嵌套,让生成的代码更紧凑。这里我们直接选择开发者模式下的Response(响应)中的JSON进行生成。下面是生成的嵌套式的结构体:
type AutoGenerated struct {
Rc int `json:"rc"`
Wiki struct {
KnownInLaguages int `json:"known_in_laguages"`
Description struct {
Source string `json:"source"`
Target interface{} `json:"target"`
} `json:"description"`
ID string `json:"id"`
Item struct {
Source string `json:"source"`
Target string `json:"target"`
} `json:"item"`
ImageURL string `json:"image_url"`
IsSubject string `json:"is_subject"`
Sitelink string `json:"sitelink"`
} `json:"wiki"`
Dictionary struct {
Prons struct {
EnUs string `json:"en-us"`
En string `json:"en"`
} `json:"prons"`
Explanations []string `json:"explanations"`
Synonym []string `json:"synonym"`
Antonym []string `json:"antonym"`
WqxExample [][]string `json:"wqx_example"`
Entry string `json:"entry"`
Type string `json:"type"`
Related []interface{} `json:"related"`
Source string `json:"source"`
} `json:"dictionary"`
}
这样我们就得到了response结构体。接下来我们修改代码,我们先定一个response结构体的对象,然后我们用JSON.unmarshal把Body反序列化到这个结构体,再打印出来。下面是得到response之后反序列化并输出的代码,这里打印的时候使用%#v,这样可以让打印出来的结果容易阅读。
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", dictResponse)
打印结果
上面得到的结果如下:
main.DictResponse{Rc:0, Wiki:struct { KnownInLaguages int "json:"known_in_laguages""; Description struct { Source string "json:"source""; Target interface {} "json:"target"" } "json:"description""; ID string "json:"id""; Item struct { Source string "json:"source""; Target string "json:"target"" } "json:"item""; ImageURL string "json:"image_url""; IsSubject string "json:"is_subject""; Sitelink string "json:"sitelink"" }{KnownInLaguages:63, Description:struct { Source string "json:"source""; Target interface {} "json:"target"" }{Source:"tangible and intangible thing, except labor tied services, that satisfies human wants and provides utility", Target:interface {}(nil)}, ID:"Q28877", Item:struct { Source string "json:"source""; Target string "json:"target"" }{Source:"good", Target:"商品"}, ImageURL:"www.caiyunapp.com/imgs/link_d…", IsSubject:"true", Sitelink:"www.caiyunapp.com/read_mode/?…, Dictionary:struct { Prons struct { EnUs string "json:"en-us""; En string "json:"en"" } "json:"prons""; Explanations []string "json:"explanations""; Synonym []string "json:"synonym""; Antonym []string "json:"antonym""; WqxExample [][]string "json:"wqx_example""; Entry string "json:"entry""; Type string "json:"type""; Related []interface {} "json:"related""; Source string "json:"source"" }{Prons:struct { EnUs string "json:"en-us""; En string "json:"en"" }{EnUs:"[gʊd]", En:"[gud]"}, Explanations:[]string{"a.好的;善良的;快乐的;真正的;宽大的;有益的;老练的;幸福的;忠实的;优秀的;完整的;彻底的;丰富的", "n.利益;好处;善良;好人", "ad.=well"}, Synonym:[]string{"excellent", "fine", "nice", "splendid", "proper"}, Antonym:[]string{"bad", "wrong", "evil", "harmful", "poor"}, WqxExample:[][]string{[]string{"to the good", "有利,有好处"}, []string{"good, bad and indifferent", "好的,坏的和一般的"}, []string{"good innings", "长寿"}, []string{"good and ...", "很,颇;完全,彻底"}, []string{"do somebody's heart good", "对某人的心脏有益,使某人感到愉快"}, []string{"do somebody good", "对某人有益"}, []string{"be good for", "对…有效,适合,胜任"}, []string{"be good at", "在…方面(学得,做得)好;善于"}, []string{"as good as one's word", "信守诺言,值得信赖"}, []string{"as good as", "实际上,几乎等于"}, []string{"all well and good", "也好,还好,很不错"}, []string{"a good", "相当,足足"}, []string{"He is good at figures . ", "他善于计算。"}}, Entry:"good", Type:"word", Related:[]interface {}{}, Source:"wenquxing"}}
很显然这还是太复杂了,我们需要通过某种方式来筛选这些JOSN串从而得到我们想要的结果。
观察response的json结构可以看出我们需要的结果是在Dictionary.explanations。我们用for range循环来迭代它,然后直接打印结果,参照一些词典的显示方式,我们可以在那个前面打印这个单词和它的音标。
完善代码
现在我们的程序的输入还是写死的,我们把代码的主体改为一个query函数,查询的单词作为参数传递进来。我们写一个简单的main函数,这个main函数首先判断一下命令和参数的个数,如果不是两个就打印错误信息,退出程序,否则就获取到用户输入的单词,然后执行query函数。最终代码如下:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
)
type DictRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
UserID string `json:"user_id"`
}
type DictResponse struct {
Rc int `json:"rc"`
Wiki struct {
KnownInLaguages int `json:"known_in_laguages"`
Description struct {
Source string `json:"source"`
Target interface{} `json:"target"`
} `json:"description"`
ID string `json:"id"`
Item struct {
Source string `json:"source"`
Target string `json:"target"`
} `json:"item"`
ImageURL string `json:"image_url"`
IsSubject string `json:"is_subject"`
Sitelink string `json:"sitelink"`
} `json:"wiki"`
Dictionary struct {
Prons struct {
EnUs string `json:"en-us"`
En string `json:"en"`
} `json:"prons"`
Explanations []string `json:"explanations"`
Synonym []string `json:"synonym"`
Antonym []string `json:"antonym"`
WqxExample [][]string `json:"wqx_example"`
Entry string `json:"entry"`
Type string `json:"type"`
Related []interface{} `json:"related"`
Source string `json:"source"`
} `json:"dictionary"`
}
func query(word string) {
client := &http.Client{}
request := DictRequest{TransType: "en2zh", Source: word}
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)
}
req.Header.Set("Connection", "keep-alive")
req.Header.Set("DNT", "1")
req.Header.Set("os-version", "")
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
req.Header.Set("app-name", "xy")
req.Header.Set("Content-Type", "application/json;charset=UTF-8")
req.Header.Set("Accept", "application/json, text/plain, */*")
req.Header.Set("device-id", "")
req.Header.Set("os-type", "web")
req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
req.Header.Set("Sec-Fetch-Site", "cross-site")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
req.Header.Set("Cookie", "_ym_uid=16456948721020430059; _ym_d=1645694872")
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)
}
if resp.StatusCode != 200 {
log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
}
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
for _, item := range dictResponse.Dictionary.Explanations {
fmt.Println(item)
}
}
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)
}
运行的时候注意是把go run main.go和你要查询的单词一起输入到终端里。
总结
在该实战案例中,我们接触了最简单的请求、返回以及对JOSN串和结构体的相互处理。