跳到主要内容

Input and Output(输入与输出)

Go 的输入输出(I/O)主要通过标准库完成。fmt 负责格式化,os.Stdinos.Stdoutos.Stderr 表示进程的标准流,bufio 提供缓冲读取与写入,而 io.Readerio.Writer 则是连接文件、网络和内存数据的通用接口。

输出到终端

fmt.Printfmt.Printlnfmt.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 时,nameage 会分别接收两个 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.Readerio.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.Readerbytes.Buffer,无需真的操作终端或文件。

输入校验与资源边界

来自终端、文件或网络的输入都不可信。读取成功只代表字节已到达,不代表业务值有效。程序还应根据场景检查:

  • 数字是否在允许范围内。
  • 字符串是否为空、过长或编码无效。
  • 输入数量是否符合协议。
  • 扫描 token、请求体或文件大小是否有上限。
  • 输出错误是否已传播或转化为正确的退出状态。

打开文件后还要关闭资源,并同时考虑读取错误与关闭错误。更复杂的文件 I/O 应在专门的文件处理专题中结合权限、原子写入和并发访问继续学习。

与 Node.js 的关键差异

Go 的 io.Readerio.Writer 是同步接口;一次调用可能阻塞当前 goroutine,但 Go 运行时可以调度其他 goroutine。它们不等同于 Node.js Stream 或 Promise。Go 调用通常通过返回值显式报告错误,缓冲写入还需要显式 Flush 并检查结果。

延伸学习

小结

  • fmt 提供格式化输入输出,标准流由 os.Stdinos.Stdoutos.Stderr 表示。
  • Fscan 适合按空白读取结构化 token,bufio.ReaderScanner 适合按行处理。
  • Scanner 结束后必须检查错误,并为可能的大输入设置合理边界。
  • 缓冲写入必须执行 Flush,关键程序还应处理刷新错误。
  • 面向 io.Readerio.Writer 编程可以复用逻辑并简化测试。
  • I/O 成功不等于输入有效,外部数据必须进行业务校验和容量限制。