Go 文件处理¶
Go 的文件 API 主要位于 os、io、bufio、path/filepath 和 io/fs 包。
文件处理不仅是读写字节,还涉及资源关闭、权限、路径边界、部分写入和崩溃一致性。
对小配置文件可整体读取;对日志、上传和大型数据应采用流式处理。
打开、创建与关闭¶
os.Open 以只读方式打开文件,os.Create 会创建或截断文件并使用默认权限掩码。
需要明确控制标志和权限时使用 os.OpenFile。
f, err := os.Open("config.json")
if err != nil {
return fmt.Errorf("open config: %w", err)
}
defer f.Close()
成功打开资源后立即安排 defer,能覆盖后续所有返回路径。
循环中打开大量文件时,不要把 defer 堆积到外层函数结束;把单次处理提取为函数。
func process(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// 处理单个文件。
return nil
}
整体读取与流式读取¶
os.ReadFile 简洁但会把全部内容放入内存,适合大小已知且受控的小文件。
流式读取使用 io.Reader,可限制内存占用,也便于组合解压、哈希和协议解析。
data, err := os.ReadFile("settings.json")
if err != nil {
return fmt.Errorf("read settings: %w", err)
}
func copyWithLimit(dst io.Writer, src io.Reader, limit int64) error {
n, err := io.Copy(dst, io.LimitReader(src, limit+1))
if err != nil {
return fmt.Errorf("copy: %w", err)
}
if n > limit {
return fmt.Errorf("input exceeds %d bytes", limit)
}
return nil
}
差异示例 1:os.ReadFile 以简单性换取与文件大小相当的内存;io.Copy 以固定缓冲流式传输,更适合大文件。
对不可信输入设置上限,避免攻击者通过超大文件或无限流耗尽内存和磁盘。
bufio 与逐行处理¶
bufio.Scanner 适合逐行或按 token 读取,但默认 token 上限为 64 KiB 左右。
长行输入必须调用 Scanner.Buffer 调整上限,并检查 scanner.Err()。
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("scan file: %w", err)
}
需要精确保留分隔符、处理超长记录或更细粒度控制时,使用 bufio.Reader。
写入许多小块时使用 bufio.Writer,并务必检查 Flush 返回的错误。
写入、权限与关闭错误¶
Unix 风格权限 0o600 表示仅所有者可读写,0o644 表示所有者可写、其他用户只读。
实际权限还受进程 umask 和操作系统语义影响,Windows 不完整实现 Unix 权限模型。
func writePrivate(path string, data []byte) (err error) {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return fmt.Errorf("open %q: %w", path, err)
}
defer func() {
err = errors.Join(err, f.Close())
}()
if _, err := f.Write(data); err != nil {
return fmt.Errorf("write %q: %w", path, err)
}
return nil
}
差异示例 2:os.WriteFile 适合一次性写入小内容;os.OpenFile 加流式写入适合大内容并提供追加、独占创建等标志控制。
追加日志可使用 os.O_APPEND,避免多个写入者自行先 seek 到末尾造成覆盖,但仍要考虑一条记录是否会被拆分。
路径处理与安全边界¶
文件系统路径使用 path/filepath;URL 路径使用 path,两者不可混用。
filepath.Join 会清理分隔符,但不会自动保证结果仍位于预期根目录。
不要把用户输入直接拼到根目录,也不要仅靠检查字符串中是否含 ..。
func safePath(root, name string) (string, error) {
if filepath.IsAbs(name) {
return "", errors.New("absolute path is not allowed")
}
clean := filepath.Clean(name)
full := filepath.Join(root, clean)
rel, err := filepath.Rel(root, full)
if err != nil {
return "", fmt.Errorf("resolve path: %w", err)
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", errors.New("path escapes root")
}
return full, nil
}
这类词法检查不能完全阻止符号链接竞态。 高安全场景应使用操作系统提供的目录句柄和“不跟随符号链接”能力,或把不可信操作隔离到专用目录/进程。 上传文件名应重新生成,不能信任客户端给出的路径、扩展名或 MIME 类型。
原子写入概念¶
直接截断目标文件后写入,进程崩溃时可能留下空文件或半写文件。
常见做法是在目标同一目录创建临时文件,写入并检查错误,必要时 Sync,再用 os.Rename 替换目标。
func atomicWrite(path string, data []byte, perm fs.FileMode) (err error) {
dir := filepath.Dir(path)
tmp, err := os.CreateTemp(dir, ".tmp-*")
if err != nil {
return fmt.Errorf("create temp file: %w", err)
}
tmpName := tmp.Name()
defer func() {
_ = tmp.Close()
_ = os.Remove(tmpName)
}()
if err := tmp.Chmod(perm); err != nil {
return fmt.Errorf("set temp permission: %w", err)
}
if _, err := tmp.Write(data); err != nil {
return fmt.Errorf("write temp file: %w", err)
}
if err := tmp.Sync(); err != nil {
return fmt.Errorf("sync temp file: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close temp file: %w", err)
}
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("replace target: %w", err)
}
return nil
}
同目录临时文件通常确保 rename 不跨文件系统;rename 的覆盖语义在不同操作系统上仍有差异。 严格持久性还可能要求同步父目录,是否需要取决于操作系统、文件系统和业务耐久性要求。
常见错误与工程注意¶
- 不要忽略
Read返回的字节数;n > 0与非 nil 错误可以同时出现。 - 不要假设一次
Write必然写完全部数据;通用io.Writer允许短写,优先使用io.Copy或检查n。 - 不要忘记检查
Scanner.Err、Writer.Flush、关键写入的Close和Sync错误。 - 不要对大文件使用无上限的
io.ReadAll。 - 不要用
0777作为默认权限,敏感配置和密钥应从最小权限开始。 - 临时文件也可能包含敏感数据,应放在受控目录、设置权限并确保失败时删除。
- 先验证文件类型和大小,再解析不可信内容;压缩文件还要防止解压路径穿越和压缩炸弹。
- 文件名比较、大小写和符号链接行为依赖平台,跨平台程序必须在目标系统测试。
小结¶
小文件可用 os.ReadFile/WriteFile,大文件使用 io.Reader/io.Writer 流式处理。
资源成功打开后立即安排关闭,并检查关键写入链路中的刷新、关闭和同步错误。
权限、路径约束、输入上限与原子替换共同构成可靠文件处理,而不是附加选项。