Input and Output(输入与输出)
Go 的输入输出(I/O)主要通过标准库完成。fmt 负责格式化,os.Stdin、os.Stdout 和 os.Stderr 表示进程的标准流,bufio 提供缓冲读取与写入,而 io.Reader、io.Writer 则是连接文件、网络和内存数据的通用接口。
输出到终端
fmt.Print、fmt.Println 和 fmt.Printf 默认写入标准输出:
package main
import "fmt"
func main() {
name := "Ada"
requests := 3
fmt.Print("user: ", name, "\n")
fmt.Println("requests:", requests)
fmt.Printf("%s has %d requests\n", name, requests)
}
常用格式化动词包括:
| 动词 | 含义 |
|---|---|
%v | 按默认格式输出值 |
%+v | 输出结构体时包含字段名 |
%T | 输出值的类型 |
%d | 十进制整数 |
%f | 浮点数 |
%s | 字符串 |
%q | 带引号并进行必要转义的字符串 |
%t | 布尔值 |
错误和诊断信息通常写到标准错误,便于调用方把正常输出与错误输出分别处理:
package main
import (
"fmt"
"os"
)
func main() {
fmt.Fprintln(os.Stderr, "configuration is invalid")
}
读取按空白分隔的值
fmt.Fscan 可以从指定的 io.Reader 按空白分隔读取值:
package main
import (
"fmt"
"os"
)
func main() {
var name string
var age int
fmt.Print("请输入姓名和年龄:")
if _, err := fmt.Fscan(os.Stdin, &name, &age); err != nil {
fmt.Fprintln(os.Stderr, "读取输入失败:", err)
os.Exit(1)
}
fmt.Printf("姓名:%s,年龄:%d\n", name, age)
}
输入 Ada 20 时,name 和 age 会分别接收两个 token。必须传入变量地址,扫描函数才能修改变量。Fscan 适合结构简单、以空白分隔的数据;它不会把包含空格的整行文本作为一个字符串读取。
按行读取输入
读取用户输入的一整行时,可以使用 bufio.Reader:
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("请输入标题:")
line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
fmt.Fprintln(os.Stderr, "读取输入失败:", err)
os.Exit(1)
}
title := strings.TrimSpace(line)
if title == "" {
fmt.Fprintln(os.Stderr, "标题不能为空")
os.Exit(1)
}
fmt.Println("标题:", title)
}
最后一行可能没有换行符,此时 ReadString 可以同时返回已读取数据和 io.EOF。程序应先处理已读取数据,而不是看到 io.EOF 就直接丢弃结果。strings.TrimSpace 会删除两端所有 Unicode 空白;如果业务只允许去掉行尾换行,应使用更精确的处理方式。
使用 Scanner 逐行处理
需要循环读取多行时,bufio.Scanner 提供了方便的按行接口:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
lineNumber := 0
for scanner.Scan() {
lineNumber++
fmt.Printf("%d: %s\n", lineNumber, scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "读取输入失败:", err)
os.Exit(1)
}
}
循环结束后必须检查 scanner.Err(),否则 I/O 错误会被误认为正常结束。Scanner 对单个 token 有大小限制;处理可能很长的行时,应在扫描前通过 Scanner.Buffer 设置经过评估的上限,或改用 bufio.Reader。上限也应存在,避免不受控制的内存占用。
缓冲输出与 Flush
频繁执行小块写入时,可以使用 bufio.Writer 减少底层写入次数:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
writer := bufio.NewWriter(os.Stdout)
if _, err := fmt.Fprintln(writer, "first line"); err != nil {
fmt.Fprintln(os.Stderr, "写入失败:", err)
os.Exit(1)
}
if _, err := fmt.Fprintln(writer, "second line"); err != nil {
fmt.Fprintln(os.Stderr, "写入失败:", err)
os.Exit(1)
}
if err := writer.Flush(); err != nil {
fmt.Fprintln(os.Stderr, "刷新输出失败:", err)
os.Exit(1)
}
}
缓冲区中的数据只有刷新后才保证提交给底层写入器。命令行工具尤其要检查 Flush 返回的错误,因为磁盘已满或管道提前关闭等失败可能直到刷新时才出现。
io.Reader 与 io.Writer
许多标准库 API 不要求具体文件类型,而只依赖小接口:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
因此,同一段处理逻辑可以用于标准输入、文件、网络连接或内存缓冲区:
package main
import (
"fmt"
"io"
"os"
"strings"
)
func copyMessage(dst io.Writer, src io.Reader) error {
_, err := io.Copy(dst, src)
return err
}
func main() {
if err := copyMessage(os.Stdout, strings.NewReader("Hello, Go!\n")); err != nil {
fmt.Fprintln(os.Stderr, "复制失败:", err)
os.Exit(1)
}
}
依赖接口可以让函数更容易测试:测试时可传入 strings.Reader、bytes.Buffer,无需真的操作终端或文件。
输入校验与资源边界
来自终端、文件或网络的输入都不可信。读取成功只代表字节已到达,不代表业务值有效。程序还应根据场景检查:
- 数字是否在允许范围内。
- 字符串是否为空、过长或编码无效。
- 输入数量是否符合协议。
- 扫描 token、请求体或文件大小是否有上限。
- 输出错误是否已传播或转化为正确的退出状态。
打开文件后还要关闭资源,并同时考虑读取错误与关闭错误。更复杂的文件 I/O 应在专门的文件处理专题中结合权限、原子写入和并发访问继续学习。
与 Node.js 的关键差异
Go 的 io.Reader 和 io.Writer 是同步接口;一次调用可能阻塞当前 goroutine,但 Go 运行时可以调度其他 goroutine。它们不等同于 Node.js Stream 或 Promise。Go 调用通常通过返回值显式报告错误,缓冲写入还需要显式 Flush 并检查结果。
延伸学习
- File(文件):文件关闭错误、流式读取、路径和原子替换。
- Unit Test(单元测试):使用内存中的 reader、writer 和表格驱动测试验证 I/O 逻辑。
小结
fmt提供格式化输入输出,标准流由os.Stdin、os.Stdout和os.Stderr表示。Fscan适合按空白读取结构化 token,bufio.Reader和Scanner适合按行处理。Scanner结束后必须检查错误,并为可能的大输入设置合理边界。- 缓冲写入必须执行
Flush,关键程序还应处理刷新错误。 - 面向
io.Reader和io.Writer编程可以复用逻辑并简化测试。 - I/O 成功不等于输入有效,外部数据必须进行业务校验和容量限制。