Go 编程实战系列(十五)
上一篇《Go fmt 包详解》中,我们学习了 fmt.Print、fmt.Println、fmt.Printf、fmt.Sprintf 等常用格式化方法。本篇继续学习 Go 中非常基础但非常重要的内容:输入与输出。
所谓输入,就是程序从外部读取数据;输出,就是程序将处理结果发送到外部。最简单的输入来自键盘,最简单的输出就是终端打印。实际开发中,输入输出还包括文件、网络、管道等多种形式。
本文主要介绍:
-
标准输入和标准输出;
-
fmt.Scan; -
fmt.Scanln; -
fmt.Scanf; -
bufio.Reader; -
bufio.Writer; -
os.Stdin; -
os.Stdout; -
输入错误处理;
-
实际命令行程序案例。
一、什么是输入输出?
程序运行时通常需要和外部环境交互。
例如,一个登录程序需要用户输入:
用户名
密码
程序处理后输出:
登录成功
这里:
用户名、密码 = 输入
登录成功 = 输出
再比如计算器:
输入:
10
20
输出:
30
Go 中最基础的输入输出通常通过:
bash
fmt
bufio
os
这些标准库完成。
二、标准输入与标准输出
操作系统通常为程序提供三个标准流:
c
标准输入 stdin
标准输出 stdout
标准错误 stderr
Go 中分别对应:
lua
os.Stdin
os.Stdout
os.Stderr
其中 os.Stdin 通常连接键盘,os.Stdout 通常连接终端。
例如:
go
package main
import (
"fmt"
"os"
)
func main() {
fmt.Fprintln(os.Stdout, "Hello Go")
}
输出:
Hello Go
实际上,我们平时写的:
arduino
fmt.Println("Hello Go")
最终也是把内容输出到标准输出。
三、使用 fmt.Scan 输入数据
最简单的输入方法是:
scss
fmt.Scan()
例如:
go
package main
import "fmt"
func main() {
var name string
fmt.Print("请输入姓名:")
fmt.Scan(&name)
fmt.Println("你好:", name)
}
运行:
请输入姓名:Tom
你好: Tom
这里需要重点注意:
yaml
&name
为什么要加 &?
因为 fmt.Scan 需要修改变量中的值,因此需要获得变量地址。
如果写成:
scss
fmt.Scan(name)
就不能正确接收输入。
四、同时输入多个值
fmt.Scan 可以一次读取多个数据。
例如:
go
package main
import "fmt"
func main() {
var name string
var age int
fmt.Print("请输入姓名和年龄:")
fmt.Scan(&name, &age)
fmt.Println("姓名:", name)
fmt.Println("年龄:", age)
}
输入:
Tom 20
输出:
姓名: Tom
年龄: 20
多个输入值之间可以使用空格、换行等空白字符分隔。
五、Scan 会自动进行类型解析
例如:
csharp
var age int
fmt.Scan(&age)
如果输入:
20
Go 会尝试把输入内容解析成 int。
再比如:
go
var price float64
fmt.Scan(&price)
输入:
99.9
最终变量中就是对应的浮点数。
如果输入内容和目标类型不匹配,则会返回错误。
例如:
go
var age int
_, err := fmt.Scan(&age)
if err != nil {
fmt.Println("输入错误:", err)
}
如果用户输入:
hello
就无法转换成整数。
六、fmt.Scan 的返回值
fmt.Scan 会返回两个值:
css
n, err := fmt.Scan(...)
其中:
n
表示成功扫描的数据数量。
err
表示是否出现错误。
例如:
go
package main
import "fmt"
func main() {
var name string
var age int
n, err := fmt.Scan(&name, &age)
fmt.Println("成功读取:", n)
if err != nil {
fmt.Println("读取失败:", err)
}
}
实际开发中,不建议完全忽略 err。
七、fmt.Scanln
Scanln 和 Scan 类似。
例如:
csharp
var name string
fmt.Scanln(&name)
它会读取一行中的数据,在遇到换行时结束。
示例:
go
package main
import "fmt"
func main() {
var name string
var age int
fmt.Print("请输入姓名和年龄:")
fmt.Scanln(&name, &age)
fmt.Println(name, age)
}
输入:
Tom 20
输出:
Tom 20
需要注意,Scanln 并不适合直接读取包含空格的完整句子。
例如用户输入:
Hello Go World
如果只有一个字符串变量:
vbnet
var text string
fmt.Scanln(&text)
通常只能得到第一个单词:
Hello
如果需要读取整行文本,更适合使用 bufio.Reader。
八、fmt.Scanf
Scanf 可以按照指定格式读取数据。
例如:
go
package main
import "fmt"
func main() {
var name string
var age int
fmt.Print("请输入:")
fmt.Scanf("%s %d", &name, &age)
fmt.Println(name, age)
}
输入:
Tom 20
输出:
Tom 20
这里:
perl
%s
表示字符串。
perl
%d
表示整数。
它和之前学习的 Printf 格式占位符非常类似。
九、为什么需要 bufio?
对于简单输入:
scss
fmt.Scan()
已经够用。
但是如果需要读取:
-
一整行文本;
-
包含空格的字符串;
-
大量数据;
-
文件内容;
-
高频输入输出;
通常会使用:
bufio
bufio 表示 buffered I/O,也就是带缓冲的输入输出。
它可以减少频繁的底层读写操作,提高效率。
十、使用 bufio.Reader 读取一整行
例如:
go
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("请输入一句话:")
text, err := reader.ReadString('\n')
if err != nil {
fmt.Println("读取失败:", err)
return
}
text = strings.TrimSpace(text)
fmt.Println("你输入的是:", text)
}
输入:
Hello Go World
输出:
你输入的是: Hello Go World
这里:
lua
bufio.NewReader(os.Stdin)
创建一个缓冲读取器。
scss
ReadString('\n')
表示一直读取到换行符。
这种方式非常适合交互式命令行程序。
十一、为什么需要 TrimSpace?
使用:
scss
ReadString('\n')
读取的数据通常包含结尾换行。
例如用户输入:
Tom
实际字符串可能类似:
Tom\n
因此通常需要:
arduino
strings.TrimSpace(text)
去掉:
-
空格;
-
\n; -
\r\n; -
制表符等首尾空白。
例如:
arduino
text = strings.TrimSpace(text)
这样后续比较字符串更加方便。
十二、使用 bufio.Scanner
除了 Reader,还可以使用:
bufio.Scanner
例如:
erlang
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("请输入内容:")
if scanner.Scan() {
fmt.Println("你输入:", scanner.Text())
}
}
Scanner 默认按行读取。
输入:
学习 Go 编程
输出:
你输入: 学习 Go 编程
如果要连续读取多行:
scss
for scanner.Scan() {
fmt.Println(scanner.Text())
}
当输入结束后,循环结束。
十三、标准输出 os.Stdout
前面我们一直使用:
scss
fmt.Println()
其实 fmt 还提供:
scss
fmt.Fprint()
fmt.Fprintln()
fmt.Fprintf()
它们可以把内容写入指定目标。
例如写到标准输出:
lua
fmt.Fprintln(os.Stdout, "Hello Go")
也可以格式化:
perl
fmt.Fprintf(
os.Stdout,
"姓名:%s 年龄:%d\n",
"Tom",
20,
)
这种设计很重要,因为同一套输出逻辑不仅可以输出到终端,也可以输出到文件。
十四、使用 bufio.Writer 输出
大量输出时,可以使用:
bufio.Writer
例如:
erlang
package main
import (
"bufio"
"os"
)
func main() {
writer := bufio.NewWriter(os.Stdout)
writer.WriteString("Hello Go\n")
writer.WriteString("学习输入输出\n")
writer.Flush()
}
必须注意:
scss
writer.Flush()
缓冲数据可能暂时存储在内存中,如果不调用 Flush(),部分内容可能还没有真正写出去。
实际项目中常见写法:
css
writer := bufio.NewWriter(os.Stdout)
defer writer.Flush()
十五、标准错误输出
错误信息可以写到:
lua
os.Stderr
例如:
lua
fmt.Fprintln(
os.Stderr,
"程序运行失败",
)
标准输出和标准错误分离后,在 Linux、Docker、Shell 脚本和服务器环境中更加方便处理。
例如可以分别重定向:
正常输出
错误日志
十六、读取数字并处理
我们写一个简单的加法程序:
go
package main
import "fmt"
func main() {
var a int
var b int
fmt.Print("请输入两个数字:")
_, err := fmt.Scan(&a, &b)
if err != nil {
fmt.Println("输入格式错误")
return
}
result := a + b
fmt.Println("计算结果:", result)
}
输入:
10 20
输出:
计算结果: 30
这已经是一个最基础的交互式命令行程序。
十七、一个完整的用户信息输入案例
go
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("请输入姓名:")
name, err := reader.ReadString('\n')
if err != nil {
fmt.Println("姓名读取失败:", err)
return
}
name = strings.TrimSpace(name)
fmt.Print("请输入年龄:")
ageText, err := reader.ReadString('\n')
if err != nil {
fmt.Println("年龄读取失败:", err)
return
}
ageText = strings.TrimSpace(ageText)
age, err := strconv.Atoi(ageText)
if err != nil {
fmt.Println("年龄必须是数字")
return
}
fmt.Printf(
"用户信息:姓名=%s,年龄=%d\n",
name,
age,
)
}
输入:
请输入姓名:Tom
请输入年龄:20
输出:
用户信息:姓名=Tom,年龄=20
这个例子综合使用了:
bash
bufio
os.Stdin
strings
strconv
fmt
也是实际命令行程序常见的处理方式。
十八、Scan 和 bufio 如何选择?
对于简单程序:
scss
fmt.Scan()
使用最方便。
例如:
输入整数
输入几个单词
简单练习程序
如果需要读取完整一行:
bufio.Reader
更加合适。
如果需要逐行处理:
bufio.Scanner
更加方便。
如果需要大量输出:
bufio.Writer
通常性能更好。
可以简单记住:
简单输入:fmt.Scan
整行输入:bufio.Reader
逐行扫描:bufio.Scanner
大量输出:bufio.Writer
十九、常见错误
1. 忘记使用地址符号
错误:
csharp
var age int
fmt.Scan(age)
正确:
scss
fmt.Scan(&age)
因为 Scan 需要修改变量。
2. 忽略输入错误
不推荐:
scss
fmt.Scan(&age)
更稳妥:
go
_, err := fmt.Scan(&age)
if err != nil {
fmt.Println(err)
return
}
3. 使用 Scan 读取带空格文本
例如:
Hello Go World
Scan 会按照空白符分割,不能直接得到整句话。
推荐:
lua
bufio.NewReader(os.Stdin)
4. bufio.Writer 忘记 Flush
错误:
css
writer := bufio.NewWriter(os.Stdout)
writer.WriteString("Hello")
推荐:
css
writer := bufio.NewWriter(os.Stdout)
defer writer.Flush()
二十、本章小结
本篇介绍了 Go 中最基础的输入输出操作,主要学习了:
-
使用
fmt.Scan获取输入; -
使用
fmt.Scanln按行扫描; -
使用
fmt.Scanf按格式读取; -
使用
bufio.Reader读取完整文本; -
使用
bufio.Scanner逐行扫描; -
使用
bufio.Writer提高输出效率; -
使用
os.Stdin表示标准输入; -
使用
os.Stdout表示标准输出; -
使用
os.Stderr输出错误信息。
输入输出是很多程序的基础。以后开发文件处理工具、CLI 工具、网络程序和 Web 服务时,都会进一步接触 Go 的 I/O 体系。
本章练习
- 使用
fmt.Scan输入姓名和年龄。 - 编写一个程序输入两个整数并输出它们的和。
- 使用
bufio.Reader读取包含空格的一句话。 - 使用
strconv.Atoi将用户输入的年龄转换成整数。 - 使用
bufio.Scanner连续读取多行文本。 - 使用
bufio.Writer输出 100 行内容。 - 尝试将错误信息输出到
os.Stderr。
下一篇预告
《Go 运算符详解》
下一篇将介绍 Go 中常用的运算符,包括:
-
算术运算符;
-
比较运算符;
-
逻辑运算符;
-
赋值运算符;
-
自增与自减;
-
位运算符;
-
运算符优先级;
-
实际业务判断案例。
掌握运算符之后,我们就可以开始编写更加复杂的计算和逻辑判断程序。