两个小接口撑起了很多标准库
Go 标准库里有两个非常重要的接口:io.Reader 和 io.Writer。它们看起来小到不能再小,却贯穿文件、网络、压缩、HTTP、模板、JSON、命令行工具和测试。理解它们后,你会发现很多 API 的设计突然变得一致。
Reader 表示“能读出字节”,Writer 表示“能写入字节”。文件能读写,HTTP 响应体能读,请求体能读,内存 buffer 能读写,字符串也能包装成 reader。函数只要依赖这些接口,就能同时支持很多数据来源和目标。
这篇文章用几个小例子讲清楚数据流抽象。重点不是背接口定义,而是学会写不依赖具体文件路径的函数。
Reader 和 Writer 的定义
简化来看:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
你很少需要手写实现这两个接口,但会经常接收它们作为参数。
读取全部内容:
func ReadText(r io.Reader) (string, error) {
data, err := io.ReadAll(r)
if err != nil {
return "", err
}
return string(data), nil
}
这个函数可以读文件:
file, err := os.Open("note.txt")
if err != nil {
return err
}
defer file.Close()
text, err := ReadText(file)
也可以读字符串:
text, err := ReadText(strings.NewReader("hello"))
这就是接口的力量:函数只关心“能读”,不关心来源。
写入目标也可以抽象
func WriteLines(w io.Writer, lines []string) error {
for _, line := range lines {
if _, err := fmt.Fprintln(w, line); err != nil {
return err
}
}
return nil
}
写到标准输出:
err := WriteLines(os.Stdout, []string{"a", "b"})
写到文件:
file, err := os.Create("out.txt")
if err != nil {
return err
}
defer file.Close()
err = WriteLines(file, []string{"a", "b"})
测试时写到内存:
var buf bytes.Buffer
err := WriteLines(&buf, []string{"a", "b"})
fmt.Println(buf.String())
这种设计让代码天然可测试。你不需要为了测试真的创建文件,也不需要捕获标准输出。
io.Copy 连接 Reader 和 Writer
复制数据:
written, err := io.Copy(dst, src)
比如复制文件:
func CopyFile(dstPath, srcPath string) error {
src, err := os.Open(srcPath)
if err != nil {
return fmt.Errorf("open source: %w", err)
}
defer src.Close()
dst, err := os.Create(dstPath)
if err != nil {
return fmt.Errorf("create destination: %w", err)
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
return fmt.Errorf("copy data: %w", err)
}
return nil
}
io.Copy 内部会循环读取并写入。你不用自己写缓冲区循环。很多数据转发场景都能用它,比如 HTTP 下载保存到文件、解压流写入目标、请求体转发到另一个服务。
LimitReader 防止读太多
如果输入不可信,不要无限读取:
func ReadLimited(r io.Reader, max int64) ([]byte, error) {
limited := io.LimitReader(r, max)
return io.ReadAll(limited)
}
HTTP handler 里也常见类似思路。对外部请求体、上传内容、日志片段,都要考虑大小上限。
需要注意:io.LimitReader 只限制最多读多少,不会告诉你原始数据是否超过限制。如果你需要判断超限,可以多读一个字节:
func ReadWithLimit(r io.Reader, max int64) ([]byte, error) {
var buf bytes.Buffer
_, err := io.CopyN(&buf, r, max+1)
if err != nil && err != io.EOF {
return nil, err
}
if int64(buf.Len()) > max {
return nil, fmt.Errorf("input too large")
}
return buf.Bytes(), nil
}
边界处理比最短代码更重要。
小结
io.Reader 和 io.Writer 是 Go 标准库最重要的抽象之一。它们让函数不依赖具体文件、网络连接或内存对象,只依赖“能读”和“能写”这两个能力。io.Copy 把二者连接起来,bytes.Buffer 和 strings.Reader 让测试变得简单。
写 Go 程序时,如果一个函数要处理输入,先考虑接收 io.Reader;如果要输出内容,先考虑接收 io.Writer。这样代码会更通用、更容易测试,也更贴近标准库风格。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。