GO语言实战案例 猜数字&在线词典 | 青训营

143 阅读13分钟

GO语言实战案例 猜数字&在线词典

前置

笔者环境

  • macos 10.15.7
  • Golang 1.18
  • GoLand 2022.01

读完本文可以获得

  1. 标准输入的读取与处理
  2. 随机生成指定区间的整数
  3. 发送Http请求
  4. 解析JSON
  5. 代码生成工具

一、 猜谜游戏

项目效果

随机产生一个数值(1-100以内),用户通过控制台输入数字,程序处理输入并提示用户相关信息(比随机大、小或是猜对了)。

Screen Shot 2023-07-27 at 2.28.44 PM.png

学习内容

  • 标准输入的读取与处理

  • 随机生成指定区间的整数

1.1 通过bufio.Reader读取输入

bufio.Reader

bufio.Reader是Go语言bufio包下提供的带缓冲的Reader实现,可以有效提高文本IO的效率。

主要的功能和优点有:

  1. 缓冲读取 - bufio.Reader使用内部缓冲区,减少读取次数,提高效率。

  2. 非阻塞读取 - 支持设置超时,可以实现非阻塞IO。

  3. 文本读取 - 支持按行读取字符串,方便处理文本输入。

  4. 灵活读取 - 提供Peek,ReadBytes等多种读取方法,可以灵活控制读取。

  5. 错误处理 - 所有操作返回错误对象,可以直接判断。

使用bufio.Reader主要分3步:

  1. 创建Reader对象:
reader := bufio.NewReader(os.Stdin)
  1. 调用读取方法,如ReadString()等。

  2. 使用错误检查。

例如:

reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n') 
if err != nil {
    return err
}

这样就可以从标准输入缓冲读取一行文本了。

功能实现

通过读取标准输入并转换为int类型,并与随机数值进行比较,并提示用户相关信息。同时通过判断error异常信息,来确保用户输入的值是个整数。

package main

import (
   "bufio"
   "fmt"
   "math/rand"
   "os"
   "strconv"
   "strings"
)

func main() {

   maxNum := 101 // 随机数的最大值
   // rand.Seed在1.20版本之前,默认的随机数生成器是rand.Seed(1);1.20版本后,默认的随机数生成器会在程序启动时随机地初始化种子值,保证保证每次启动的随机数序列都不同。
   // rand.Seed(time.Now().UnixNano()) // 使用启动时间戳随机初始化种子,保证每次启动的随机数序列都不同。
   secretNum := rand.Intn(maxNum) // 返回[0,101)直接的随机数
   for {
      fmt.Print("Please input your guess:")
      reader := bufio.NewReader(os.Stdin)   // 创建一个从标准输入读取数据的缓冲读取器
      input, err := reader.ReadString('\n') // 读取用户输入的一行字符串,直到遇到换行符’\n’
      if err != nil {
         fmt.Println("An error occurred while reading input. Please try again.")
         continue
      }
      input = strings.TrimSuffix(input, "\n") // 去掉input字符串末尾的换行符
      guess, err := strconv.Atoi(input)       // 将input字符串转换为int类型
      if err != nil {
         fmt.Println("Invalid input. Please enter an integer between 1 and 100.")
         continue
      }
      if guess > secretNum {
         fmt.Println("Your guess is greater than the secret number. Please try again.")
      } else if guess < secretNum {
         fmt.Println("Your guess is less than the secret number. Please try again.")
      } else {
         fmt.Println("Correct, you legend!")
         break
      }
   }

}

1.2 通过fmt.Scanf读取输入

fmt.Scanf

fmt.Scanf()是Go语言fmt包下的一个输入函数,用于从标准输入中读取格式化的文本输入。类似与C语言的scanf()函数。

基本使用格式:

fmt.Scanf(format string, a ...interface{})

其中format参数指定了格式控制符,后面可以跟多个变量,用于接收输入的数据。

例如:

fmt.Scanf("%d %s", &num, &name) 

这会从标准输入读取一个整数到num,以及一个字符串到name

fmt.Scanf会阻塞等待用户输入,输入回车后才会返回结果。

常用的格式控制符有:

  • %d - 十进制整数

  • %x - 十六进制整数

  • %f - 浮点数

  • %s - 字符串

fmt.Scanf会跳过空白,遇到匹配的格式才会读取输入。

与之相似的函数还有fmt.Scan()和fmt.Scanln(),不同的是它们会以空格分割,而Scanf可以指定格式。

功能实现

package main

import (
	"bufio"
	"fmt"
	"math/rand"
	"os"
)

func main() {

	maxNum := 101                  // 随机数的最大值
	secretNum := rand.Intn(maxNum) // 返回[0,101)直接的随机数
	guess := 0                     // 用户猜的数字
	for {
		fmt.Print("Please input your guess:")
		_, err := fmt.Scanf("%d", &guess) // 读取用户输入的数字
		if err != nil {
			fmt.Println("Invalid input. Please enter an integer between 1 and 100.")
			bufio.NewReader(os.Stdin).ReadBytes('\n') // 清空缓冲区,避免读取到多余的字符,如错误输入后的换行
			continue
		}
		if guess > secretNum {
			fmt.Println("Your guess is greater than the secret number. Please try again.")
		} else if guess < secretNum {
			fmt.Println("Your guess is less than the secret number. Please try again.")
		} else {
			fmt.Println("Correct, you legend!")
			break
		}
	}

}

1.3 差异

差异点fmt.Scanf()bufio.NewReader
读取方式通过格式字符串读取通过ReadString等方法读取
处理空格跳过空白字符保留空格和换行符
错误处理返回读取的数目返回错误对象
阻塞方式完全阻塞支持超时非阻塞
缓存机制无缓存使用缓冲区缓存

总结一下:

  • fmt.Scanf() 通过格式化读取输入,遇到匹配才读取
  • bufio.NewReader 通过缓冲和非格式化方式读取
  • 前者读取方式简单直接,后者更灵活可控

二、 在线词典

项目效果

通过读取命令行参数,调用第三方API查询到单词的翻译并打印出来

Screen Shot 2023-07-27 at 8.32.09 PM.png 新增一个引擎

Screen Shot 2023-08-04 at 2.41.57 PM.png

学习内容

  • 发送Http请求
  • 解析JSON
  • 代码生成

2.1 抓包

  1. 进入彩云小译 - 在线翻译 (caiyunapp.com)

    Screen Shot 2023-07-27 at 3.05.00 PM.png

  2. 点击检查或F12进入控制台功能,并点击网络选项

    Screen Shot 2023-07-27 at 3.06.36 PM.png

  3. 在翻译框中输入“hello”,发现下方出现的若干个dict名称的条目

    Screen Shot 2023-07-27 at 3.09.55 PM.png

  4. 点击dict名称的条目,找到POST请求

    Screen Shot 2023-07-27 at 3.13.02 PM.png

  5. 点击预览,我们想的信息是dictionary里的explanations字段

    Screen Shot 2023-07-27 at 3.14.46 PM.png

  6. 我们要在Go里面发这个Http的请求,然后显示explanations的信息。因此右键点击名称下的dict,复制为cURL

    Screen Shot 2023-07-27 at 3.17.56 PM.png

    复制完成后,cURL命令如下

    curl 'https://api.interpreter.caiyunai.com/v1/dict' \
      -H 'authority: api.interpreter.caiyunai.com' \
      -H 'accept: application/json, text/plain, */*' \
      -H 'accept-language: zh,en;q=0.9' \
      -H 'app-name: xy' \
      -H 'content-type: application/json;charset=UTF-8' \
      -H 'device-id: 02374c968dc8ea87ec9f04d6890e8bd7' \
      -H 'origin: https://fanyi.caiyunapp.com' \
      -H 'os-type: web' \
      -H 'os-version;' \
      -H 'referer: https://fanyi.caiyunapp.com/' \
      -H 'sec-ch-ua: "Not.A/Brand";v="8", "Chromium";v="114", "Microsoft Edge";v="114"' \
      -H 'sec-ch-ua-mobile: ?0' \
      -H 'sec-ch-ua-platform: "macOS"' \
      -H 'sec-fetch-dest: empty' \
      -H 'sec-fetch-mode: cors' \
      -H 'sec-fetch-site: cross-site' \
      -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67' \
      -H 'x-authorization: token:qgemv4jr1y38jyq6vhvi' \
      --data-raw '{"trans_type":"en2zh","source":"hello"}' \
      --compressed
    
  7. 进入Convert curl commands to Go (curlconverter.com),该网页可以运行curl命令,并生成Http请求的代码。我们选择生成Go语言的代码。

    Screen Shot 2023-07-27 at 3.25.40 PM.png

  8. 将代码copy到IDE中,然后进行编译运行

Screen Shot 2023-07-27 at 3.33.26 PM.png

如果有报错,可能是由于有几个header比较复杂,生成的代码有转义导致的错误,将标红的代码删除再次编译运行即可。

2.2 发送HTTP请求

HTTP请求的基本过程

// 创建HTTP客户端
client := &http.Client{} 

// 构造请求Body
var data = strings.NewReader(`{"trans_type":"en2zh","source":"hello"}`)

// 创建POST请求,指定翻译API地址  
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)

// 设置请求头信息
req.Header.Set("Content-Type", "application/json") 

// 发送请求
resp, err := client.Do(req)

// 关闭响应体
// Close() 是在逻辑上关闭,允许资源重用。但物理上数据仍在,可以读取。只有等 Go 垃圾回收时,内存才会真正释放。
defer resp.Body.Close() 

//读取响应体
body,err := io.ReadAll(resp.Body)

// 打印翻译结果
fmt.Println(string(body))

HTTP请求基本流程是:

  1. 创建client对象,它负责处理请求细节

  2. 构造请求数据,如JSON Body

  3. 用http.NewRequest创建请求对象,指定方法、URL等

  4. 设置请求头部信息,如Content-Type等

  5. 使用client.Do发送请求

  6. 处理响应,一般需要读取resp.Body

  7. 关闭响应体

  8. 对响应数据进行处理

通过这些步骤,我们就可以通过HTTP请求获取API的数据了。

更改请求体

  1. 我们运行项目后,控制台打印了响应体的内容,是一段JSON,表示请求发送成功

    {"rc":0,"wiki":{},"dictionary":{"prons":{"en-us":"[h\u0259\u02c8lo]","en":"[\u02c8he\u02c8l\u0259u]"},"explanations":["int.\u5582;\u54c8\u7f57","n.\u5f15\u4eba\u6ce8\u610f\u7684\u547c\u58f0","v.\u5411\u4eba\u547c(\u5582)"],"synonym":["greetings","salutations"],"antonym":[],"wqx_example":[["say hello to","\u5411\u67d0\u4eba\u95ee\u5019,\u548c\u67d0\u4eba\u6253\u62db\u547c"],["Say hello to him for me . ","\u4ee3\u6211\u95ee\u5019\u4ed6\u3002"]],"entry":"hello","type":"word","related":[],"source":"wenquxing"}}
    
    
  2. 我们的请求体目前是固定的,意味着我们的单词只能是hello,请求体是一个JSON,我们需要用变量来代替单词,然后构建一个JSON,并设置请求体

    var data = strings.NewReader(`{"trans_type":"en2zh","source":"hello"}`)
    
  3. 该请求体有三个字段,我们可以构建一个结构体(需要设置结构体标签,如 json:"trans_type"),使用变量来代替单词然后组装结构体,然后通过json.Marshal将结构体序列化成JSON,因为json.Marshal返回的是一个字节数组,因此我们需要使用bytes.NewReader来读取字节数组并返回Reader

    // 结构体
    type DictRequest struct {
    	TransType string `json:"trans_type"` 
    	Source    string `json:"source"`
    }
    
    // 请求体
    client := &http.Client{}
    	word := "snorkeling"
    	request := DictRequest{TransType: "en2zh", Source: word}
    	buf, err := json.Marshal(request)
    	if err != nil {
    		log.Fatal(err)
    	}
    	var data = bytes.NewReader(buf)
    
  4. 再次运行项目,我们发现已经可以根据调整word的值来返回不同的结果

2.3 解析JSON

下面我们就要将返回的响应体进行解析,常用的解析做法是写一个结构体,然后一一对应响应体里面的字段,然后使用json.Unmarshal将响应体反序列化到结构体。

但由于我们的响应体非常有很多字段,手动写效率低且容易出错,我们因此常使用代码生成工具进行JSON数据的解析。

  1. 进入JSON转Golang Struct - 在线工具 - OKTools,并粘贴我们的JSON,如果不需要对转换的结果进行操作,则可以选择转换嵌套,让代码更加的紧凑

    {"rc":0,"wiki":{},"dictionary":{"prons":{"en-us":"[\u02c8sn\u0254rkl]","en":"[\u02c8sn\u0254\u02d0kl]"},"explanations":["n.(\u6f5c\u6c34\u8247\u7684)\u6c34\u4e0b\u901a\u6c14\u7ba1;(\u6f5c\u6e38\u8005\u4f7f\u7528\u7684)\u6c34\u4e0b\u547c\u5438\u7ba1"],"synonym":[],"antonym":[],"wqx_example":[],"entry":"snorkel","type":"word","related":[],"source":"wenquxing"}}
    
    

    ![Screen Shot 2023-07-27 at 4.19.00 PM](GO语言实战案例.assets/Screen Shot 2023-07-27 at 4.19.00 PM.png)

    转换后的结构体

    type AutoGenerated 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 []interface{} `json:"synonym"`
    		Antonym []interface{} `json:"antonym"`
    		WqxExample []interface{} `json:"wqx_example"`
    		Entry string `json:"entry"`
    		Type string `json:"type"`
    		Related []interface{} `json:"related"`
    		Source string `json:"source"`
    	} `json:"dictionary"`
    }
    
  2. 将转换后的结构体粘贴到IDE中,并修改名称为DictResponse

    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      []interface{} `json:"synonym"`
    		Antonym      []interface{} `json:"antonym"`
    		WqxExample   []interface{} `json:"wqx_example"`
    		Entry        string        `json:"entry"`
    		Type         string        `json:"type"`
    		Related      []interface{} `json:"related"`
    		Source       string        `json:"source"`
    	} `json:"dictionary"`
    }
    
  3. 使用json.Unmarshal将响应体反序列化到结构体DictResponse

    var dictResponse DictResponse
    err = json.Unmarshal(bodyText, &dictResponse)
    if err != nil {
       log.Fatal(err)
    }
    
    fmt.Printf("%#v\n", dictResponse)
    
  4. 下面我们就要DictResponse中需要的字段然后打印出来。我们需要的字段是Dictionary.Explanations,它是一个数组,里面包含单词的中文解释,使用for range取出并输出。还需要Dictionary.prons,他是一个结构体,里面包含英式和美式的音标,将它们取出后并进行输出。

    fmt.Println(word)
    	fmt.Println("彩云小译", "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
    	for _, item := range dictResponse.Dictionary.Explanations {
    		fmt.Println(item)
    	}
    
  5. 我们再次运行项目,发现已经成功打印了我们想要的结果

    snorkeling
    彩云小译 UK: [ˈsnɔːkl] US: [ˈsnɔrkl]
    n.(潜水艇的)水下通气管;(潜游者使用的)水下呼吸管
    
    

2.4 获取命令行参数

项目要求我们通过命令行参数来指定单词,因此我们要获取命令行参数,并将结果赋值为变量word

func main() {
	if len(os.Args) != 2 {
		fmt.Fprintf(os.Stderr, `usage: simpleDict WORD\nexample: simpleDict hello`)
		os.Exit(1)
	}
	word := os.Args[1]
	client := &http.Client{}
...
}

在控制台输入go run xxx.go hello,xxx.go为你当前的go文件,即可看到结果

2.5 代码整合

我们发现main函数太臃肿了,它应该只承担数据的输入和输出,因此我们抽象出函数query(),将处理部分放入到query函数中

完整代码示例

package main

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

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      []interface{} `json:"synonym"`
		Antonym      []interface{} `json:"antonym"`
		WqxExample   []interface{} `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("authority", "api.interpreter.caiyunai.com")
	req.Header.Set("accept", "application/json, text/plain, */*")
	req.Header.Set("accept-language", "zh,en;q=0.9")
	req.Header.Set("app-name", "xy")
	req.Header.Set("content-type", "application/json;charset=UTF-8")
	req.Header.Set("device-id", "02374c968dc8ea87ec9f04d6890e8bd7")
	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="114", "Microsoft Edge";v="114"`)
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("sec-ch-ua-platform", `"macOS"`)
	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 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67")
	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)
	var dictResponse DictResponse
	err = json.Unmarshal(bodyText, &dictResponse)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(word)
	fmt.Println("彩云小译", "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\nexample: simpleDict hello`)
		os.Exit(1)
	}
	word := os.Args[1]
	query(word)
}

在控制台输入go run xxx.go hello,即可看到结果

spaceqi@192 simpleDict % go run main.go hello
hello
彩云小译 UK: [ˈheˈləu] US: [həˈlo]
int.喂;哈罗
n.引人注意的呼声
v.向人呼(喂)

2.6 增加新的翻译引擎

  1. 使用有道翻译引擎产品文档-自然语言翻译服务 (youdao.com),用户需要先注册有道智云,然后开通文本翻译服务。

  2. download.ydstatic.com/ead/transla…

  3. 运行项目,将控制台输出的响应体复制出来,然后生成响应体结构体,然后将响应体使用反序列化到结构体上。

  4. 使用并行处理两个翻译引擎

  5. 完整代码

    package main
    
    import (
    	"bytes"
    	"crypto/rand"
    	"crypto/sha256"
    	"encoding/hex"
    	"encoding/json"
    	"fmt"
    	"io"
    	"log"
    	"net/http"
    	neturl "net/url"
    	"os"
    	"strconv"
    	"strings"
    	"time"
    )
    
    type DictRequestCAIYUN struct {
    	TransType string `json:"trans_type"`
    	Source    string `json:"source"`
    }
    type DictResponseCAIYUN 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      []interface{} `json:"synonym"`
    		Antonym      []interface{} `json:"antonym"`
    		WqxExample   []interface{} `json:"wqx_example"`
    		Entry        string        `json:"entry"`
    		Type         string        `json:"type"`
    		Related      []interface{} `json:"related"`
    		Source       string        `json:"source"`
    	} `json:"dictionary"`
    }
    
    func queryCAIYUN(word string) {
    	client := &http.Client{}
    	request := DictRequestCAIYUN{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("authority", "api.interpreter.caiyunai.com")
    	req.Header.Set("accept", "application/json, text/plain, */*")
    	req.Header.Set("accept-language", "zh,en;q=0.9")
    	req.Header.Set("app-name", "xy")
    	req.Header.Set("content-type", "application/json;charset=UTF-8")
    	req.Header.Set("device-id", "02374c968dc8ea87ec9f04d6890e8bd7")
    	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="114", "Microsoft Edge";v="114"`)
    	req.Header.Set("sec-ch-ua-mobile", "?0")
    	req.Header.Set("sec-ch-ua-platform", `"macOS"`)
    	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 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67")
    	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)
    	var dictResponse DictResponseCAIYUN
    	err = json.Unmarshal(bodyText, &dictResponse)
    	if err != nil {
    		log.Fatal(err)
    	}
    	fmt.Println(word)
    	fmt.Println("彩云小译", "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
    	for _, item := range dictResponse.Dictionary.Explanations {
    		fmt.Println(item)
    	}
    }
    
    type DictYOUDAO struct {
    	ReturnPhrase []string `json:"returnPhrase"`
    	Query        string   `json:"query"`
    	ErrorCode    string   `json:"errorCode"`
    	L            string   `json:"l"`
    	TSpeakURL    string   `json:"tSpeakUrl"`
    	Web          []struct {
    		Value []string `json:"value"`
    		Key   string   `json:"key"`
    	} `json:"web"`
    	Translation   []string `json:"translation"`
    	MTerminalDict struct {
    		URL string `json:"url"`
    	} `json:"mTerminalDict"`
    	Dict struct {
    		URL string `json:"url"`
    	} `json:"dict"`
    	Weict struct {
    		URL string `json:"url"`
    	} `json:"weict"`
    	Basic struct {
    		ExamType   []string `json:"exam_type"`
    		UsPhonetic string   `json:"us-phonetic"`
    		Phonetic   string   `json:"phonetic"`
    		UkSpeech   string   `json:"uk-speech"`
    		Explains   []string `json:"explains"`
    	} `json:"basic"`
    	IsWord   bool   `json:"isWord"`
    	SpeakURL string `json:"speakUrl"`
    }
    
    func AddAuthParams(appKey string, appSecret string, params map[string][]string) {
    	qs := params["q"]
    	if qs == nil {
    		qs = params["img"]
    	}
    	var q string
    	for i := range qs {
    		q += qs[i]
    	}
    	salt := getUuid()
    	curtime := strconv.FormatInt(time.Now().Unix(), 10)
    	sign := CalculateSign(appKey, appSecret, q, salt, curtime)
    	params["appKey"] = []string{appKey}
    	params["salt"] = []string{salt}
    	params["curtime"] = []string{curtime}
    	params["signType"] = []string{"v3"}
    	params["sign"] = []string{sign}
    }
    func CalculateSign(appKey string, appSecret string, q string, salt string, curtime string) string {
    	strSrc := appKey + getInput(q) + salt + curtime + appSecret
    	return encrypt(strSrc)
    }
    
    func encrypt(strSrc string) string {
    	bt := []byte(strSrc)
    	bts := sha256.Sum256(bt)
    	return hex.EncodeToString(bts[:])
    }
    
    func getInput(q string) string {
    	str := []rune(q)
    	strLen := len(str)
    	if strLen <= 20 {
    		return q
    	} else {
    		return string(str[:10]) + strconv.Itoa(strLen) + string(str[strLen-10:])
    	}
    }
    
    func getUuid() string {
    	b := make([]byte, 16)
    	_, err := io.ReadFull(rand.Reader, b)
    	if err != nil {
    		return ""
    	}
    	b[6] = (b[6] & 0x0f) | 0x40
    	b[8] = (b[8] & 0x3f) | 0x80
    	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
    }
    
    func createRequestParams(word string) map[string][]string {
    	from := "en"
    	to := "zh-CHS"
    
    	return map[string][]string{
    		"q":    {word},
    		"from": {from},
    		"to":   {to},
    	}
    }
    
    func queryYOUDAO(word string) {
    	// 您的应用ID
    	appKey := "您的应用ID"
    	// 您的应用密钥
    	appSecret := "您的应用密钥"
    
    	// 添加请求参数
    	paramsMap := createRequestParams(word)
    	header := map[string][]string{
    		"Content-Type": {"application/x-www-form-urlencoded"},
    	}
    	// 添加鉴权相关参数
    	AddAuthParams(appKey, appSecret, paramsMap)
    	// 请求api服务
    	result := DoPost("https://openapi.youdao.com/api", header, paramsMap, "application/json")
    	//	将返回结果反序列化到结构体
    	var dictYOUDAO DictYOUDAO
    	json.Unmarshal(result, &dictYOUDAO)
    
    	fmt.Println(word)
    	fmt.Println("有道翻译", "UK:", dictYOUDAO.Basic.Phonetic, "US:", dictYOUDAO.Basic.UsPhonetic)
    	for _, item := range dictYOUDAO.Basic.Explains {
    		fmt.Println(item)
    	}
    
    }
    
    func DoPost(url string, header map[string][]string, bodyMap map[string][]string, expectContentType string) []byte {
    	client := &http.Client{
    		Timeout: time.Second * 3,
    	}
    	params := neturl.Values{}
    	for k, v := range bodyMap {
    		for pv := range v {
    			params.Add(k, v[pv])
    		}
    	}
    	req, _ := http.NewRequest("POST", url, strings.NewReader(params.Encode()))
    	for k, v := range header {
    		for hv := range v {
    			req.Header.Add(k, v[hv])
    		}
    	}
    	res, err := client.Do(req)
    	if err != nil {
    		fmt.Print("request failed:", err)
    		return nil
    	}
    	defer res.Body.Close()
    	body, _ := io.ReadAll(res.Body)
    	contentType := res.Header.Get("Content-Type")
    	if !strings.Contains(contentType, expectContentType) {
    		print(string(body))
    		return nil
    	}
    	return body
    
    }
    
    func main() {
    	if len(os.Args) != 2 {
    		fmt.Fprintf(os.Stderr, `usage: simpleDict WORD\nexample: simpleDict hello`)
    		os.Exit(1)
    	}
    	word := os.Args[1]
    
    	var wg sync.WaitGroup
    	wg.Add(2) // 等待2个goroutine
    	go func() {
    		defer wg.Done()
    		queryCAIYUN(word)
    	}()
    	go func() {
    		defer wg.Done()
    		queryYOUDAO(word)
    	}()
    	wg.Wait() // 等待goroutine结束
    }
    

    运行结果

    spaceqi@192 simpleDictHomeWork % go run main.go snorkeling
    snorkeling
    彩云小译 UK: [ˈsnɔːkl] US: [ˈsnɔrkl]
    n.(潜水艇的)水下通气管;(潜游者使用的)水下呼吸管
    snorkeling
    有道翻译 UK: ˈsnɔ:klɪŋ US: ˈsnɔːrkəlɪŋ
    n. 浮潜;潜水;浅滩潜水
    v. 用水下通气管潜航;用通气管潜泳(snorkel 的 ing 形式)
    adj. 潜浮的