Go语言实战案例(上) | 青训营

Go语言实战案例(上)

实践记录 · 2023/7/30 · 玉米哥

目录

猜数游戏

在线词典

SOCKS5代理

前言

在前两天,我在掘金平台上陆续发布了两篇文章Go语言入门指南:基础语法(上)Go语言入门指南:基础语法(下),总字数超过一万字,本以为这两篇文章会是自我感动之作,默默无闻石沉大海无人问津,但是没想到会收到大家的评论和点赞,还有几个小伙伴表示文章很清晰易懂,这让我备受鼓舞。与此同时,也感到诚惶诚恐。

当然,我目前能够做的,就是持续输出自己对知识点的理解和思考,我写文章的目的很明确:就是给别人看的,因此评价指标也很简单,小伙伴认可的文章,就是好文章;小伙伴看了一头雾水的文章,就得回炉重造。

废话不多说,开始讲今天的东西。

以下内容主要总结于

猜数游戏

课程中的第一个例子是猜数游戏,这个例子麻雀虽小,但是五脏俱全,包含了以下知识点。

  • 输入的获取
  • 官方库函数的使用
  • 循环与选择语句结合的控制流

在这篇文章中,我假设小伙伴有其他语言的学习基础,因此不会讲解整个程序的实现过程,而是将重点放在输入的获取上。

从控制台读取输入

一个程序,可以从很多渠道获取输入,对于刚学习编程的小伙伴,接触最多的可能就是从命令行敲下自己的输入,按下回车键,然后将输入发送给程序。

输入除了来自于键盘,还可以来自本地磁盘中保存的文件,以及从网络接受到的数据。

在视频中,王克纯老师使用bufio包中的函数来从控制台获取输入,起初,我很疑惑,为什么处理输入的方式这么麻烦。

老式处理输入的方式

C++中,当我想从键盘获取输入,并保存 到整型变量guess中,我可以这样做。

C++ 复制代码
int guess;
cin >> guess;

我相信绝大多数小伙伴,此前也是通过这样的方式来处理的,可是示例却是这样的。

go 复制代码
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
input = strings.TrimSuffix(input, "\n")
guess, err := strconv.Atoi(input)

从键盘获取输入的数字,并保存到guess变量中,经过了两个中间变量readerinput

自己难为自己,何苦呢?

直到我查阅了Go语言中关于处理输入的函数,才发现,如果还在用之前学习CC++的思维去处理输入,才是真的难为自己。

Go语言中与处理输入有关的库函数。

新式处理输入的方式

如此之多的函数,对输入的处理各有不同,什么时候保留\n,什么时候丢弃\n,读取字符串时什么规则,读取数字时又是什么规则。纷繁复杂,无法记忆。而且记了恐怕也难以用得上,最终只会为难自己。因此,我们可以制定一种读取输入的规范,来简化输入处理的流程。

  • 明确输入源
  • 一次读取一整行输入
  • 将读取到的输入按需处理

遵从这个规范,到现在,我们可以好好介绍下示例中的bufio了。

明确输入源

在上文我们提到,输入可以来源于键盘、网络、文件等。在示例中,我们需要从键盘中读取数据。因此,我们创建了一个reader,并使用os.Stdin进行初始化。os.Stdin指标准输入,一般情况下,标准输入与键盘相关联。这下子,我们就明确了目的:从键盘读取输入。

go 复制代码
reader := bufio.NewReader(os.Stdin)

什么是reader?

Go语言的io包中,定义了一个接口io.Reader,该接口中包含了一个Read函数。

如果你不懂什么是接口,这并不要紧。目前,你只需要将自己看作一名水管工人,数据就是水流。io.Reader是一种特殊的工具,你使用它连接到不同的水源。当你连接到一种水源后,便可以打开水龙头(调用Read函数或类似的函数)获取水源,水流会一点点流入你的水桶,直至水桶装满或者水流完。

处理输入并存入变量
go 复制代码
input, err := reader.ReadString('\n')

明确输入的来源后,就可以正式读取输入了,执行上面代码时,会按照顺序发生下面的事情。

  1. 程序的运行会发生阻塞,将控制权交给键盘。

  2. 在键盘上敲击50<Enter>,程序此时接收到了来自键盘的输入流50\n,该输入流的本质是由5 0 \n组成的一连串字节序列。

  3. Reader.readString函数对该字符串进行处理。

    ReadString reads until the first occurrence of delim in the input, returning a string containing the data up to and including the delimiter.

    Go语言官网对该函数的介绍说,该函数读取输入的字符串,直至遇到指定的分隔符,然后返回该字符串和紧随其后的分隔符。

在示例中,我们使用变量input接受该函数返回的结果,显而易见,input变量中包含了字符串50\n

按需处理保存的字符串

然而我们想要的是数字50,需要使用strconv.Atoi函数将字符串转换为数字,在这之前,还需要去掉字符串的后缀\n,再做转换。

go 复制代码
input = strings.TrimSuffix(input, "\n")
guess, err := strconv.Atoi(input)

大功告成!我们终于得到了魂牵梦索的guess变量,后续就是与猜数游戏控制流相关的逻辑,这里不再赘述了。

从字符串读取输入

为什么要浪费大把的墨水来写仅仅一个guess变量的读取呢,如果你认真读完了上面的内容,现在就到了触类旁通的时候了。我们按照一样的规定从字符串中读取输入。

明确输入源

我们很明确,需要从字符串中读取输入,因此,我们应该创建一个从字符串读取输入的reader

go 复制代码
greeting := "你好,字节跳动"
reader := strings.NewReader(greeting)

strings.NewReader函数接受一个字符串,返回一个reader。

使用Read函数读取字符串

当我们从输入源获取数据后,现在是时候将数据保存在一些地方了,需要一个类似guess的变量来充当水桶接水。

go 复制代码
// bottle是一个字节切片,大小为3字节
bottle := make([]byte, 3)

注意,UTF-8编码的中文字符使用3个字节表示。

使用Read函数(打开水龙头),将输入源的数据保存到变量bottle中(用指定大小的瓶子装水)。

第一次调用Read函数
go 复制代码
// 从reader中读取3个字节的数据,填充满bottle变量
// bottle == "你"
// bytesRead == 3,表示本次从reader读取了3个字节的数据
// err == nil,表示读取正常
bytesRead, err := reader.Read(bottle)

到目前为止,reader中的数据已经被取走了3个字节,被保存到bottle中。仔细一想,reader中的数据并没有被取完(水源中还有水)。

我们可以再次调用Read函数来读取输入源中剩下的数据。

第二次调用Read函数
go 复制代码
// 从reader继续读取3个字节的数据,填充满bottle变量
// bottle == "好"
// bytesRead == 3,表示本次从reader读取了3个字节的数据
// err == nil,表示读取正常
bytesRead, err = reader.Read(bottle)

有心的小伙伴想一想,总共需要调用几次Read函数才能将reader中的数据读取完毕呢?

答案就是7次。因为我们的输入源,也就是字符串你好,字节跳动一共有7个中文字符,总计21个字节。而我们的bottle变量每次只能存储3个字节的数据,因此一共需要读取7次。

使用for循环读取

为了省时省力省心,我们可以使用for循环来读取reader中的数据。基本原理同上。

go 复制代码
for {
    bytesRead, err := reader.Read(bottle)    
    if err != nil {
        break;
    }

    fmt.Printf("本次读取的字节数 %v 本次读取的内容 %v 本次读取的状态 %v\n", bytesRead, bottle, err)
}

请尝试运行,你会得到下面的结果。

cmd 复制代码
本次读取的字节数 3 本次读取的内容 [228 189 160] 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 [229 165 189] 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 [239 188 140] 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 [229 173 151] 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 [232 138 130] 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 [232 183 179] 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 [229 138 168] 本次读取的状态 <nil>

奇了怪了,为什么读取的内容是一个字节切片呢?

请回头看看,我们的bottle变量就是大小为3个字节的字节切片。

有伙伴可能会问,会什么我们不直接将bottle定义为string类型呢?

因为Read函数的参数必须使用[]bytes,也就是字节切片。

那我们该如何得到正常的字符串呢,答案也很简单,使用%s替换%v即可。

cmd 复制代码
本次读取的字节数 3 本次读取的内容 你 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 好 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 , 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 字 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 节 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 跳 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 动 本次读取的状态 <nil>

最后一个问题,是什么原因使得这个死循环停下来了呢?从结果来看,当字符串读取完毕时,循环就停下来了。其实原因就在err身上。第七次调用Read函数后,数据已经被取完了,第八次调用Read函数时,已经没有数据了,Read函数会返回io.EOF,表示读取状态为已经读取完毕。很显然这个值不等于nil,于是控制流跳出了循环。

从文件读取输入

如果你耐心看到了这里,那么你对reader应该有了更为深刻的认识了。现在我们按照同样的准则读取文件。

在go源文件目录下新建文本文件sample.txt

txt 复制代码
你好,字节跳动。字节和心脏,只能有一个跳动。先进团队,先用飞书。抖音,记录美好生活。
  1. 创建一个reader,同时指明输入源是文件。
go 复制代码
fileReader, err := os.Open("sample.txt")
if err != nil {
log.Fatal(err)
}
  1. 使用Read函数读取。
go 复制代码
bottle := make([]byte, 3)
for {
	bytesRead, err := fileReader.Read(bottle)
	if err != nil {
			break
	}
	fmt.Printf("本次读取的字节数 %v 本次读取的内容 %s 本次读取的状态 %v\n", bytesRead, bottle, err)
}

结果呈现。

cmd 复制代码
本次读取的字节数 3 本次读取的内容 你 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 好 本次读取的状态 <nil>
......(省略中间的输出)......
本次读取的字节数 3 本次读取的内容 生 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 活 本次读取的状态 <nil>
本次读取的字节数 3 本次读取的内容 。 本次读取的状态 <nil>

更进一步

写到这里,小伙伴应该对输入的读取没有问题了,可是,我想一次性读取整个文件,而不是每次都读取这三瓜两枣,我该怎么办?

回到对reader的讨论,在本篇文章的例子中,我们使用了这些方法创建一个reader

  • reader := bufio.NewReader(os.Stdin)
  • reader := strings.NewReader(greeting)
  • fileReader, err := os.Open("sample.txt")

我们使用了这些方法读取对应的reader

  • input, err := reader.ReadString('\n')

  • bytesRead, err := reader.Read(bottle)

  • bytesRead, err := fileReader.Read(bottle)

很明显,第二、三个reader使用了同样的读取函数Read,也是所有reader都具有的共同函数。你可以自己试一试,第一个reader也可以使用该Read函数。

那第一个reader使用的ReadString函数是怎么回事呢,其实,这是bufioreader做了一层包装,添加了一些其他的函数,方便对输入源进行不同的处理。

比如,ReadString函数就可以接受一个参数作为分割符,将分隔符之前的输入包括分隔符返回到一个变量中。

那输入源是字符串和文件的reader就注定没办法使用ReadString函数了吗?

其实不是,我们只需要使用bufio对它俩的reader做一个包装即可。

go 复制代码
strReader, err := strings.NewReader(greeting)
bufReader, err := bufio.NewReader(strReader)
go 复制代码
fileReader, err := os.Open("sample.txt")
bufReader, err := bufio.NewReader(fileReader)

bufio可以接受一个reader参数并对其包装,返回一个新的reader

现在我们的bufReader可以接受来自键盘、字符串、文件的输入源,并使用ReadString函数读取了。

到这里,我们还是没有找到一次性读取整个文件的办法。小伙伴们可以尝试一下,在上文创建的三种reader中,找不到一个类似reader.ReadAll可以读取全部字符串的函数。

幸运的是,io.ReadAll函数可以接受reader参数,并以字节切片的形式返回reader中的所有数据。

go 复制代码
fileReader, err := os.Open("sample.txt")
	if err != nil {
		log.Fatal(err)
	}

	allBytes, err := io.ReadAll(fileReader)
	if err != nil {
		return
	}
	fmt.Printf("%s", allBytes)

下一步

本篇文章到这里就结束了,由于篇幅的限制,我只讲解了猜数游戏相关的话题,并将重点放在了输入的获取上。下一篇,我将讲解客户端与服务器通信的过程,会涉及到HTTP协议,如果你没有学过计算机网络,那也不要紧,在这里,我推荐这本书《图解HTTP》。了解了HTTP相关的知识后,学习Web开发也会变得更加容易,如果你无法找到这本书的电子版本,出于学习或者研究的目的,可以向我咨询。

码字不易,如果您看到了这里,听我说谢谢你😀

如果您觉得本文还不错,请留下小小的赞😀

如果您有感而发,请留下宝贵的评论😀

相关推荐
千慌百风定乾坤1 天前
Go 语言入门指南:基础语法和常用特性解析(下) | 豆包MarsCode AI刷题
青训营笔记
FOFO1 天前
青训营笔记 | HTML语义化的案例分析: 粗略地手绘分析juejin.cn首页 | 豆包MarsCode AI 刷题
青训营笔记
滑滑滑3 天前
后端实践-优化一个已有的 Go 程序提高其性能 | 豆包MarsCode AI刷题
青训营笔记
柠檬柠檬3 天前
Go 语言入门指南:基础语法和常用特性解析 | 豆包MarsCode AI刷题
青训营笔记
用户967136399653 天前
计算最小步长丨豆包MarsCodeAI刷题
青训营笔记
用户52975799354724 天前
字节跳动青训营刷题笔记2| 豆包MarsCode AI刷题
青训营笔记
clearcold4 天前
浅谈对LangChain中Model I/O的见解 | 豆包MarsCode AI刷题
青训营笔记
夭要7夜宵4 天前
【字节青训营】 Go 进阶语言:并发概述、Goroutine、Channel、协程池 | 豆包MarsCode AI刷题
青训营笔记
用户336901104445 天前
数字分组求和题解 | 豆包MarsCode AI刷题
青训营笔记
dnxb1235 天前
GO语言工程实践课后作业:实现思路、代码以及路径记录 | 豆包MarsCode AI刷题
青训营笔记