第 18 章 / 共 20 章
文件与 IO
os / bufio / io 接口家族、权限、临时文件、原子写
一次性读写整文件
对几 MB 以内的小文件,直接 ReadFile / WriteFile 最省事——Go 帮你处理 Open / Read / Close 整套流程。
package main
import (
"log"
"os"
)
func main() {
if err := os.WriteFile("out.txt", []byte("hello\n"), 0o644); err != nil {
log.Fatal(err)
}
data, err := os.ReadFile("out.txt")
if err != nil {
log.Fatal(err)
}
os.Stdout.Write(data)
}os.OpenFile 与权限位
需要追加、控制读写模式或自定义权限时,用 os.OpenFile。Linux 权限位最常用:0o644(owner 读写,其他只读)、0o600(仅 owner 读写)、0o755(目录或可执行)。Windows 下权限位会被忽略,写文件用 0o644 即可。
package main
import (
"fmt"
"log"
"os"
)
func main() {
f, err := os.OpenFile("app.log",
os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
if err != nil {
log.Fatal(err)
}
defer f.Close()
fmt.Fprintln(f, "boot ok")
// 常用 flag 组合:
// O_RDONLY | O_RDWR | O_WRONLY 访问模式
// O_CREATE 不存在则创建
// O_TRUNC 存在则清空
// O_APPEND 写入时追加
// O_EXCL 配合 O_CREATE,存在则报错(防止覆盖)
}按行读取大文件
bufio.Scanner 一次只持有一行,适合扫几百 MB 的日志。默认行长上限 64KB,超长行需要用 Buffer 扩。
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
f, err := os.Open("big.log")
if err != nil {
log.Fatal(err)
}
defer f.Close()
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 1024*1024), 16*1024*1024) // 16MB 单行上限
for sc.Scan() {
line := sc.Text()
fmt.Println(line)
}
if err := sc.Err(); err != nil {
log.Fatal(err)
}
}io.Reader / io.Writer 家族
Go 用接口抽象 IO:任何能读的实现 io.Reader,能写的实现 io.Writer。标准库的核心 IO 函数都接这两个接口,所以同一段代码能处理文件、网络、内存、压缩流。
package main
import (
"fmt"
"io"
"log"
"os"
"strings"
)
func main() {
// 准备一个源文件
_ = os.WriteFile("a.bin", []byte("hello world"), 0o644)
src, err := os.Open("a.bin")
if err != nil {
log.Fatal(err)
}
defer src.Close()
dst, err := os.Create("b.bin")
if err != nil {
log.Fatal(err)
}
defer dst.Close()
n, _ := io.Copy(dst, src)
fmt.Println("copied", n)
// 内存里造一个 Reader
r := strings.NewReader("hello")
buf, _ := io.ReadAll(r)
fmt.Println(string(buf))
// 同时写文件 + 终端
mw := io.MultiWriter(dst, os.Stdout)
fmt.Fprintln(mw, "broadcast")
// 限流读取
lr := io.LimitReader(strings.NewReader(strings.Repeat("x", 100)), 10)
out, _ := io.ReadAll(lr)
fmt.Println(len(out)) // 10
}缓冲与 Flush
频繁的小写入要包一层 bufio.Writer,能把上百次系统调用合并成几次。注意写完一定要 Flush——否则缓冲里的数据可能没落盘。
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
f, err := os.Create("out.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
bw := bufio.NewWriter(f)
defer bw.Flush() // 重要:不 Flush 数据可能没落盘
for i := 0; i < 10000; i++ {
fmt.Fprintln(bw, "line", i)
}
}临时文件与临时目录
package main
import (
"fmt"
"log"
"os"
)
func main() {
// 临时文件,文件名带随机后缀
f, err := os.CreateTemp("", "upload-*.bin")
if err != nil {
log.Fatal(err)
}
defer os.Remove(f.Name())
defer f.Close()
// 临时目录
dir, err := os.MkdirTemp("", "build-")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(dir)
fmt.Println(f.Name(), dir)
}原子写:写临时再 rename
直接覆盖目标文件,进程中途崩溃会留下半截文件。标准做法是写到同目录的临时文件,flush 后 rename——同分区 rename 在大多数文件系统上是原子操作。
package main
import (
"fmt"
"os"
"path/filepath"
)
func atomicWrite(path string, data []byte) error {
dir := filepath.Dir(path)
f, err := os.CreateTemp(dir, ".tmp-*")
if err != nil {
return err
}
tmp := f.Name()
defer os.Remove(tmp) // 失败时清理
if _, err := f.Write(data); err != nil {
f.Close()
return err
}
if err := f.Sync(); err != nil { // 落盘
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
return os.Rename(tmp, path)
}
func main() {
if err := atomicWrite("config.json", []byte(`{"v":1}`)); err != nil {
fmt.Println("err:", err)
return
}
fmt.Println("written safely")
}path/filepath 与目录遍历
filepath 自动适配 Windows / POSIX 的路径分隔符。WalkDir(Go 1.16+)比早期 Walk 更快,因为它复用目录条目的元数据,不会为每个文件再 Stat 一次。
package main
import (
"fmt"
"io/fs"
"log"
"path/filepath"
)
func main() {
p := filepath.Join("data", "users", "list.json")
fmt.Println(filepath.Dir(p), filepath.Base(p), filepath.Ext(p))
abs, _ := filepath.Abs(p)
fmt.Println(abs)
fmt.Println(filepath.Clean("a/./b/../c")) // a/c
matches, _ := filepath.Glob("*.go")
fmt.Println(matches)
err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && d.Name() == ".git" {
return filepath.SkipDir
}
if !d.IsDir() {
fmt.Println(path)
}
return nil
})
if err != nil {
log.Fatal(err)
}
}目录管理与判断存在
package main
import (
"errors"
"fmt"
"io/fs"
"log"
"os"
)
func main() {
if err := os.MkdirAll("data/cache", 0o755); err != nil {
log.Fatal(err)
}
path := "data/cache"
if info, err := os.Stat(path); err == nil {
fmt.Println("size", info.Size(), "mode", info.Mode())
} else if errors.Is(err, fs.ErrNotExist) {
fmt.Println("missing")
}
// 重命名 / 移动 / 删除
_ = os.WriteFile("a.txt", []byte("x"), 0o644)
_ = os.Rename("a.txt", "b.txt")
_ = os.Remove("b.txt")
_ = os.RemoveAll("data/cache") // 递归删除整目录
}embed:把文件编进二进制
Go 1.16+ 通过 //go:embed 指令,把静态资源一起打进可执行文件。部署单二进制时非常实用。
package main
import (
"embed"
"io/fs"
"log"
"net/http"
)
//go:embed assets/* templates/*.html
var staticFS embed.FS
func main() {
sub, _ := fs.Sub(staticFS, "assets")
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
log.Fatal(http.ListenAndServe(":8080", nil))
}