Go语言实践案例之猜谜游戏 | 青训营

72 阅读4分钟

目标实现

程序首先生成一个介于 1 与 100 之间的随机整数,玩家可以猜测这个数是多少,玩家每次猜错,程序都会提示玩家所猜的数是太大或是太小,并且让玩家再次猜测,如果玩家猜对了,则告诉玩家成功并自动退出程序。

具体过程

生成随机数

生成随机数的函数在 "math/rand" 包里,使用 rand.Intn() 函数来生成随机数,其传入的参数代表了随机数的上限(下限为 1 ),但是在以往,用该函数生成随机数仍然存在问题,即生成随机数的种子不变,则生成的随机数也不变,因此需要把随机数种子与时间挂钩,即使用 time.Now().UnixNano() 获取时间戳作为随机数种子, 传入 rand.Seed() 函数,但如今后者已弃用,只需使用 rand.Intn() 函数即可生成次次不同的随机数。

读取用户输入

这里选用的是缓冲读取的方式,涉及到多个特殊功能的使用:

  • os.Stdin 表示标准输入流,常作为文件对象被 bufio.NewReader() 等读取函数读取;
  • bufio.NewReader() 表示带缓冲的读取器,用于从标准输入中读取数据,返回指针;
  • ReadString()bufio 包中的函数,从输入流读取字符串直到指定的分隔符,并返回读取到的字符串(但并非直接输出)。

相比于直接使用 fmt.Scanf() ,缓冲读取有以下几点不同:

  1. 行读取:

    bufio.NewReader(os.Stdin) 创建了一个带有缓冲区的读取器,它可以很方便地一次性读取整行输入(包括空格和换行符),并对输入进行处理。这在需要读取整行文本的情况下非常有用。

    fmt.Scanf() 主要是按照格式化字符串指定的格式逐个读取数据,它对于整行文本的处理不如 bufio.NewReader(os.Stdin) 方便。

  2. 自定义解析:

    bufio.NewReader(os.Stdin) 允许你根据需要自定义解析用户输入。你可以使用 ReadString() 方法读取整行文本,然后根据自己的逻辑解析其中的数据。

    fmt.Scanf() 提供了格式化输入,但在处理复杂输入时可能需要多次调用并使用不同的格式化字符串,可能会变得冗长和不够灵活。

  3. 错误处理:

    bufio.NewReader(os.Stdin) 提供了更灵活的错误处理。你可以检查读取错误并采取适当的措施,例如重新读取或提供错误消息。

    fmt.Scanf() 也会返回错误,但对于复杂的输入和错误处理,bufio.NewReader(os.Stdin) 通常更强大。

读取用户输入部分代码如下所示:

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
	}
	input = strings.Trim(input, "\r\n")

其中 strings.Trim() 用于除去字符串的指定前后缀,\r\n 表示回车换行,也即电脑上的 Enter

然后还需用 strconv.Atoi() 函数把字符串转成整型(毕竟不是 fmt.Scanf() 那样的格式化输出)。

实现判断逻辑

只需把玩家输入的数与随机数比较,用 if else 语句判断即可。

实现游戏循环

把用户输入和判断逻辑的具体实现搬到 for 的死循环里,在判断逻辑中用 continue 和 break 控制循环即可。

代码优化

如果不使用缓冲读取,则可使用 fmt.Scanf() 来简化代码,如下所示:

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	//生成随机数
	maxNum := 100
	secretNumber := rand.Intn(maxNum)

	//用户输入、判断与循环的逻辑实现
	fmt.Println("Please input your guess")
	var guess int
	for {
		_, err := fmt.Scanf("%d\r\n", &guess)
		if err != nil {
			fmt.Println("Invalid input. Please enter an integer value")
			continue
		}
		fmt.Println("You guess is", guess)
		if guess > secretNumber {
			fmt.Println("Your guess is bigger than the secret number. Please try again")
		} else if guess < secretNumber {
			fmt.Println("Your guess is smaller than the secret number. Please try again")
		} else {
			fmt.Println("Correct, you Legend!")
			break
		}
	}
}

不过个人感觉这里面还是有一些奇怪的事情发生,这是以前写 C++ 的时候一直没有注意到的,正常来说,第 17 行代码应该直接写 "%d" 就好了(至少在 C/C++ 里这样写是绝对没有问题的),但是在这里,输入一次,fmt.Scanf() 却读取了两次,然后才可以再次输入。第一次读取是正常的,而第二次读取并没有改变 guess 的值,但是第 17 行就像跳过了一样,err 也不为空了,感觉这就是读取机制的问题了。

个人认为,在 C/C++ 中,分隔符是不会被读取的,单纯起到的就是一个分隔的作用,不会对第二次输入造成影响;然而 Golang 里分隔符似乎是会被读取的,所以如果以 "%d" 格式化输入,第一次读取的确实是赋给 guess 的值,但第二次读取的就是分隔符,分隔符自然不满足 "%d" 的格式化输入,返回不为空的错误,但由于解析失败并没有新值赋给 guess ,所以 guess 的值没变。此即为一次输入两次读取的过程。

因此,解决方法就是把 %d 改成 %d\r\n,既保证了一次输入一次读取,又能将值确实地赋给 guess