序言
这是一篇初学者的实践记录,问题详见“Go语言的实战案例”的实践内容。 本次任务要求如下:
1.在给出的彩云小译之外,添加对另外一种翻译引擎的支持
2.在1的条件下,并行请求两个引擎的翻译结果
本文实现了Go语言请求API翻译并返回结果的过程,并通过协程并行请求两个翻译结果。
有道翻译实现
访问新手指南 获得一个翻译API。翻译API包含两部分:应用ID以及应用密钥。 阅读有道翻译API 文档,得知我们需要构造的请求如下:
字段名 | 类型 | 含义 | 必填 | 备注 |
---|---|---|---|---|
q | text | 待翻译文本 | True | 必须是UTF-8编码 |
from | text | 源语言 | True | 参考下方支持语言(可设置为auto) |
to | text | 目标语言 | True | 参考下方支持语言 |
appKey | text | 应用ID | True | 可在应用管理查看 |
salt | text | 随机字符串 | True | uuid(可使用uuid生成) |
sign | text | 签名 | True | sha256(应用ID+input+salt+curtime+应用密钥) |
signType | text | 签名类型 | True | v3 |
curtime | text | 当前UTC时间戳 | True | TimeStamp |
ext | text | 翻译结果音频格式 | False | 支持mp3 |
作为一个简单的文本翻译,我们不关心非必填项,因此,构造如下结构体:
type YoudaoTranslateRequest struct {
Q string `json:"q"`
From string `json:"from"`
To string `json:"to"`
AppKey string `json:"appKey"`
Salt string `json:"salt"`
Sign string `json:"sign"`
SignType string `json:"signType"`
Curtime string `json:"curtime"`
}
同样地,我们阅读返回结果如下:
字段名 | 类型 | 含义 | 备注 |
---|---|---|---|
errorCode | text | 错误返回码 | 一定存在 |
query | text | 源语言 | 查询正确时,一定存在 |
translation | Array | 翻译结果 | 查询正确时,一定存在 |
l | text | 源语言和目标语言 | 一定存在 |
dict | text | 词典deeplink | 查询语种为支持语言时,存在 |
webdict | text | webdeeplink | 查询语种为支持语言时,存在 |
tSpeakUrl | text | 翻译结果发音地址 | 翻译成功一定存在,需要应用绑定语音合成服务才能正常播放,否则返回110错误码 |
speakUrl | text | 源语言发音地址 | 翻译成功一定存在,需要应用绑定语音合成服务才能正常播放,否则返回110错误码 |
务必注意一定要阅读文档中示例给出的对“good”的翻译请求结果。我们在其中注意到dict和webdict实际上是一个结构体而非text。编写代码时特别注意,否则会报告json: cannot unmarshal object into Go struct field这个错误。
// 定义响应结构体
type YoudaoTranslateResponse struct {
ErrorCode string `json:"errorCode"`
Query string `json:"query"`
Translation []string `json:"translation"`
L string `json:"l,omitempty"`
Dict *struct {
URL string `json:"url"`
} `json:"dict,omitempty"`
Webdict *struct {
URL string `json:"url"`
} `json:"webdict,omitempty"`
TSpeakUrl *string `json:"tSpeakUrl,omitempty"`
SpeakUrl *string `json:"speakUrl,omitempty"`
}
现在,我们开始编写 youdaoTranslate翻译函数。按照文档要求,首先传入我们的API,然后构造请求结构体中的所有内容:
首先准备应用ID和密钥,加的盐(salt)是随机字符串,官方推荐uuid实现。curtime则请求一个当前时间戳,防止重放攻击
func youdaoTranslate(text, from, to string) (string, error) {
const appKey = "xxx"
const appSecret = "xxx"
salt := uuid.New().String()
curtime := strconv.FormatInt(time.Now().Unix(), 10)
随后我们计算输入字符串的 input,文档已经给出计算方法,直接实现即可
var input string
if len(text) > 20 {
input = text[:10] + strconv.Itoa(len(text)) + text[len(text)-10:]
} else {
input = text
}
最后计算签名 sign,同样参考文档计算方法:
signStr := appKey + input + salt + curtime + appSecret
hash := sha256.New()
hash.Write([]byte(signStr))
sign := hex.EncodeToString(hash.Sum(nil))
构造请求结构体并编码URL:
reqData := YoudaoTranslateRequest{
//......
}
params := url.Values{}
params.Set("q", reqData.Q)
//......
随后,发送请求,接受请求,转换为json格式,检查错误码。
// 发送 POST 请求
resp, err := http.PostForm("https://openapi.youdao.com/api", params)
if err != nil {
return "", fmt.Errorf("Send Request Error: %v", err)
}
defer resp.Body.Close()
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("Read Response Error: %v", err)
}
// 解析 JSON 响应到响应结构体
var result YoudaoTranslateResponse
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("Parse JSON Response Error: %v", err)
}
// 检查错误码并返回结果
if result.ErrorCode != "0" {
return "", fmt.Errorf("API Error: %s", result.ErrorCode)
}
if len(result.Translation) == 0 {
return "", fmt.Errorf("No Translation Found: %s", text)
}
return result.Translation[0], nil
错误捕捉:我们在不同的阶段定义不同的错误,如Send Request Error等,这部分手动给出的错误标识了错误发生在哪个阶段,随后跟随其他来源(如API)给出的错误。
并行请求两个来源的翻译
这部分就是对协程的应用。我们首先修改query()函数,让它返回结果字符串而不是直接打印:
var result strings.Builder
// 单词和发音
result.WriteString(fmt.Sprintf("%s\nUK: %s\nUS: %s\n", word, dictResponse.Dictionary.Prons.En, dictResponse.Dictionary.Prons.EnUs))
// 释义
for _, item := range dictResponse.Dictionary.Explanations {
result.WriteString(fmt.Sprintf("%s\n", item))
}
return result.String(),nil
由于需要多次拼接字符串,因此,使用strings.Builder更加高效。因为我们在这里已经知道要多次拼接字符串,而直接使用+会带来多次拷贝开销。
随后,在一个新的函数中启动两个协程,并行执行 query() 和 youdaoTranslate() 函数。
至此,任务完成。