Go基础语法实战案例——在线词典 | 青训营笔记

124 阅读9分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 5 天

本文是对Go基础语法实战案例的整理与补充

Go基础语法实战案例——在线词典

在本案例中,用户可以在命令行查询一个单词,然后我们通过调用第三方的API查询到单词的翻译并打印出来。在这个例子里,我们会学习如何用Go来发送HTTP请求、解析json,还会学习如何使用代码生成从而提高效率。

抓包

彩云科技提供的在线翻译为例

打开我们要用的网站,右键检查进入浏览器的开发者模式。在网页左侧框中输入你要翻译的英文,点击翻译(实际上你输入以后网页会自动发送请求进行翻译),然后在开发者工具找到network(网络),然后从下往上找名为dict的请求,注意要找请求方法为POST的。

翻译网站.png 对这个查询单词的请求进行分析后,发现它的请求标头非常复杂,有十来个。在Payload(载荷)中,有一个json里面有两个字段——source和trans_type,前者是你要查询的单词,后者是你要从什么语言转换成什么语言。

请求头.png 在Preview(预览)中可以看到API的返回结果,有dictionary和wike两个字段,我们需要用的结果主要在dictionary.Explanations字段里面。其他有些字段好包括音标等信息。

预览.png

代码生成

分析完请求后,我们要想办法在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解析出来。

需要解析的response body.png 在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串和结构体的相互处理。