Go 语言实战 - 在线词典 | 青训营笔记

200 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天。今天学习了下 Go 语言的实战案例 —— 实现一个命令行在线词典。

调用第三方 API

命令行在线词典的实现原理是调用第三方的 API 进行翻译,将返回的结果打印到终端上。这里我们以彩云小译的 API 接口为例。

首先打开翻译网站,找到 “开发者工具 - 网络”,接下来在网站提供的翻译框中随意输入一个单词进行翻译,此时在开发者工具中可以看到许多的请求,找到其中提交了我们翻译内容的 POST 方法:

image-20230116124523799

载荷(payload):

image-20230116124846712

预览(preview):

image-20230116124621150

cURL 命令生成代码

有了这个 POST 请求,我们就可以用 Go 语言模拟浏览器向网页提交翻译请求了,但是请求头中包含的内容很多,如果手动构造请求比较麻烦,因此我们可以使用代码生成工具:curlconverter

curlconverter 可以将 cURL 命令转换为各种语言的代码,右键刚才找到的 POST 请求,选择复制 cURL,注意需要复制为 bash 格式:

image-20230116130039741

打开 curlconverter,选择Go语言,将 cURL 粘贴到输入栏中即可得到 Go 语言代码:

image-20230116130740939

直接将代码复制到编辑器,有可能会报错,需要小改一下,一般直接把报错的地方删除即可,不影响代码运行。像这里我复制到编辑器后出现了一个警告,说的是 io/ioutil 这个包已经弃用,其中的内容已经被包含在了它的上一级 io 包中,更改为 io 包即可。

image-20230116131910835

确保没有报错后运行代码,不出意外的话可以得到一串 json 文本:

image-20230116132416799

读取用户输入

我们注意到,自动生成的代码中被翻译的内容是固定的,我们需要能够翻译用户输入的内容,比较简单直接的方法是使用字符串拼接:

var input string
fmt.Scanf("%s", &input)
var data = strings.NewReader(`{"trans_type":"en2zh","source":"`+input+`"}`)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)

更为优雅的方法则是使用 json 序列化的方式,首先构造一个与请求内容中的 json 文本结构相同的结构体:

type DictRequest struct {
	TransType string `json:"trans_type"`
	Source string `json:"source"`
}

创建结构体并赋值后将其进行序列化,然后就可以作为请求内容发送请求了:

var input string
fmt.Scanf("%s", &input)
request := DictRequest{TransType: "en2zh", Source: input}
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)

JSON 反序列化

到目前为止,我们的请求得到的内容是一串 json 文本,为了方便阅读,我们还需要将这串 json 文本进行反序列化保存到结构体中,也就是说我们需要为返回内容也定义一个结构体。但是要手动给如此冗长的 json 文本定义结构体未免太麻烦,所以这一步我们还是选择使用代码生成工具:OKTools

打开工具网址,把返回的 json 文本粘贴上去,选择 “转换-嵌套” ,这样生成的结构体更为紧凑些:

image-20230116170650721

有了结构体定义后就可以将 json 文本进行反序列化了:

var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
	log.Fatal(err)
}

最后便是输出结构体中保存的内容:

fmt.Println(input, "UK:", dictResponse.Dictionary.Prons.En, "US:",
		dictResponse.Dictionary.Prons.EnUs)
for _, item := range dictResponse.Dictionary.Explanations {
		fmt.Println(item)
}

运行结果:

image-20230116172510083

完整代码

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
)

type DictRequest struct {
	TransType string `json:"trans_type"`
	Source string `json:"source"`
}

type DictResponse struct {
	Rc int `json:"rc"`
	Wiki struct {
	} `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 main() {
	client := &http.Client{}
	var input string
	fmt.Scanf("%s", &input)
	request := DictRequest{TransType: "en2zh", Source: input}
	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("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")
	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="99", "Google Chrome";v="109", "Chromium";v="109"`)
	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/109.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 := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	var dictResponse DictResponse
	err = json.Unmarshal(bodyText, &dictResponse)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(input, "UK:", dictResponse.Dictionary.Prons.En, "US:",
		dictResponse.Dictionary.Prons.EnUs)
	for _, item := range dictResponse.Dictionary.Explanations {
		fmt.Println(item)
	}
}

添加其它翻译引擎

添加其它翻译引擎的思路和方法与前文大致相同,这里就不赘述了。在添加了更多的搜索引擎后,我们会发现代码变得非常臃肿,查看起来很不方便,此时我们可以考虑分文件保存代码,将不同翻译引擎的代码分别保存,下面是我分别文件保存代码后的项目目录结构:

.
├── go.mod
└── online_dict
    ├── fanyi
    │   ├── baidu.go
    │   └── caiyun.go
    └── main.go

baidu.go 和 caiyun.go 在同一级目录下,Go 语言要求同一目录下的所有同级文件必须归属同一个包,因此这两份代码都归属于 fanyi 包:

package fanyi

为了使用 fanyi 包中的内容,main.go 中需要导入这个包:

import "myProject/online_dict/fanyi"

路径中的 myProject 可以在 go.mod 文件中找到:

module myProject

go 1.19

这里的 myProject 是先前使用 go mod init 命令初始化项目时所指定的模块导入路径。

还有一点需要注意的是,如果想要在 main.go 中使用 fanyi 包中的函数,我们需要将包中的函数 “导出”,导出的方法很简单,只需要将函数的首字母改为大写即可。

例如有这么一个自行编写的包:

package myMath

func Add(x int, y int) int {
    return x + y
}

func mul(x int, y int) int {
    return x * y
}

其它包引入了 myMath 这个包后,只能使用 Add() 函数,而 mul() 则不可使用。这是因为 mul()myMath 包中的私有函数,仅在本包内可见,包外是无法访问的,而 Add() 函数的首字母为大写,因此被视为导出函数,包内外都可以访问,也就是公有函数。

在分文件后 main.go 看起来就清爽了许多:

package main

import (
	"fmt"
	"sync"
	"myProject/online_dict/fanyi"
)

func main() {
	var word string
	fmt.Scanln(&word)
	fanyi.Caiyun(word)
	fanyi.Baidu(word)
}

实现并发请求

Go 语言通过协程实现并发,协程是轻量级的线程,它的内存消耗和切换调度开销远比线程要小,因此可以更为轻松的实现几万个协程并发执行。

在 Go 中使用协程很简单,只需要在调用的函数前加上 go 关键字,例如:

go fun(a, b, c)

该语句创建了一个协程,协程执行函数 fun() 中的语句。

我们把前面的主函数稍微修改一下就可以实现并发请求了:

func main() {
	var word string
	fmt.Scanln(&word)
	go fanyi.Caiyun(word)
	go fanyi.Baidu(word)
}

但是运行程序时会发现每次输入完单词后什么也没有输出程序就退出了,这是因为 main() 函数启动了协程后会继续向下执行,于是 main() 函数退出了,协程还没来得及执行结束就被一起关闭了,因此我们需要让 main() 等待协程运行结束。

我们可以暴力的用 time.Sleep() 方法强制让 main() 等待 1 秒钟:

func main() {
	var word string
	fmt.Scanln(&word)
	go fanyi.Caiyun(word)
	go fanyi.Baidu(word)
        time.Sleep(time.Second)
}

更优雅的方法在后面的学习中自然会了解到,这里就先不展开了。