Go 语言入门实战:猜迷游戏&在线词典 | 青训营

173 阅读5分钟

【tips:对照案例最后的完整代码一起阅读】

1. 猜数游戏

1.1. 需求概述

程序生成一个在1到100之间的随机整数,然后提示用户进行猜测。用户每输入一个数字,程序会提示用户猜测的数字高于还是低于秘密随机数,并让用户再次猜测,直到猜中为止,提示并退出程序。

1.2. 生成随机数

  1. 使用rand.Inti()生成随机数

Go 的 math/rand 包提供了伪随机数生成器

Inti():返回一个非负的伪随机数,值的范围在[0,n)内。如果n<=0,会出错。

rand.Intn()函数是个伪随机函数,不管运行多少次都会返回同样的随机数,因为它默认的种子值就是确定的。

  1. 使用时间戳初始化随机数种子

rand.Seed()用于设置种子值

time.Now().UnixNano()获取时间戳(纳秒)

使用时间戳,种子值就会随时间而变化,从而每次都会生成不同的随机数。

1.3. 读取用户输入

1.3.1. 获取键盘输入

从键盘和标准输入os.Stdin读取输入,最简单的办法是使用fmt包提供的Scan和Sscan开头的函数。

示例1:

package main

import "fmt"

func main() {
    /*Scan开头函数示例*/
    var name string
    fmt.Println("Please Enter Your Name:")
    fmt.Scanln(&name)
    //fmt.Scanf("%s", &name) 这种用法和C语言类似
    fmt.Println(name)

    /*Sscan开头函数示例:*/
    var i int
    var f float32
    var input = "52 / 66.2" //定义要读取的内容
    var fomat = "%d / %f"   //定义读取的格式
    fmt.Sscanf(input, fomat, &i, &f)
    fmt.Println(i, f)	//输出:52 66.2
}

Scanln扫描来自标准输入的文本,将空格分隔的值依次存放到后续的参数内,直到碰到换行。Scanf与其类似,除了Scanf的第一个参数用作格式字符串,用来决定如何读取。

Sscan和以Sscan开头的函数则是从字符串读取,除此之外,与Scanf相同。如果这些函数读取到的结果与预想的不同,可以检查成功读入数据的个数和返回的错误。

也可以使用bufio包提供的缓冲读取(buffered reader)来读取数据

示例2:

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	inputReader := bufio.NewReader(os.Stdin)
	fmt.Println("Please input:")
	input, err := inputReader.ReadString('\n')
	if err == nil {
		fmt.Println("The input is: " + input)
	}
}

inputReader是一个指向bufio.Reader的指针。inputReader := bufio.NewReader(os.Stdin)这行代码,会创建一个读取器,并将其与标准输入绑定。

函数返回一个新的带缓冲的io.Reader对象,它将从指定读取器(例如os.Stdin)读取内容。

返回的读取器对象提供一个方法ReadString(delim byte),该方法从输入中读取内容,直到碰到指定的分隔符,然后将读取到的内容连同分隔符一起放到缓冲区。

ReadString返回读取到的字符串,如果碰到错误则返回nil。如果它一直读到文件结束,则返回读取到的字符串和io.EOF。如果读取过程中没有碰到分隔符,将返回错误err!=nil

在上面的例子中,会读取键盘输入,直到回车键(\n)被按下。

1.3.2. 去除回车换行符

该实战案例使用上述示例2的方式获取键盘输入的结果,结果中包含结尾的回车换行符(\r\n)要使用strings.TrimSuffix()把回车换行符去掉。

strings.TrimSuffix(s, suffix string):返回没有提供的尾随后缀字符串的s。如果s不以suffix结尾,则s原样返回

1.3.3. 字符串转换为数字

随机数为int类型,而读取到用户输入为字符串,所以要使用strconv.Atoi()转换(等效于ParseInt(str string,base int,bitSize int))。该函数有两个返回值,第一个返回值是转换成int的值,第二个返回值判断是否转换成功。转换失败则将失败提示打印出来。

tips:这一部分可以使用示例1中的方法简化代码实现

1.4. 实现判断逻辑

比较用户输入的数和随机生成的秘密值,输入的值大于、等于或小于秘密值,输出对应的提示。这部分用if-else就能实现。

1.5. 实现游戏循环

此时程序可以正常工作了,但是用户只能输入一次猜测,无论猜测是否正确,程序都会退出。为了让游戏可以正常玩下去,需要加一个循环。把刚刚的代码移到一个for循环里面,再把return改成continue以便于在猜错时游戏能够继续循环。在用户输入正确时break,从而在用户胜利的时候退出游戏。

1.6. 完整实现

package main

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

func main() {
	//1. 生成随机数
	maxNum := 100
	rand.Seed(time.Now().UnixNano())  //使用时间戳初始化种子数
	secretNumber := rand.Intn(maxNum) //生成随机数
	//fmt.Println(secretNumber)

	//2. 读取用户输入
	fmt.Println("Please input your guess")
	reader := bufio.NewReader(os.Stdin) //读取数据
	for {
		input, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("An error occured while reading input.Please try again", err)
			continue
		}
		input = strings.TrimSuffix(input, "\r\n") //去除回车换行符

		guess, err := strconv.Atoi(input) //将输入转换为数字
		if err != nil {
			fmt.Println("Invalid input.Please enter an integer value")
			continue
		}
		fmt.Println("You guess is", guess)

		//3. 判断逻辑
		if guess > secretNumber {
			fmt.Println("You guess is bigger than the secret number.Please try again.")
		} else if guess < secretNumber {
			fmt.Println("You guess is smaller than the secret number.Please try again.")
		} else {
			fmt.Println("You Win!")
			break
		}
	}
}

2. 在线词典

2.1. 需求概述

用户在命令行里面查询一个单词,通过调用第三方API查询到的单词翻译并打印出来。

在本案例中,会学习如何用go语言发送HTTP请求、解析JSON以及如何使用代码生成提高开发效率。

2.2. 抓包

使用到的API是彩云小译(fanyi.caiyunapp.com/#/

进入网页,打开开发人员工具。

点击翻译按钮,会发现浏览器发送了一系列请求。

这是一个HTTP的post的请求。请求头是一个json,里面有两个字段,一个代表是从什么语言转化成什么语言,source就是你要查询的单词。API的返回结果里面会有Wiki和dictionary两个字段。我们需要用的结果主要在dictionary.Explanations字段里面。其他字段里面还包括音标等信息。

2.3. 代码生成

我们需要在Golang里面发送这个请求。因为这个请求比较复杂,用代码构造比较麻烦,所以可以用浏览器中的“复制为cURL(bash)”。

复制完成后粘贴出来,可以成功返回一串JSON。

打开该网站(curlconverter.com/go/)粘贴上面复制的cURL,选择Go语言,就能生成代码。

接着来看一下生成的代码,首先创建了一个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打印出来。

运行代码,可以看到成功发送出请求并把返回的JSON打印出来。但此时的输入是固定的,我们需要一个变量来输入,这就要用到JSON序列化。

2.4. 生成request body

生成一段JSON,常用方法是构造一个结构体,该结构体与要生成的JSON一一对应。

在这个案例中,构造的结构体包含三个字段,TransType、Source、UserID。

然后定义一个变量,初始化每个结构体成员,再调用JSON.Marshal得到序列化之后的字符串。

不同于上一步抓包得到是字符串,这里得到的是一个字节数组。所以要把strings.newReader改成bytes.newReader来构造request body。剩下的代码不变,这样就能成功发送HTTP请求。

2.5. 解析response body

和request的一样,写一个结构体,把返回的JSON反序列化到结构体中。但这个API返回的结构非常复杂,如果一一定义结构体字段,非常繁琐。那么可以选择对应的代码生成工具(oktools.net/json2go),把JSON字符串粘贴进去,生成对应的结构体。如果不需要对返回的结果进行很多精细操作,可以选择“转换-嵌套”,让生成的代码更加紧凑。

接着修改代码,先定义一个response结构体对象,然后用JSON.unmarshal反序列化到结构体中,再打印出来。

var dictResponse DictResponse
	err = json.Unmarshal(bodyText, &dictResponse)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%#v\n", dictResponse)

运行结果:

2.6. 打印结果

下面我们要把代码修改为打印指定字段。

观察打印出的JSON可以看出我们需要的结果在Dictionary.explanations。用for range循环遍历它,然后直接打印需要的内容。

运行结果:

2.7. 完善代码

主要功能已经完成,但程序的输入还是写死的。把代码主体改成query函数,查询的单词作为参数传递进来。

  1. 使用命令运行:

写一个main函数,这个main函数首先判断命令和参数个数,如果不是两个,那就打印错误信息,退出程序。反之获取到用户输入的单词,执行query函数。

if len(os.Args) != 2 {
		fmt.Println(os.Stderr, `usage: simpleDict WORD example: simpleDict hello`)
		os.Exit(1)
	}
	word := os.Args[1]
	query(word)

  1. 在GoLand开发工具运行:

将main函数改成猜数游戏案例中的读取用户输入即可。

fmt.Printf("请输入要查询的单词:")
	var word string
	fmt.Scanln(&word)
	query(word)

2.8. 完整实现

package main

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

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 {
	} `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"}`)

	//生成request body
	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-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", "6597226ceac6ce668c385c117013a951")
	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="115", "Chromium";v="115"`)
	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/115.0.0.0 Safari/537.36 Edg/115.0.1901.200")
	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)
	}
	//fmt.Printf("%s\n", bodyText)

	//解析response body
	var dictResponse DictResponse
	err = json.Unmarshal(bodyText, &dictResponse)
	if err != nil {
		log.Fatal(err)
	}
	//fmt.Printf("%#v\n", dictResponse)
	
	//输出结果
	fmt.Println(word, "\n英音", dictResponse.Dictionary.Prons.En, "美音", dictResponse.Dictionary.Prons.EnUs)
	for _, item := range dictResponse.Dictionary.Explanations {
		fmt.Println(item)
	}
}
func main() {
	fmt.Printf("请输入要查询的单词:")
	var word string
	fmt.Scanln(&word)
	query(word)
}

如有错漏之处,敬请批评指正!