Skip to content

《Go Cookbook CN》系列 07:常规输入输出全解析

约 2934 字大约 10 分钟

《Go Cookbook CN》系列Go语言

2026-04-02

本文聚焦Go语言中常规输入输出(I/O)的核心操作,从io包的基础接口出发,系统讲解输入读取、输出写入、数据复制、文本文件读写及临时文件使用的全流程实操方法。无论你是Go语言入门学习者,还是需要夯实I/O基础的开发人员,学完本文都能熟练掌握Go常规I/O的核心用法,解决日常开发中的数据读写场景问题。

【本篇核心收获】

  • 掌握Go语言io包中ReaderWriter核心接口的定义、方法签名及数据流转逻辑
  • 熟练运用io.ReadAllstrings.NewReader等工具完成输入数据的读取操作
  • 掌握io.Writer接口的使用方式,以及bytes.Bufferhttp.ResponseWriter等典型应用场景
  • 理解io.Copy函数的优势,能对比常规读写与Copy方式的性能差异并落地最优方案
  • 掌握文本文件“一次性读写”和“分步读写”两种模式,以及临时文件/目录的创建与清理方法

7.0 概述

输入和输出(I/O)是计算机与外部世界通信的核心方式,常见输入源包括键盘、鼠标、摄像头等,输出则涵盖屏幕显示、打印、网络传输等场景。I/O是软件开发的关键环节,Go语言通过标准库提供了完善的I/O处理能力,其中io包是基础核心。

io包的核心是一系列接口,最常用的是ReaderWriter,还有ReadWriterTeeReaderWriterTo等变体。这些接口本质是函数描述符:实现某接口的结构体必须包含对应的方法(如Reader需实现Read方法,WriterTo需实现WriteTo方法),部分接口是多个基础接口的组合(如ReadWriter同时包含ReadWrite方法)。

模块小结:本模块梳理了Go I/O的核心定位及io包的接口设计逻辑,是后续所有I/O操作的基础认知。

7.1 从输入读取

7.1.1 核心问题

如何从输入数据流中读取数据?

7.1.2 解决方案

基于io.Reader接口实现输入数据的读取。

7.1.3 实操解析

Go语言通过io.Reader接口定义“从输入流读取数据”的能力,其核心方法签名如下:

type Reader interface {
    Read(p []byte) (n int, err error)
}

只要结构体实现了上述Read方法,就属于Reader类型。

基础读取方式

读取数据需先创建字节切片,将其传入Read方法,数据会从reader流入字节切片:

bytes := make([]byte, 1024)
reader.Read(bytes)

⚠️ 注意:数据流向是reader → bytes,而非直觉上的反向,需重点区分。

读取全部内容

Read方法仅填充字节切片至其容量,若需读取reader中所有内容,推荐使用io.ReadAll函数:

bytes, err := io.ReadAll(reader)

该方式更直观,数据从reader全量流入bytes,无需关注切片容量限制。

字符串转Reader

若需将字符串传递给“接收Reader作为参数”的函数,可通过strings.NewReader创建Reader实例:

str := "My String Data"  
reader := strings.NewReader(str)

模块小结:本模块讲解了io.Reader接口的定义与三种核心使用场景,重点需掌握数据流向逻辑及io.ReadAllstrings.NewReader的实操方法。

7.2 写入到输出

7.2.1 核心问题

如何将数据写入到输出数据流中?

7.2.2 解决方案

基于io.Writer接口实现输出数据的写入。

7.2.3 实操解析

io.Writer接口定义“向输出流写入数据”的能力,核心方法签名如下:

type Writer interface {
    Write(p []byte) (n int, err error)
}

调用Write方法时,字节数据会从传入的切片流入writer对应的底层数据流:

bytes := []byte("Hello World")
writer.Write(bytes)

⚠️ 注意:Writer的数据流向是bytes → writer,与Reader完全相反,需严格区分。

典型应用模式

Go中常见“函数接收Writer作为参数”的设计模式,以bytes.Buffer为例:

var buf bytes.Buffer
fmt.Fprintf(&buf, "Hello %s", "World")
s := buf.String() // s == "Hello World"

bytes.Buffer实现了io.Writer接口,fmt.Fprintf将格式化数据写入缓冲区,后续可从缓冲区提取数据。

另一个典型场景是HTTP处理器中的http.ResponseWriter

func myHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello World"))
}

写入到http.ResponseWriter的数据会被传递至浏览器,完成HTTP响应的输出。

模块小结:本模块讲解了io.Writer接口的定义、数据流向及两种典型实战场景,需掌握WriterReader的流向差异,以及核心API的使用方式。

7.3 从 Reader 复制到 Writer

7.3.1 核心问题

如何高效地将数据从Reader复制到Writer

7.3.2 解决方案

使用io.Copy函数实现ReaderWriter的直接复制,替代“先读取到缓冲区再写入”的低效方式。

7.3.3 实操与性能对比

常规读写方式(低效)

以下载文件为例,常规方式需先读取http.Response.Body(Reader)到字节切片,再写入文件:

// 使用随机1MB测试文件
var url string = "http://speedtestftp.otenet.gr/files/test1Mb.db"  

func readWrite() {  
    r, err := http.Get(url)  
    if err != nil {  
        log.Println("Cannot get from URL", err)  
    }  
    defer r.Body.Close()  
    data, err := io.ReadAll(r.Body)  
    if err != nil {
        log.Println("Cannot read body", err)
    }
    os.WriteFile("rw.data", data, 0755)  
}

基准测试结果(以darwin/arm64环境为例):

  • 耗时:约3.37秒
  • 内存占用:5.27MB
  • 内存分配次数:218次

io.Copy方式(高效)

io.Copy可直接从Reader读取并写入Writer,无需全量缓存到内存:

func copyFunc() {
    r, err := http.Get(url)
    if err != nil {
        log.Println("Cannot get from URL", err)
    }
    defer r.Body.Close()
    file, _ := os.Create("copy.data") // 创建文件
    defer file.Close()
    writer := bufio.NewWriter(file) // 创建带缓冲的Writer
    io.Copy(writer, r.Body)
    writer.Flush() // 刷新缓冲区,确保写入文件
}

基准测试结果(同环境):

  • 耗时:约1.61秒(速度提升约2倍)
  • 内存占用:43.2kB(仅为常规方式的1%以下)
  • 内存分配次数:62次

⚠️ 避坑指南:对于大文件,常规方式易耗尽内存,io.Copy是最优选择,需优先使用。

基准测试方法

创建测试文件,编写Benchmark函数:

import "testing"

func BenchmarkReadWrite(b *testing.B){
    readWrite()
}

func BenchmarkCopy(b *testing.B){
    copyFunc()
}

执行基准测试命令:

go test -bench=. -benchmem

模块小结:本模块对比了常规读写与io.Copy的性能差异,核心结论是io.Copy在速度和内存占用上优势显著,尤其适用于大文件传输场景。

7.4 从文本文件读取

7.4.1 核心问题

如何将文本文件的内容读取到内存中?

7.4.2 解决方案

有两种核心方式:os.ReadFile(一次性读取)、os.Open+Read(分步读取)。

7.4.3 实操解析

方式1:一次性读取所有内容(简单高效)

os.ReadFile可直接读取文件全量内容,无需手动打开/关闭文件:

data, err := os.ReadFile("data.txt")  
if err != nil {  
    log.Printf("Cannot read file: %v", err)  
}  
fmt.Printf(string(data))

示例中若data.txt内容为hello world!,执行后会直接打印该内容。

方式2:打开文件后分步读取(灵活可控)

分步读取需手动管理文件的打开、读取、关闭,适合按需读取部分内容的场景:

  1. 打开文件(只读模式),并通过defer确保关闭:

    // 打开文件
    file, err := os.Open("data.txt")
    if err != nil {
        log.Println("Cannot open file:", err)
    }
    // 函数结束前关闭文件
    defer file.Close()
  2. 获取文件大小,创建对应容量的字节切片:

    // 获取文件信息
    stat, err := file.Stat()
    if err != nil {
        log.Println("Cannot read file stats:", err)
    }
    // 创建存储数据的字节切片
    data := make([]byte, stat.Size())
  3. 读取文件内容到字节切片:

    // 读取文件
    bytes, err := file.Read(data)  
    if err != nil {  
        log.Printf("Cannot read file:", err)  
    }  
    fmt.Printf("Read %d bytes from file\n", bytes)  
    fmt.Printf(string(data))

执行后输出示例:

Read 13 bytes from file
hello world!

模块小结:本模块讲解了文本文件读取的两种方式,os.ReadFile适合简单场景,os.Open+Read适合灵活控制的场景,需根据实际需求选择。

7.5 写入到文本文件

7.5.1 核心问题

如何将数据写入到文本文件中?

7.5.2 解决方案

两种核心方式:os.WriteFile(一次性写入)、os.Create+Write(分步写入)。

7.5.3 实操解析

方式1:一次性写入文件(简单高效)

os.WriteFile可直接将字节数组写入文件,自动处理文件创建/覆盖:

data := []byte("Hello World!\n")  
err := os.WriteFile("data.txt", data, 0644)  
if err != nil {  
    log.Println("Cannot write to file:", err)  
}

⚠️ 注意:

  • 第三个参数为Unix文件权限(0644表示所有者可读可写,其他用户只读);
  • 若文件不存在则创建,若已存在则覆盖原有内容,权限保持不变。

方式2:创建文件后分步写入(灵活可控)

分步写入适合分块写入、写入前需操作文件的场景:

  1. 创建/打开文件(覆盖原有内容),并确保关闭:

    data := []byte("Hello World!\n")  
    // 创建/打开文件
    file, err := os.Create("data.txt")  
    if err != nil {  
        log.Println("Cannot create file:", err)  
    }  
    defer file.Close()
  2. 写入数据到文件,返回写入的字节数:

    bytes, err := file.Write(data)  
    if err != nil {  
        log.Printf("Cannot write to file:", err)  
    }  
    fmt.Printf("Wrote %d bytes to file\n", bytes)

模块小结:本模块讲解了文本文件写入的两种方式,需注意文件权限设置及覆盖写入的特性,根据场景选择一次性或分步写入。

7.6 使用临时文件

7.6.1 核心问题

如何创建临时文件/目录,并在程序结束后自动清理?

7.6.2 解决方案

使用os.CreateTemp创建临时文件、os.MkdirTemp创建临时目录,结合defer+os.Remove/os.RemoveAll实现自动清理。

7.6.3 实操解析

1. 查看系统临时目录

不同操作系统的临时目录路径不同,可通过os.TempDir获取:

fmt.Println(os.TempDir())

示例输出(macOS):

/var/folders/nj/2xd4ssp94zz41gnvsvyth38m0000gn/T/

Linux系统通常为/tmp,Windows系统为C:\Users\<UserName>\AppData\Local\Temp

2. 创建临时目录(可选)

若需批量管理临时文件,可先创建临时子目录,使用后整体删除:

tmpdir, err := os.MkdirTemp(os.TempDir(), "mytmpdir*")  
if err != nil {  
    log.Println("Cannot create temp directory:", err)  
}  
// 函数结束后删除临时目录及所有内容
defer os.RemoveAll(tmpdir)

⚠️ 注意:模式字符串中的*会被随机字符串替换,避免文件名冲突。

3. 创建临时文件

基于临时目录创建临时文件(也可直接使用系统临时目录):

tmpfile, err := os.CreateTemp(tmpdir, "mytmp_*")  
if err != nil {
    log.Println("Cannot create temp file:", err)  
}

4. 操作临时文件

临时文件的读写与普通文件一致:

data := []byte("Some random stuff for the temporary file")
_, err = tmpfile.Write(data)
if err != nil {
    log.Printf("Cannot write to temp file:", err)
}
err = tmpfile.Close()
if err != nil {
    log.Printf("Cannot close temp file:", err)
}

5. 自动清理临时文件

若未使用独立临时目录,可直接删除单个临时文件:

defer os.Remove(tmpfile.Name())

模块小结:本模块讲解了临时文件/目录的创建、使用与自动清理方法,核心是利用os.CreateTemp/os.MkdirTemp创建,结合defer实现自动清理,避免残留文件。

本篇核心知识点速记

  1. 核心接口:Reader(Read方法,数据→字节切片)、Writer(Write方法,字节切片→数据),二者数据流向相反;
  2. 输入读取:io.ReadAll读取全部内容,strings.NewReader将字符串转为Reader;
  3. 输出写入:bytes.Bufferhttp.ResponseWriterWriter的典型实现,fmt.Fprintf支持向Writer写入格式化数据;
  4. 数据复制:io.Copy比“读取+写入”更高效,内存占用仅为常规方式的1%以下,适合大文件;
  5. 文件读写:
    • 读取:os.ReadFile(一次性)、os.Open+Read(分步);
    • 写入:os.WriteFile(一次性)、os.Create+Write(分步);
  6. 临时文件:os.CreateTemp创建文件、os.MkdirTemp创建目录,defer+os.Remove/RemoveAll自动清理,模式字符串*避免命名冲突。