【tips:对照案例最后的完整代码一起阅读】
1. 猜数游戏
1.1. 需求概述
程序生成一个在1到100之间的随机整数,然后提示用户进行猜测。用户每输入一个数字,程序会提示用户猜测的数字高于还是低于秘密随机数,并让用户再次猜测,直到猜中为止,提示并退出程序。
1.2. 生成随机数
- 使用
rand.Inti()生成随机数
Go 的 math/rand 包提供了伪随机数生成器
Inti():返回一个非负的伪随机数,值的范围在[0,n)内。如果n<=0,会出错。
但rand.Intn()函数是个伪随机函数,不管运行多少次都会返回同样的随机数,因为它默认的种子值就是确定的。
- 使用时间戳初始化随机数种子
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函数,查询的单词作为参数传递进来。
- 使用命令运行:
写一个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)
- 在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)
}
如有错漏之处,敬请批评指正!