Go实践案例-猜数游戏、在线词典 | 青训营笔记

143 阅读9分钟

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

案例一——猜数游戏

游戏说明

程序首先会生成个介于1到100之间的随机整数,然后提示玩家进行猜测。玩家每次输入一个数字,程序会告诉玩家这个清测的值是高于还是低于那个秘密的随机数,并且让玩家继续猜测

如果玩家猜对了,告诉玩家取得胜利,游戏结束

游戏构建

随机数生成

v1

package main
​
import (
   "fmt"
   "math/rand"
)
​
func main() {
   MaxNum := 100
   //生成随机数
   secretNum := rand.Intn(MaxNum)
   fmt.Println("随机数为:", secretNum)
}

运行过后可以发现,每次的结果都是相同的

v2

这是因为rand.Intn函数在使用之前要设置随机数种子,否则每一次都会生成相同的数字

一般的惯例是在使用之前,用启动的时间戳初始化随机数种子

读取用户输入输出

然后接下来我们需要实现用户输入输出,并理解析成数字。

  • bufio读取Stdin文件实现读取输入

    每个程序执行的时候都会打开几个文件,stdin stdout stderr等,stdin 文件可以用 os.Stdin 来得到。

    然后直接操作这个文件很不方便,我们会用 bufio.NewReader 把一个文件转换成一个 reader 变量。

    reader 变量上会有很多用来操作一个流的操作,我们可以用它的 ReadString 方法来读取一行。

    如果失败了的话,我们会打印错误并能退出。

  • Scanf函数实现读取输入

ReadString 返回的结果包含结尾的换行符,我们把它去掉,再转换成数字。如果转换失败,我们同样打印错误,退出。

package main
​
import (
   "bufio"
   "fmt"
   "math/rand"
   "os"
   "strconv"
   "strings"
   "time"
)
​
func main() {
   //生成随机数
   MaxNum := 100
   rand.Seed(time.Now().UnixNano())
   secretNum := rand.Intn(MaxNum)
   fmt.Println("随机数为:", secretNum)
​
   //获取输入输出
   fmt.Println("Please input your guess:")
   //读入数据
   reader := bufio.NewReader(os.Stdin)   //把文件转换为只读的流
   input, err := reader.ReadString('\n') //读取流中的字符串
   if err != nil {
      fmt.Println("An error occured while reading input,Please try again!", err)
      return
   }
   //去掉多余的换行符(亲测还有一个\r)
   input = strings.TrimSuffix(input, "\r\n")
​
   // 转换为数字
   guess, err := strconv.Atoi(input)
   if err != nil {
      fmt.Println("输入不合法,请输入一个整数值", err)
      return
   }
   fmt.Println("你猜的数字为", guess)
}

运行结果略

实现判断逻辑

太简单了,直接上代码

package main
​
import (
   "bufio"
   "fmt"
   "math/rand"
   "os"
   "strconv"
   "strings"
   "time"
)
​
func main() {
   //生成随机数
   MaxNum := 100
   rand.Seed(time.Now().UnixNano())
   secretNum := rand.Intn(MaxNum)
   fmt.Println("随机数为:", secretNum)
​
   //获取输入输出
   fmt.Println("Please input your guess:")
   //读入数据
   reader := bufio.NewReader(os.Stdin)   //把文件转换为只读的流
   input, err := reader.ReadString('\n') //读取流中的字符串
   if err != nil {
      fmt.Println("An error occured while reading input,Please try again!", err)
      return
   }
   //去掉多余的换行符(亲测还有一个\r)
   input = strings.TrimSuffix(input, "\r\n")
​
   // 转换为数字
   guess, err := strconv.Atoi(input)
   if err != nil {
      fmt.Println("输入不合法,请输入一个整数值", err)
      return
   }
   fmt.Println("你猜的数字为", guess)
​
   // 判断输入值大小
   if guess > secretNum {
      fmt.Println("Your guess is bigger than secret number.Please try again.")
   } else if guess < secretNum {
      fmt.Println("Your guess is smaller than secret number.Please try again.")
   } else {
      fmt.Println("Correct!You Legend!")
   }
​
}

实现游戏循环

改写for循环,胜利break,出错continue

package main
​
import (
   "bufio"
   "fmt"
   "math/rand"
   "os"
   "strconv"
   "strings"
   "time"
)
​
func main() {
   //生成随机数
   MaxNum := 100
   rand.Seed(time.Now().UnixNano())
   secretNum := rand.Intn(MaxNum)
   //fmt.Println("随机数为:", secretNum)
​
   for {
      //获取输入输出
      fmt.Println("Please input your guess:")
      //读入数据
      reader := bufio.NewReader(os.Stdin)   //把文件转换为只读的流
      input, err := reader.ReadString('\n') //读取流中的字符串
      if err != nil {
         fmt.Println("An error occured while reading input,Please try again!", err)
         continue
      }
      //去掉多余的换行符(亲测还有一个\r)
      input = strings.TrimSuffix(input, "\r\n")
​
      // 转换为数字
      guess, err := strconv.Atoi(input)
      if err != nil {
         fmt.Println("输入不合法,请输入一个整数值", err)
         continue
      }
      fmt.Println("你猜的数字为", guess)
​
      // 判断输入值大小
      if guess > secretNum {
         fmt.Println("Your guess is bigger than secret number.Please try again.")
      } else if guess < secretNum {
         fmt.Println("Your guess is smaller than secret number.Please try again.")
      } else {
         fmt.Println("Correct!You Legend!")
         break
      }
   }
​
}

优化

使用fmt.Scanf简化代码

案例二——在线词典

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

抓包

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

image.png

找到Network里包含hello的dict数据包

预览如下:

image.png

请求数据:

{
    "trans_type": "en2zh",
    "source": "hello"
}
  • trans_type:翻译类型(en英语转zh中文)
  • source:翻译的词

响应数据:

{
  "prons": {
    "en-us": "[həˈlo]",
    "en": "[ˈheˈləu]"
  },
  "explanations": [
    "int.喂;哈罗",
    "n.引人注意的呼声",
    "v.向人呼(喂)"
  ],
  "synonym": [
    "greetings",
    "salutations"
  ],
  "antonym": [],
  "wqx_example": [
    [
      "say hello to",
      "向某人问候,和某人打招呼"
    ],
    [
      "Say hello to him for me . ",
      "代我问候他。"
    ]
  ],
  "entry": "hello",
  "type": "word",
  "related": [],
  "source": "wenquxing"
}

代码生成

使用Go去发送这个请求时很麻烦的,故采用代码生成的方法

首先右键请求、copy as curl

image.png

curl 'https://api.interpreter.caiyunai.com/v1/dict' \
  -H 'authority: api.interpreter.caiyunai.com' \
  -H 'accept: application/json, text/plain, */*' \
  -H 'accept-language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6' \
  -H 'app-name: xy' \
  -H 'content-type: application/json;charset=UTF-8' \
  -H 'device-id;' \
  -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="99", "Microsoft Edge";v="109", "Chromium";v="109"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'sec-fetch-dest: empty' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-site: cross-site' \
  -H '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 Edg/109.0.1518.55' \
  -H 'x-authorization: token:qgemv4jr1y38jyq6vhvi' \
  --data-raw '{"trans_type":"en2zh","source":"hello"}' \
  --compressed

打开另一个网址:Convert curl commands to Go (curlconverter.com)

image.png

package main
​
import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "strings"
)func main() {
    client := &http.Client{}
    //指定参数
    var data = strings.NewReader(`{"trans_type":"en2zh","source":"hello","user_id":"63c6bb0b5834410017188916"}`)
    //创建请求
    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,en-GB;q=0.7,en-US;q=0.6")
    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", "Microsoft Edge";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 Edg/109.0.1518.55")
    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)
}
​

我们来看一下这生成的代码,首先第 12 行我们创建了一个 HTTP client,创建的时候可以指定很多参数,包括比如请求的超时是否使用 cookie 等。

接下来是构造一个 HTTP 请求,这是一个 post 请求,然后会用到 HTTP .NewRequest ,第一个参数是 http 方法 POST, 第二个参数是 URL, 最后一个参数是 body ,body因为可能很大,为了支持流式发送,是一个只读流。

我们用了 strings.NewReader 来把字符串转换成一个流。这样我们就成功构造了一个 HTTP request ,接下来我们需要对这个 HTTP request 来设置一堆 header。 接下来我们把我们调用 client.do request ,就能得到 response 如果请求失败的话,那么这个 error 会返回非 nil,会打印错误并且退出进程。response 有它的 HTTP 状态码, response header和body。 body同样是一个流,在golang里面,为了避免资源泄露,你需要加一个 defer 来手动关闭这个流,这个 defer 会在这个函数运行结束之后去执行。接下来我们是用 ioutil.ReadAll 来读取这个流,能得到整个body。我们再用 print 打印出来。

构造Request Body并序列化JSON

  • 定义结构体
  • 创建结构体对象
  • 序列化
  • 利用bytes.NewBuffer(buf)将byte数组转换为字符串
package main
​
import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)type DictRequest struct {
    TransType string `json:"trans_type"`
    Source    string `json:"source"`
    UserID    string `json:"user_id"`
}
​
func main() {
    client := &http.Client{}
​
    //指定参数
    //var data = strings.NewReader(`{"trans_type":"en2zh","source":"hello","user_id":"63c6bb0b5834410017188916"}`)
    //创建结构体对象
    request := DictRequest{TransType: "en2zh", Source: "hello"}
    //序列化
    buf, err := json.Marshal(request)
    if err != nil {
        log.Fatal(err)
    }
    var data = bytes.NewBuffer(buf) //byte数组转字符串
    //创建请求
    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,en-GB;q=0.7,en-US;q=0.6")
    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", "Microsoft Edge";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 Edg/109.0.1518.55")
    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)
}
​

解析Response Body

Go中最常用的解析方法,是创建一个结构体,各字段与json中的字段对应,再将请求回来的Response Body反序列化到结构体对象

手动构造结构体容易出错,因此依然使用代码生成来实现

JSON转Golang Struct - 在线工具 - OKTools

image.png

点击展开会分成多个结构体

嵌套则会写在一个结构体中

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      []interface{} `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,也有可能是参数错了之类的错误

导致出现403、404…….

出错的时候应该检测到它不是200

应该把返回的状态码和报文信息打印出来方便诊断问题

完整代码

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      []interface{} `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{}
​
   //指定参数
   //var data = strings.NewReader(`{"trans_type":"en2zh","source":"hello","user_id":"63c6bb0b5834410017188916"}`)
   //创建结构体对象
   request := DictRequest{TransType: "en2zh", Source: word}
   //序列化
   buf, err := json.Marshal(request)
   if err != nil {
      log.Fatal(err)
   }
   var data = bytes.NewBuffer(buf) //byte数组转字符串
   //创建请求
   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,en-GB;q=0.7,en-US;q=0.6")
   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", "Microsoft Edge";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 Edg/109.0.1518.55")
   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)
   }
​
   //防御式编程
   if resp.StatusCode != 200 {
      log.Fatal("Bad StatusCode:", resp.StatusCode, "\nBody:", string(bodyText))
   }
​
   //fmt.Printf("%s\n", bodyText)
   //创建响应结构体变量
   var dictresponse DictResponse
   //json反序列化到结构体变量
   err = json.Unmarshal(bodyText, &dictresponse)
   if err != nil {
      log.Fatal(err)
   }
   //fmt.Printf("%#v\n", dictresponse)
   //只显示需要的字段
   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.Println(os.Stderr, `usage: simpleDict WORD example:simpleDict hello`)
      os.Exit(1)
   }
   word := os.Args[1]
   Query(word)
}

运行:

go run .\案例二.在线词典.go abandon

优化

  • 增加另一种翻译引擎的支持(Google Translate、火山翻译…..)
  • 并行请求两个翻译引擎提高响应速度