Go语言项目案例 | 青训营笔记

137 阅读8分钟

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

1 猜数字游戏

1.1 实现思路

由系统生成一个随机数,用户输入猜测的数字,程序将用户输入的数字与系统生成的数字相比较,猜测过大或者过小都会给予用户提示,直到用户猜测正确为止。

1.2 注意事项

上课时老师采用了系统标准库的读取标准输入流的方式进行读取用户输入,这里采用了fmt.Scanf()的语法进行读取。实现起来比较简单语法标准参考C语言。

1.3 代码实现

package main

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

func main() {
	maxNum := 100	//猜数字的范围是0 ~ 100
	rand.Seed(time.Now().Unix())//设置随机数种子
	secretNumber := rand.Intn(maxNum)//生成随机数
	fmt.Println(secretNumber)
	fmt.Println("Hello,Welcome to play Gussing Number Game!")
	fmt.Println("Please input your guess:")
	reader := ""
	fmt.Scanf(reader)//从标准输入读取一个数字
	guessNum := 1
	for
	{
                //上课时老师使用标准输入流的方式读入用户输入,这里采用了另一种fmt.Scanf()的方式读入,其语法类似于C语言。
		// input, err := reader.ReadString('\n')//读入标准输入的换行符
		// if err != nil {
		// 	fmt.Println("An error occured while reading input. Please try again!")
		// 	continue
		// }
		// input = strings.Trim(input, "\r\n")//去除标准输入的换行符
		fmt.Scanf("%s", &reader)//从标准输入读取一个数字
		guess, err := strconv.Atoi(reader)//将读入的字符串转换成整数
		if err != nil {
			fmt.Println("An error occured while reading input. Please try again!")
			continue
		}

		if guess > secretNumber {
			fmt.Println("Your guess is bigger than secret number, Please try again.")
			guessNum += 1
		}else if guess < secretNumber {
			fmt.Println("Your guess is smaller than secret number, Please try again.")
			guessNum += 1
		}else {
			fmt.Println("Correct, you Legend! Use times is", guessNum)
			break
		}

	}
}

2 命令行词典

2.1 实现思路

实现一个可以使用命令行参数来调用第三方词典并返回翻译的在线词典。具体实现思路为:

2.1.1 获取API

选择彩云小译

image.png 这样我们就可以得到所需的POST请求。

image.png image.png 当我们点击翻译的时候就会触发POST请求,请求参数是一个JSON,内部包含两个字段:其中source是我们填入的需要翻译的单词,trans_type表示我们需要从英文翻译成中文。其中resonse里面就是我们查询单词的各种释义,包括中文解释和音标等。 接下来我们需要使用Go去发送这个请求。

2.1.2 发送请求

由于这个请求十分复杂,所以我们选择使用代码生成工具来生成这个请求。 首先右键点击请求,然后找到复制按钮,选择复制时的命令行格式。 image.png 复制完成后,我们可以在对应的终端上粘贴所复制的curl命令,正常情况下会返回一大段的JSON。 接着我们打开代码生成网址,在网页的上方填入刚刚复制的命令,下方语言选择Go,即可生成对应代码。 image.png 将代码直接粘贴进IDE,由于几个Header比较复杂,生成代码可能会有一些由于转义导致的编译错误,我们直接把错误代码删除即可。删除后的代码是可以正确编译和运行的。 生成的代码中,首先创建一个http.Client,创建时可以指定参数timeout,判断是否超时。

client := &http.Client{}

接着创建一个httpPOST请求,创建请求时传入参数为请求格式,urldatadata表示一个流,并非一个字符串。我们正常情况下会输入一个字符串,所以我们会用到strings.NewReader输入的字符串转换成一个流。将参数设置成一个流,可以在发送请求的时候减小内存消耗,流式创建这个请求。

var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
if err != nil {
	log.Fatal(err)
}

接下来创建了一大堆请求。

req.Header.Set("Connection", "keep-alive")
req.Header.Set("DNT", "1")
req.Header.Set("os-version", "")
req.Header.Set("sec-ch-ua-mobile", "?0")
...

然后真正地发起请求,如果由于DNS解析失败或者断网等各种因素导致请求发送失败连接不上服务器,那么将会生成错误信息,直接退出进程。

resp, err := client.Do(req)
if err != nil {
	log.Fatal(err)
}

前面代码没有错误的话,我们就得到了response响应。按照golang的编程习惯,由于返回的resp.Body同样是一个流,为了避免资源泄漏,需要手动关闭这个流,defer语句会在函数结束是自动地从下往上触发

defer resp.Body.Close()

将得到的resp.Body流读入到内存里,使用ioutil.ReadALL将其转换成一个字符串,使用%s将其打印出来,得到请求得到的响应JSON字符串。

bodyText, err := ioutil.ReadAll(resp.Body)
if err != nil {
	log.Fatal(err)
}
fmt.Printf("%s\n", bodyText)

2.1.3 JSON序列化

我们刚刚完成了发送请求,得到了响应的JSON,但是现在存在一个问题即,程序的输入是固定的,我们最终肯定要从一个变量中得到输入单词的值而不是从一个JSON的字符串输入。接下来就需要JSON序列化。

根据之前的知识,要序列化一个JSON,我们需要构建一个与JSON内部结构一一对应的结构体,接着直接调用JSON.Marshal即可。

//这里使用了三个字段,但其实只用到了两个。
type DictRequest struct {
	TransType string `json:"trans_type"`
	Source    string `json:"source"`
	UserID    string `json:"user_id"`
}

定义结构体以后,我们首先创建一个结构体变量,初始化其内部字段名。

client := &http.Client{}
request := DictRequest{TransType: "en2zh", Source: "hello"}

接着调用JSON.Marshal方法将定义的结构体变量request序列化成一个byte数组,然后将byte数组转化到data里。注意这里区别的地方是之前的data使用的是strings.NewReader()方法创建的一个流,而json.Marshal方法得到的是一个byte数组而非一个字符串。所以我们要采用bytes.NewReader()将字符串转化成流。

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)
}

这一步完成后我们代码的执行结果应该是完全不变的。

2.1.4 response的反序列化

在实现了请求的序列化后,接下来我们要完成返回的response的反序列化。需要将之前得到的巨大的response解析出来,然后得到其中某些关键字段的值,例如释义和音标。将其输入到屏幕上。

image.pngjs或者python这些脚本语言中,这个response.body返回的将是一个map或者字典,可以通过.或者[]获取其里面的值,但是golang作为一个强类型语言,这种方式并不是最佳实现方式。

更常用的处理方式是像request处理一样,创建一个字段名和返回的response是一一对应的结构体,再将返回的JSON字符串反序列化到结构体里面。但是显然,浏览器告诉我们这个API返回的结构非常复杂,所以我们使用另一个代码生成工具完成这个结构体的定义。 类似地,打开网站后将之前得到的预览JSON字符串放入指定位置,点击转换-嵌套就可以生成对应的Go结构体。

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

得到这个巨大的结构体后,我们需要将之前的打印字符串的语句删掉,然后定义一个DictResponse结构体变量dictResponse,使用json.Unmarshal()方法就可以将之前得到的bodyText反序列化到结构体里。(注意结构体参数在传入的时候需要加&取地址符)接着使用%#v来打印结构体Di,这样可以详细的看到结构体内部的组成。

在打印出response结构体后,会发现大部分字段都是我们不需要的,我们所需要的字段只有音标和释义两个,可以在浏览器里找到这两个字段的名称,分别是英式音标dictResponse.Dictionary.Prons.En和美式音标dictResponse.Dictionary.Prons.EnUs,以及释义数组dictResponse.Dictionary.Explanations,对于释义数组可以使用range关键字很方便地进行打印。

var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
    log.Fatal(err)
}
//fmt.Printf("%#v\n", dictResponse)
fmt.Println("UK:", dictResponse.Dictionary.Prons.En," US:", dictResponse.Dictionary.Prons.EnUs)
for _, item := range dictResponse.Dictionary.Explanations {
	fmt.Println(item)
}

为了防止网络出错等错误原因,建议在代码中添加防卫式的声明,以防由于404或者403错误导致打印空结果从而很难排查问题。

if resp.StatusCode != 200 {
    log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
}

完成上述工作后,需要再整体修改一下代码结构,将上述功能封装成一个函数,主函数从命令行接收参数传入程序并判断输入是否合法,提升代码的鲁棒性。

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

2.2 具体代码

太长,为了70%文字代码比就不放了。

3 Socks5代理服务器

不会,暂时没写。