文件 I/O:Go 的文件操作全攻略

全面讲解Go语言文件I/O操作,从基础读写、缓冲IO到高级零拷贝和内存映射技术,覆盖大文件处理、日志轮转、CSV解析等实战场景

文件 I/O:Go 的文件操作全攻略

几乎所有程序都需要和文件打交道——读配置文件、写日志、处理用户上传的图片、导出数据到 CSV……文件操作是编程中最基础也最实用的技能之一。

Go 语言在文件 I/O 方面设计得非常优雅。它基于 Unix 哲学——“一切皆文件”,用统一的接口(io.Reader 和 io.Writer)处理各种 I/O 操作。这意味着你学会了文件读写,也就学会了网络通信、内存缓冲等等。

今天我们就来全面学习 Go 的文件操作,从最简单的读写开始,一直到高级的缓冲、临时文件、文件监控等话题。

读取文件

一次性读取整个文件

对于小文件,最简单的方式是一次性读取所有内容:

package main

import (
	"fmt"
	"os"
)

func main() {
	// ReadFile 一次性读取整个文件(Go 1.16+)
	data, err := os.ReadFile("example.txt")
	if err != nil {
		fmt.Println("读取失败:", err)
		return
	}
	fmt.Println(string(data))
}

注意:os.ReadFile 会把整个文件加载到内存中。对于大文件(比如几个 GB 的日志文件),这种方式会让内存爆炸。

逐行读取

处理大文件时,更常见的方式是逐行读取:

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	file, err := os.Open("example.txt")
	if err != nil {
		fmt.Println("打开文件失败:", err)
		return
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	lineNum := 0
	for scanner.Scan() {
		lineNum++
		line := scanner.Text()
		fmt.Printf("%4d: %s\n", lineNum, line)
	}

	if err := scanner.Err(); err != nil {
		fmt.Println("读取错误:", err)
	}
}

bufio.Scanner 是一个行扫描器,默认按换行符分割。它内部使用缓冲区,性能很好。

按字节块读取

对于非常大的文件或者需要精细控制的场景,可以按块读取:

package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	file, err := os.Open("example.txt")
	if err != nil {
		fmt.Println("打开文件失败:", err)
		return
	}
	defer file.Close()

	buf := make([]byte, 1024)  // 1KB 缓冲区
	totalBytes := 0

	for {
		n, err := file.Read(buf)
		if err != nil {
			if err == io.EOF {
				break  // 文件结束
			}
			fmt.Println("读取错误:", err)
			return
		}
		totalBytes += n
		// 处理 buf[:n] 中的数据
	}

	fmt.Printf("总共读取了 %d 字节\n", totalBytes)
}

随机读取(Seek)

有时候你需要跳到文件的特定位置读取:

file, _ := os.Open("example.txt")
defer file.Close()

// 跳到文件开头后 100 字节的位置
file.Seek(100, io.SeekStart)

// 从当前位置向后跳 50 字节
file.Seek(50, io.SeekCurrent)

// 从文件末尾向前 20 字节
file.Seek(-20, io.SeekEnd)

buf := make([]byte, 10)
file.Read(buf)

Seek 的三个常量:

  • io.SeekStart:从文件开头算起
  • io.SeekCurrent:从当前位置算起
  • io.SeekEnd:从文件末尾算起

写入文件

一次性写入

package main

import (
	"fmt"
	"os"
)

func main() {
	content := []byte("Hello, Go!\n这是第二行\n")

	// WriteFile 一次性写入,权限 0644
	err := os.WriteFile("output.txt", content, 0644)
	if err != nil {
		fmt.Println("写入失败:", err)
	}
}

注意:os.WriteFile 会覆盖已有文件的内容。

追加写入

file, err := os.OpenFile("output.txt", os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
	fmt.Println("打开文件失败:", err)
	return
}
defer file.Close()

file.WriteString("追加的内容\n")

文件打开模式

os.OpenFile 的第二个参数是标志位,可以用 | 组合:

标志说明
os.O_RDONLY只读
os.O_WRONLY只写
os.O_RDWR读写
os.O_CREATE如果文件不存在则创建
os.O_APPEND追加模式
os.O_TRUNC打开时截断文件
os.O_EXCL和 O_CREATE 一起使用,文件存在则报错

使用缓冲写入

频繁的磁盘写入性能很差。用 bufio.Writer 可以缓冲写入,提升性能:

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	file, err := os.Create("output.txt")
	if err != nil {
		fmt.Println("创建文件失败:", err)
		return
	}
	defer file.Close()

	writer := bufio.NewWriter(file)
	defer writer.Flush()  // 重要!确保缓冲区内容写入磁盘

	for i := 0; i < 1000; i++ {
		fmt.Fprintf(writer, "第 %d 行\n", i+1)
	}

	// Flush 在 defer 中调用,确保所有数据都写入
}

重要:使用 bufio.Writer 时,一定要在合适的时候调用 Flush(),否则缓冲区中的数据可能不会被写入磁盘。

文件信息

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	info, err := os.Stat("example.txt")
	if err != nil {
		if os.IsNotExist(err) {
			fmt.Println("文件不存在")
		} else {
			fmt.Println("获取信息失败:", err)
		}
		return
	}

	fmt.Printf("文件名: %s\n", info.Name())
	fmt.Printf("大小: %d 字节\n", info.Size())
	fmt.Printf("权限: %v\n", info.Mode())
	fmt.Printf("修改时间: %v\n", info.ModTime().Format(time.RFC3339))
	fmt.Printf("是否目录: %v\n", info.IsDir())
}

检查文件是否存在

Go 没有直接的 exists() 函数,但可以通过 os.Stat 判断:

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return !os.IsNotExist(err)
}

func isDir(path string) bool {
	info, err := os.Stat(path)
	if err != nil {
		return false
	}
	return info.IsDir()
}

目录操作

创建目录

// 创建单个目录
os.Mkdir("mydir", 0755)

// 递归创建目录(包括所有不存在的父目录)
os.MkdirAll("a/b/c/d", 0755)

列出目录内容

package main

import (
	"fmt"
	"os"
)

func main() {
	entries, err := os.ReadDir(".")
	if err != nil {
		fmt.Println("读取目录失败:", err)
		return
	}

	for _, entry := range entries {
		info, _ := entry.Info()
		if entry.IsDir() {
			fmt.Printf("DIR %s\n", entry.Name())
		} else {
			fmt.Printf("FILE %s (%d bytes)\n", entry.Name(), info.Size())
		}
	}
}

遍历目录

filepath.Walk 和 filepath.WalkDir(Go 1.16+)可以递归遍历目录:

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

func main() {
	root := "./myproject"

	err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return err
		}

		if d.IsDir() {
			fmt.Printf("DIR %s\n", path)
		} else {
			info, _ := d.Info()
			fmt.Printf("FILE %s (%d bytes)\n", path, info.Size())
		}

		return nil
	})

	if err != nil {
		fmt.Println("遍历失败:", err)
	}
}

路径处理

path/filepath 包提供了跨平台的路径处理工具:

package main

import (
	"fmt"
	"path/filepath"
)

func main() {
	// 拼接路径(跨平台)
	path := filepath.Join("home", "user", "documents", "file.txt")
	fmt.Println(path)  // home/user/documents/file.txt(Linux/macOS)
	                   // home\user\documents\file.txt(Windows)

	// 拆分路径
	dir, file := filepath.Split("/home/user/file.txt")
	fmt.Printf("dir: %s, file: %s\n", dir, file)

	// 获取扩展名
	ext := filepath.Ext("image.jpg")
	fmt.Println(ext)  // .jpg

	// 获取绝对路径
	abs, _ := filepath.Abs("./file.txt")
	fmt.Println(abs)

	// 匹配通配符
	matched, _ := filepath.Match("*.go", "main.go")
	fmt.Println(matched)  // true

	// 清理路径
	cleaned := filepath.Clean("./a/../b/./c")
	fmt.Println(cleaned)  // b/c
}

小贴士:始终使用 filepath.Join 来拼接路径,而不是用字符串拼接。这样可以保证跨平台兼容性。

复制、移动和删除文件

复制文件

func copyFile(src, dst string) error {
	srcFile, err := os.Open(src)
	if err != nil {
		return err
	}
	defer srcFile.Close()

	dstFile, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer dstFile.Close()

	_, err = io.Copy(dstFile, srcFile)
	return err
}

移动/重命名文件

os.Rename("old.txt", "new.txt")

删除文件

// 删除单个文件
os.Remove("file.txt")

// 递归删除目录及其内容
os.RemoveAll("mydir")

临时文件

os 包和 io/ioutil 包提供了创建临时文件的便捷方式:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 创建临时文件
	tmpFile, err := os.CreateTemp("", "example-*.txt")
	if err != nil {
		fmt.Println("创建临时文件失败:", err)
		return
	}
	defer os.Remove(tmpFile.Name())  // 用完删除
	defer tmpFile.Close()

	fmt.Println("临时文件:", tmpFile.Name())

	tmpFile.WriteString("一些临时数据")

	// 创建临时目录
	tmpDir, err := os.MkdirTemp("", "example-*")
	if err != nil {
		fmt.Println("创建临时目录失败:", err)
		return
	}
	defer os.RemoveAll(tmpDir)

	fmt.Println("临时目录:", tmpDir)
}

实战:文本文件分析器

让我们把所有知识综合起来,写一个文本文件分析工具:

package main

import (
	"bufio"
	"fmt"
	"os"
	"sort"
	"strings"
	"unicode"
)

// FileStats 文件统计信息
type FileStats struct {
	Lines       int
	Words       int
	Chars       int
	Bytes       int
	TopWords    []WordCount
}

type WordCount struct {
	Word  string
	Count int
}

func analyzeFile(path string) (*FileStats, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	info, err := file.Stat()
	if err != nil {
		return nil, err
	}

	stats := &FileStats{
		Bytes: int(info.Size()),
	}

	wordCounts := make(map[string]int)
	scanner := bufio.NewScanner(file)

	for scanner.Scan() {
		stats.Lines++
		line := scanner.Text()
		stats.Chars += len([]rune(line))

		// 分词(简单版)
		words := strings.FieldsFunc(line, func(r rune) bool {
			return !unicode.IsLetter(r) && !unicode.IsDigit(r)
		})

		for _, word := range words {
			stats.Words++
			word = strings.ToLower(word)
			wordCounts[word]++
		}
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	// 统计 Top 10 单词
	var wc []WordCount
	for word, count := range wordCounts {
		wc = append(wc, WordCount{word, count})
	}
	sort.Slice(wc, func(i, j int) bool {
		return wc[i].Count > wc[j].Count
	})

	limit := 10
	if len(wc) < limit {
		limit = len(wc)
	}
	stats.TopWords = wc[:limit]

	return stats, nil
}

func main() {
	if len(os.Args) < 2 {
		fmt.Println("用法: analyzer <文件路径>")
		os.Exit(1)
	}

	path := os.Args[1]
	stats, err := analyzeFile(path)
	if err != nil {
		fmt.Println("分析失败:", err)
		os.Exit(1)
	}

	fmt.Printf("\n文件分析: %s\n", path)
	fmt.Println(strings.Repeat("-", 40))
	fmt.Printf("字节数: %d\n", stats.Bytes)
	fmt.Printf("字符数: %d\n", stats.Chars)
	fmt.Printf("单词数: %d\n", stats.Words)
	fmt.Printf("行数:   %d\n", stats.Lines)

	fmt.Printf("\n出现最多的单词:\n")
	for i, wc := range stats.TopWords {
		fmt.Printf("%2d. %-20s %d 次\n", i+1, wc.Word, wc.Count)
	}
}

小结

今天我们全面学习了 Go 的文件 I/O 操作:

  1. 读取:ReadFile、Scanner、Read、Seek
  2. 写入:WriteFile、WriteString、缓冲写入
  3. 文件信息:Stat、权限、修改时间
  4. 目录操作:创建、列出、遍历
  5. 路径处理:filepath 包的跨平台工具
  6. 临时文件:CreateTemp、MkdirTemp

Go 的文件 I/O 基于统一的 io.Reader 和 io.Writer 接口,这让文件操作和网络操作、内存操作等使用同样的模式,学习曲线很平滑。

练习时间

  1. 日志文件分析器:统计日志文件中各级别(INFO/WARN/ERROR)的数量
  2. 文件备份工具:实现一个工具,把指定目录下的所有 .go 文件打包备份
  3. CSV 处理:读取 CSV 文件,统计某一列的数据分布
  4. 目录树打印:用递归打印一个漂亮的目录树(类似 tree 命令)
  5. 文件比较器:比较两个文件是否内容相同,如果不同,输出差异位置

进阶概念

文件描述符

Go 中 os.File 封装了底层的文件描述符(file descriptor, fd)。每个打开的文件在操作系统中都有一个唯一的 fd 号。理解 fd 有助于定位资源泄漏问题。

import "syscall"

func showFD(file *os.File) {
	// 获取底层文件描述符
	rawConn, _ := file.SyscallConn()
	rawConn.Control(func(fd syscall.RawConn) {
		// fd 可用于高级操作
	})
}

文件描述符在 Linux 系统上有数量限制(ulimit -n),大规模高并发服务要注意避免 fd 泄漏。每个 open/create 操作都会占用一个 fd,必须对应 Close 或让 GC 回收。

缓冲 IO 原理

bufio.Reader/Writer 的核心思想是减少系统调用次数:

// bufio.Reader 内部结构
type Reader struct {
    buf          []byte      // 内部缓冲区
    rd           io.Reader   // 底层 reader
    r, w         int         // buf[r:w] 是当前有效数据
    err          error
    lastByte     int
    lastRuneSize int
}

读取时不是每次向内核请求 1 字节,而是一次性读取 4KB(默认),后续读操作从内存缓冲区获取。这让连续读取小块数据的性能提升数十倍。

// 对比有无缓冲的性能
func benchmarkBuffered() {
	file, _ := os.Open("1mb.txt")
	defer file.Close()

	// 无缓冲:每次读 1 字节
	buf := make([]byte, 1)
	for {
		_, err := file.Read(buf)
		if err == io.EOF { break }
	}

	// 有缓冲:内部 4KB 缓存
	reader := bufio.NewReader(file)
	for {
		_, err := reader.Read(buf)
		if err == io.EOF { break }
	}
}

零拷贝(sendfile)

零拷贝技术避免了数据在用户空间和内核空间之间不必要的复制。Go 中的 io.Copy 在某些情况下(源和目标都是 TCP 连接或文件时会使用 sendfile 系统调用)可以利用零拷贝:

func zeroCopyExample() {
	src, _ := os.Open("big.file")
	defer src.Close()

	// 如果 dst 是 net.TCPConn,io.Copy 内部可能使用 sendfile
	dst, _ := net.Dial("tcp", "server:80")
	defer dst.Close()

	written, _ := io.Copy(dst, src)
	fmt.Println(written)
}

零拷贝的原理:内核直接把数据从文件缓存传递给 socket 缓冲区,不经过用户空间。这在传输大文件时能显著减少 CPU 使用和内存带宽占用。

内存映射 mmap

syscall 包和第三方库(如 golang.org/x/exp/mmap)提供内存映射功能,让文件内容像内存一样访问:

import "golang.org/x/exp/mmap"

func mmapExample() error {
	reader, err := mmap.Open("data.bin")
	if err != nil {
		return err
	}
	defer reader.Close()

	// 直接通过内存地址访问文件内容
	data := make([]byte, 1024)
	_, err = reader.ReadAt(data, 0)
	return err
}

mmap 将文件映射到进程的虚拟地址空间。操作系统只在真正访问某页时才从磁盘加载(按需分页)。对于超大文件和随机访问场景,mmap 比普通 read/write 更高效。

OS 页缓存

操作系统会自动将最近访问的文件数据缓存在内存中(Page Cache)。这解释了为什么同一文件第二次读取快很多:直接从内存页缓存获取,无需磁盘 I/O。

// 第一次读取(冷缓存,从磁盘装载)
data1, _ := os.ReadFile("big.log")

// 第二次读取(热缓存,从内存读取)
data2, _ := os.ReadFile("big.log")
// 第二次通常快 10-100 倍

了解页缓存的存在,有助于理解为什么"清除缓存后测试"(echo 3 > /proc/sys/vm/drop_caches)对 I/O 基准测试很重要。

实战案例

大文件分块读取

对于日志分析、数据导入等场景,分块读取是标准策略:

package main

import (
	"bufio"
	"fmt"
	"os"
)

func processLargeFile(path string, chunkSize int64) error {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	chunkNum := 0
	for {
		chunkNum++
		buf := make([]byte, chunkSize)
		n, err := file.Read(buf)
		if n == 0 {
			break
		}

		// 处理当前块
		fmt.Printf("处理块 %d, 大小 %d bytes\n", chunkNum, n)
		processChunk(buf[:n])

		if err != nil {
			break
		}
	}
	return nil
}

func processChunk(data []byte) {
	// 实际业务处理逻辑
}

func streamProcess(path string, handler func([]byte) error) error {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	// 增加扫描缓冲区以支持大段内容
	const maxCapacity = 64 * 1024
	buf := make([]byte, maxCapacity)
	scanner.Buffer(buf, maxCapacity)

	for scanner.Scan() {
		if err := handler(scanner.Bytes()); err != nil {
			return err
		}
	}
	return scanner.Err()
}

日志轮转实现

生产环境中日志文件需要定期轮换,避免单个文件过大:

package main

import (
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"
)

type RotateWriter struct {
	mu       sync.Mutex
	file     *os.File
	path     string
	maxSize  int64
	maxAge   time.Duration
	currSize int64
}

func NewRotateWriter(path string, maxSize int64, maxAge time.Duration) (*RotateWriter, error) {
	w := &RotateWriter{
		path:    path,
		maxSize: maxSize,
		maxAge:  maxAge,
	}
	if err := w.rotate(); err != nil {
		return nil, err
	}
	return w, nil
}

func (w *RotateWriter) Write(p []byte) (n int, err error) {
	w.mu.Lock()
	defer w.mu.Unlock()

	if w.currSize+int64(len(p)) > w.maxSize {
		if err := w.rotate(); err != nil {
			return 0, err
		}
	}

	n, err = w.file.Write(p)
	w.currSize += int64(n)
	return n, err
}

func (w *RotateWriter) rotate() error {
	if w.file != nil {
		w.file.Close()
		// 重命名为带时间戳的旧文件
		backup := fmt.Sprintf("%s.%s", w.path, time.Now().Format("20060102_150405"))
		os.Rename(w.path, backup)
	}

	file, err := os.OpenFile(w.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		return err
	}

	info, _ := file.Stat()
	w.file = file
	w.currSize = info.Size()
	return nil
}

func main() {
	writer, _ := NewRotateWriter("app.log", 1024*1024, 7*24*time.Hour)
	for i := 0; i < 100000; i++ {
		fmt.Fprintf(writer, "[%s] 日志记录 %d\n",
			time.Now().Format(time.RFC3339), i)
	}
}

CSV 大批量解析

package main

import (
	"encoding/csv"
	"fmt"
	"os"
)

func parseCSVBatch(path string, batchSize int) error {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	reader := csv.NewReader(file)

	// 读取 header
	headers, err := reader.Read()
	if err != nil {
		return err
	}
	fmt.Printf("列: %v\n", headers)

	var batch [][]string
	for {
		record, err := reader.Read()
		if err != nil {
			if err.Error() == "EOF" {
				break
			}
			// 跳过错误行继续处理
			continue
		}

		batch = append(batch, record)
		if len(batch) >= batchSize {
			processBatch(batch)
			batch = batch[:0]
		}
	}

	// 处理剩余
	if len(batch) > 0 {
		processBatch(batch)
	}
	return nil
}

func processBatch(records [][]string) {
	fmt.Printf("处理批次, 数量: %d\n", len(records))
	// 实际入库等操作
}

性能对比:各 IO 方式吞吐量对比表

I/O 方式适合场景吞吐量内存占用代码复杂度
os.ReadFile小文件(< 10MB)高(直接读取)高(全载内存)低
bufio.Scanner文本文件逐行读取中高低(固定缓冲区)低
bufio.Reader(手动块读)大文件流式处理高可控中
mmap(内存映射)超大文件随机访问极高低(按需分页)中高
sendfile(零拷贝)文件转网络极高几乎为零低(io.Copy自动)
io.ReadAll不确定大小的读取中等高(动态扩容)低

FAQ

Q1: os.ReadFile 和 ioutil.ReadFile 有什么区别?

A: Go 1.16 将 ioutil.ReadFile 标记为弃用,推荐使用 os.ReadFile。两者功能完全相同,但 os.ReadFile 位于更合理的包中。

Q2: defer file.Close() 放在哪里最好?

A: 应该在 os.Open() 后立即关闭,避免后续代码有提前 return 路径时漏关。但要先检查 err,如果打开失败就不需要关闭:

file, err := os.Open(path)
if err != nil {
    return err
}
defer file.Close() // 成功打开后才 defer

Q3: bufio.Writer 的 Flush 何时调用最合适?

A: 最佳实践是立即 defer,或者在固定写入周期后主动 Flush。对于长时间运行的程序,不能只依赖 defer,因为 defer 只在函数返回时执行:

writer := bufio.NewWriter(file)
defer writer.Flush()

// 重要:长循环中定期 flush
for i, line := range lines {
    writer.WriteString(line)
    if i % 1000 == 0 {
        writer.Flush()
    }
}

Q4: 如何处理 GB 级超大文件?

A: 永远不要一次性读取。推荐采用流式处理策略:按行用 Scanner,按块用 Read(buf)。如果数据格式允许,还可以考虑并发分片处理(每个 goroutine 处理文件的不同段)。

Q5: Scanner 的默认 64KB 行限制怎么解决?

A: 使用 scanner.Buffer() 方法增大缓冲区:

scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024) // 允许最大 1MB 行

Q6: 如何判断文件正在被其他进程写入?

A: 没有跨平台的原子方法。常见方案是用文件锁(syscall.Flock 在 Unix 上)或约定机制(写临时文件完成后重命名)。

Q7: filepath.Walk 和 WalkDir 有什么区别?

A: Go 1.16 引入的 WalkDir 更高效。Walk 对每个文件都调用 os.Lstat 获取文件信息。WalkDir 改用 os.Stat,在需要文件信息时才获取。对于目录树遍历,WalkDir 通常更快。

Q8: 跨平台路径分隔符怎么处理?

A: 始终使用 path/filepath 包的各种函数(Join、Split、Dir、Base),不要手动拼接字符串或用 / 硬编码路径。filepath 会自动处理 Windows 的 \ 和 Unix 的 /。

最佳实践

defer close 顺序

defer 遵循 LIFO(后进先出)顺序。如果有多个资源需要关闭,要注意依赖关系:

func processFile(path string) error {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	reader := bufio.NewReader(file)
	// 不需要 defer reader,它只持有 file 引用

	// 如果创建了解码器
	gz, err := gzip.NewReader(reader)
	if err != nil {
		return err
	}
	defer gz.Close() // 先关闭 gz,再关闭 file

	// 读取并处理
	_, err = io.ReadAll(gz)
	return err
} // defer 执行顺序:gz.Close() -> file.Close()

大文件处理策略

  1. 流式处理:始终优先使用 Reader/Scanner,避免 ReadAll
  2. 分片并行:对于无关联性的记录(如日志行),可以按文件位置切片,多 goroutine 并行处理
  3. 内存映射:超大文件需要快速随机访问时,使用 mmap
  4. 定期 Flush:写入时根据写入频率决定 Flush 策略,平衡安全性和性能
  5. 不要忽略错误:scanner.Err() 在文件损坏或有编码问题时可能非 nil

延伸阅读

我们下篇见!


参考资料:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练