Go: Go 文件 IO 与 JSON
最后更新:2026-08-26
文件 IO 和 JSON 是 Go 后端开发的两大基本功——os 包提供系统调用层面的文件操作,encoding/json 包让 Go 与 JavaScript 世界无缝互通。
Go 标准库的文件操作和 JSON 处理设计得巧妙统一:全部基于 io.Reader / io.Writer interface。这节课你将掌握文件 IO 和 JSON 处理的全部核心能力。
1. 你将学到
- os 文件操作(创建/打开/读取/写入)
os.ReadFile/os.WriteFile一次性读写- bufio 缓冲读写(逐行/大文件)
encoding/jsonMarshal / Unmarshal- JSON Encoder / Decoder 流式处理
- JSON tag 控制序列化行为
- 用文件 IO + JSON 构建数据库导出工具
2. 一个全栈工程师的真实故事
(1) 痛点:手动拼接 JSON string
Charlie 是全栈工程师,他需要把数据库中的用户数据导出为 JSON 文件供前端使用:
"我不想用第三方库,就用手动拼接 JSON string。结果用户名字段里有
"符号,JSON 格式直接炸了。100 万行数据导出了一半才报错——回滚又花了 2 小时。"
他打开自己写的代码:
// 坏代码:手动拼接 JSON 字符串
func exportUserJSON(users []User) string {
result := "["
for i, u := range users {
if i > 0 {
result += ","
}
// 手动拼接,双引号没转义
result += "{\"name\":\"" + u.Name + "\",\"age\":" + string(u.Age) + "}"
}
result += "]"
return result
}
如果 u.Name 包含 " 或 \,生成的 JSON 就坏了。而且 string(u.Age) 会把数字按 ASCII 转——28 变成 \x1c。
(2) Go 的解法:encoding/json + 文件 IO
// json_exporter.go
package main
import (
"encoding/json"
"fmt"
"os"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
City string `json:"city"`
}
func main() {
users := []User{
{Name: "Alice", Age: 28, City: "Shanghai"},
{Name: `Bob "The Builder"`, Age: 32, City: "Beijing"},
{Name: "Charlie", Age: 25, City: "Guangzhou"},
}
// JSON 序列化到文件(无需手动拼接)
file, _ := os.Create("users.json")
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
encoder.Encode(users)
fmt.Println("导出成功!")
// 验证:读取回来看
data, _ := os.ReadFile("users.json")
fmt.Println(string(data))
}
输出:
导出成功!
[
{
"name": "Alice",
"age": 28,
"city": "Shanghai"
},
{
"name": "Bob \"The Builder\"",
"age": 32,
"city": "Beijing"
},
{
"name": "Charlie",
"age": 25,
"city": "Guangzhou"
}
]
(3) 收益:JSON 处理对比
| 方式 | 双引号转义 | 特殊字符 | 大文件 | 代码量 |
|---|---|---|---|---|
| 手动拼接 | ❌ 手动转义 | ❌ 容易出错 | ❌ OOM | ~50 行 |
json.Marshal |
✅ 自动 | ✅ 自动 | ❌ 全量内存 | ~5 行 |
json.Encoder |
✅ 自动 | ✅ 自动 | ✅ 流式 | ~5 行 |
encoding/json package。它自动处理转义、编码、缩进,并且会正确处理 Go 的 UTF-8 字符。
3. os 文件操作
(1) 文件打开与创建
package main
import (
"fmt"
"os"
)
func main() {
// 创建(或截断)文件
f, _ := os.Create("test.txt")
defer f.Close()
// 写入字符串
f.WriteString("Hello, Go Files!\n")
f.Write([]byte("Second line\n"))
// 追加模式打开
f2, _ := os.OpenFile("test.txt", os.O_APPEND|os.O_WRONLY, 0644)
defer f2.Close()
f2.WriteString("Appended line\n")
// 读取文件
data, _ := os.ReadFile("test.txt")
fmt.Print(string(data))
}
输出:
Hello, Go Files!
Second line
Appended line
(2) os.ReadFile / os.WriteFile(一次性读写)
package main
import (
"fmt"
"os"
)
func main() {
// 写入(一次性)
content := []byte("line1\nline2\nline3\n")
err := os.WriteFile("data.txt", content, 0644)
if err != nil {
fmt.Printf("Write error: %v\n", err)
return
}
// 读取(一次性)
data, err := os.ReadFile("data.txt")
if err != nil {
fmt.Printf("Read error: %v\n", err)
return
}
fmt.Printf("Read %d bytes:\n%s", len(data), data)
}
▶ 示例:文件复制
package main
import (
"fmt"
"io"
"os"
)
func copyFile(src, dst string) (int64, error) {
sourceFile, err := os.Open(src)
if err != nil {
return 0, err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return 0, err
}
defer destFile.Close()
// io.Copy 使用默认 32KB 缓冲区
return io.Copy(destFile, sourceFile)
}
func main() {
// 先写源文件
os.WriteFile("source.txt", []byte("Hello World!\n"), 0644)
n, err := copyFile("source.txt", "dest.txt")
if err != nil {
fmt.Printf("Copy error: %v\n", err)
return
}
fmt.Printf("Copied %d bytes\n", n)
data, _ := os.ReadFile("dest.txt")
fmt.Printf("Content: %s", data)
}
输出:
Copied 13 bytes
Content: Hello World!
os.ReadFile 会把整个文件读入内存——适合小文件(< 100MB)。大文件(如日志 1GB+)必须用 bufio 或 io.Copy 流式处理。
4. bufio 缓冲读写
(1) bufio 逐行读取
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 先写测试文件
content := ""
for i := 1; i <= 100; i++ {
content += fmt.Sprintf("Line %d\n", i)
}
os.WriteFile("large.txt", []byte(content), 0644)
// 逐行读取
file, _ := os.Open("large.txt")
defer file.Close()
scanner := bufio.NewScanner(file)
lineCount := 0
for scanner.Scan() {
lineCount++
if lineCount <= 3 {
fmt.Println(scanner.Text())
}
}
fmt.Printf("...共 %d 行\n", lineCount)
}
输出:
Line 1
Line 2
Line 3
...共 100 行
▶ 示例:bufio 缓冲写入
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, _ := os.Create("buffered.txt")
defer file.Close()
writer := bufio.NewWriter(file)
writer.WriteString("第一行\n")
writer.WriteString("第二行\n")
writer.WriteString("第三行\n")
fmt.Printf("缓冲区大小: %d\n", writer.Buffered())
// 重要:刷新缓冲区到磁盘
writer.Flush()
data, _ := os.ReadFile("buffered.txt")
fmt.Printf("内容:\n%s", data)
}
(2) os vs bufio
| 特性 | os 直接读写 | bufio 缓冲 |
|---|---|---|
| 内部机制 | 系统调用(每次读写一次 syscall) | 用户态缓冲区(减少系统调用) |
| 适合场景 | 小文件、随机访问 | 大文件、顺序读写 |
| 逐行读取 | 不支持 | bufio.Scanner |
| 性能(大文件) | 慢(syscall 次数多) | 快(减少 90% syscall) |
| 默认缓冲区 | N/A | 4KB(可配置) |
5. encoding/json 序列化
(1) json.Marshal / json.Unmarshal
package main
import (
"encoding/json"
"fmt"
)
type Product struct {
Name string `json:"name"`
Price float64 `json:"price"`
InStock bool `json:"in_stock"`
Tags []string `json:"tags,omitempty"`
Internal string `json:"-"` // 不序列化
}
func main() {
p := Product{
Name: "Go Mug",
Price: 19.99,
InStock: true,
Tags: []string{"gift", "office"},
Internal: "secret123",
}
// 序列化
data, _ := json.Marshal(p)
fmt.Printf("Marshaled: %s\n", data)
// 带缩进
pretty, _ := json.MarshalIndent(p, "", " ")
fmt.Printf("Pretty:\n%s\n", pretty)
// 反序列化
var p2 Product
json.Unmarshal([]byte(`{"name":"Go T-Shirt","price":29.99,"in_stock":false}`), &p2)
fmt.Printf("Unmarshaled: %+v\n", p2)
}
输出:
Marshaled: {"name":"Go Mug","price":19.99,"in_stock":true,"tags":["gift","office"]}
Pretty:
{
"name": "Go Mug",
"price": 19.99,
"in_stock": true,
"tags": [
"gift",
"office"
]
}
Unmarshaled: {Name:Go T-Shirt Price:29.99 InStock:false Tags:[] Internal:}
▶ 示例:动态 JSON 解析(interface{})
package main
import (
"encoding/json"
"fmt"
)
func main() {
data := `{"name":"Alice","age":28,"address":{"city":"Shanghai","zip":"200000"}}`
// 解析到 map(无需预定义 struct)
var result map[string]interface{}
json.Unmarshal([]byte(data), &result)
fmt.Printf("name: %v\n", result["name"])
fmt.Printf("age: %v (type=%T)\n", result["age"], result["age"])
// 嵌套 map 需要类型断言(comma-ok 安全模式)
if addr, ok := result["address"].(map[string]interface{}); ok {
fmt.Printf("city: %v\n", addr["city"])
}
}
输出:
name: Alice
age: 28 (type=float64)
city: Shanghai
float64——所以 age 显示为 28 但类型是 float64。如果要用 int,要么用 struct 指明类型,要么用 json.Decoder + UseNumber()。
6. JSON Encoder / Decoder 流式处理
▶ 示例:Encoder 流式写入文件
package main
import (
"encoding/json"
"fmt"
"os"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
users := []User{
{Name: "Alice", Age: 28},
{Name: "Bob", Age: 32},
{Name: "Charlie", Age: 25},
}
// 流式写入文件(不构造完整 []byte)
file, _ := os.Create("users.json")
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
for _, u := range users {
encoder.Encode(u) // 逐条写入
}
data, _ := os.ReadFile("users.json")
fmt.Println(string(data))
}
(2) Decoder:流式读取(逐行 JSON)
package main
import (
"encoding/json"
"fmt"
"strings"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
// 模拟 JSON Lines 格式(每行一个 JSON object)
input := `{"name":"Alice","age":28}
{"name":"Bob","age":32}
{"name":"Charlie","age":25}`
// 流式解码
decoder := json.NewDecoder(strings.NewReader(input))
for {
var u User
err := decoder.Decode(&u)
if err != nil {
break // EOF
}
fmt.Printf("Decoded: %s (%d)\n", u.Name, u.Age)
}
}
输出:
Decoded: Alice (28)
Decoded: Bob (32)
Decoded: Charlie (25)
(3) Marshal vs Encoder
| 特性 | json.Marshal | json.Encoder |
|---|---|---|
| 输出目标 | []byte(内存) |
io.Writer(任意目标) |
| 内存使用 | 构建完整 []byte | 流式写入缓冲区 |
| 大文件 | ❌ OOM 风险 | ✅ 安全 |
| 缩进控制 | MarshalIndent |
SetIndent() |
| 典型场景 | API 返回 / 小数据 | 文件写入 / 网络流 |
7. JSON tag 完全指南
(1) tag 选项
type Config struct {
Name string `json:"name"` // 字段名映射
Omit string `json:"omit,omitempty"` // 零值时忽略
Skip string `json:"-"` // 跳过此字段
String int `json:"string"` // 数字转为字符串
}
(2) 全部选项
| 选项 | 语法 | 效果 |
|---|---|---|
| 重命名 | json:"new_name" |
JSON 字段名改为 new_name |
| omitempty | json:"name,omitempty" |
零值时省略该字段 |
| 跳过 | json:"-" |
不序列化/反序列化 |
| string | json:"id,string" |
数字转字符串(兼容 JavaScript) |
| 嵌套 | json:"-" 中间 struct |
控制嵌套序列化 |
▶ 示例:omitempty + string 实战
package main
import (
"encoding/json"
"fmt"
)
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message,omitempty"` // 空时省略
Data any `json:"data,omitempty"` // nil 时省略
ID int64 `json:"id,string"` // int64 → string
}
func main() {
resp := APIResponse{
Code: 200,
Data: nil, // 会被省略
ID: 9876543210123,
}
data, _ := json.MarshalIndent(resp, "", " ")
fmt.Println(string(data))
}
输出:
{
"code": 200,
"id": "9876543210123"
}
8. 完整示例:数据库导出 JSON 工具
// db_exporter.go
package main
import (
"encoding/json"
"fmt"
"os"
"time"
)
// ---------- 数据模型 ----------
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
Category string `json:"category,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
type ExportConfig struct {
FilePath string `json:"file_path"`
PrettyPrint bool `json:"pretty_print"`
IncludeMeta bool `json:"include_meta"`
}
// ---------- 模拟数据库 ----------
type Database struct {
products []Product
}
func NewDatabase() *Database {
return &Database{
products: []Product{
{ID: 1, Name: "Go Mug", Price: 19.99, Stock: 100, Category: "accessories",
CreatedAt: time.Now()},
{ID: 2, Name: `Go "Gopher" T-Shirt`, Price: 29.99, Stock: 50, Category: "clothing",
CreatedAt: time.Now()},
{ID: 3, Name: "Go Programming Book", Price: 49.99, Stock: 200, Category: "books",
CreatedAt: time.Now()},
},
}
}
func (db *Database) QueryAll() []Product {
return db.products
}
// ---------- 导出器 ----------
type Exporter struct {
config ExportConfig
}
func NewExporter(config ExportConfig) *Exporter {
return &Exporter{config: config}
}
func (e *Exporter) ExportToFile(products []Product) error {
file, err := os.Create(e.config.FilePath)
if err != nil {
return fmt.Errorf("create file: %w", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
if e.config.PrettyPrint {
encoder.SetIndent("", " ")
}
if e.config.IncludeMeta {
wrapper := map[string]interface{}{
"exported_at": time.Now().Format(time.RFC3339),
"total": len(products),
"products": products,
}
return encoder.Encode(wrapper)
}
return encoder.Encode(products)
}
func (e *Exporter) ExportToMemory(products []Product) ([]byte, error) {
if e.config.PrettyPrint {
return json.MarshalIndent(products, "", " ")
}
return json.Marshal(products)
}
// ---------- 导入器(读取 JSON 文件)----------
type Importer struct{}
func NewImporter() *Importer {
return &Importer{}
}
func (imp *Importer) ImportFromFile(path string) ([]Product, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open file: %w", err)
}
defer file.Close()
var products []Product
decoder := json.NewDecoder(file)
if err := decoder.Decode(&products); err != nil {
// 尝试带 wrapper 的格式
file.Seek(0, 0)
decoder = json.NewDecoder(file)
var wrapped struct {
Products []Product `json:"products"`
}
if err2 := decoder.Decode(&wrapped); err2 != nil {
return nil, fmt.Errorf("decode: %w", err)
}
products = wrapped.Products
}
return products, nil
}
func main() {
db := NewDatabase()
products := db.QueryAll()
// 1. 流式 JSON 写入文件
exporter := NewExporter(ExportConfig{
FilePath: "export.json",
PrettyPrint: true,
IncludeMeta: true,
})
if err := exporter.ExportToFile(products); err != nil {
fmt.Printf("Export error: %v\n", err)
return
}
fmt.Println("✅ 导出到 export.json")
// 2. 读取回来验证
importer := NewImporter()
imported, err := importer.ImportFromFile("export.json")
if err != nil {
fmt.Printf("Import error: %v\n", err)
return
}
fmt.Printf("✅ 导入 %d 个产品\n", len(imported))
for _, p := range imported {
fmt.Printf(" %d. %s ($%.2f)\n", p.ID, p.Name, p.Price)
}
// 3. 显示文件内容
data, _ := os.ReadFile("export.json")
fmt.Printf("\n文件内容:\n%s\n", data)
}
预期输出:
✅ 导出到 export.json
✅ 导入 3 个产品
1. Go Mug ($19.99)
2. Go "Gopher" T-Shirt ($29.99)
3. Go Programming Book ($49.99)
文件内容:
{
"exported_at": "2026-07-08T10:00:00Z",
"total": 3,
"products": [
{
"id": 1,
"name": "Go Mug",
"price": 19.99,
"stock": 100,
"category": "accessories",
"created_at": "2026-07-08T10:00:00Z"
},
...
]
}
flowchart LR
A[Go struct] --> B[json.Marshal]
A --> C[json.Encoder]
B --> D[[]byte 内存]
D --> E[os.WriteFile / HTTP Response]
C --> F[File / net.Conn / bytes.Buffer]
F --> G[流式写入]
H[JSON 文件/流] --> I[json.Decoder]
I --> J[Go struct]
H --> K[json.Unmarshal]
K --> J
style A fill:#e1f5fe
style J fill:#e1f5fe
omitempty 对零值时间 time.Time{} 无效——time.Time{} 不是 nil,不会被省略。如果需要省略零值时间,用 *time.Time 指针类型。
❓ 常见问题
ioutil.ReadFile 在 Go 1.19 起已废弃(deprecated)。你应该用 os.ReadFile / os.WriteFile。os.File.Read 每次都是 syscall(约 1µs 开销),bufio 一次读 4KB 减少 99.9% 的 syscall。Marshal;写入文件/网络流或大文件 → Encoder。Encoder 的优势是流式不占用大内存。json:"name" 重命名;json:"name,omitempty" 零值省略;json:"-" 跳过;json:"id,string" 数字转字符串。多个选项用逗号隔开。json.MarshalIndent 或 SetEscapeHTML(false)。json.Decoder 流式解码(decoder.Decode(&v) 逐对象),或 JSON Lines 格式(每行一个 JSON 对象)。不要用 json.Unmarshal 读整个文件。map[string]interface{} 或 json.RawMessage(延迟解析)。map[string]interface{} 灵活但需要类型断言;json.RawMessage 保留原始 JSON 稍后解析。os.Create(f) 等价于 os.OpenFile(f, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)——如果文件存在就截断(覆盖)。os.OpenFile 支持更多模式(追加/只读/创建新文件等)。📖 小节
os.ReadFile/os.WriteFile一次性读写小文件bufio.Scanner逐行处理大文件,bufio.Writer缓冲写入encoding/json包自动处理序列化/反序列化,无需手动拼字符串json.Marshal内存中处理,json.Encoder流式写入json.Decoder流式读取,适合大文件- JSON tag:
omitempty/-/string控制序列化行为 - 文件 IO 和 JSON 通过
io.Reader/io.Writer接口统一
📝 作业
-
基础题(难度⭐):写一个程序:用
os.ReadFile读取一个文本文件,统计行数、单词数、字符数,输出到另一个文件。要求用strings.Fields+bufio.Scanner+os.WriteFile。 -
进阶题(难度⭐⭐):实现一个
TodoList管理器:支持添加/列表/完成/删除操作,用encoding/json序列化到文件持久化。要求 JSON tag 控制字段名,omitempty处理可选字段。 -
挑战题(难度⭐⭐⭐):实现一个多格式导出器:从
[]Product数据源同时导出为 JSON、JSON Lines(每行一个 JSON 对象)、CSV 三种格式。要求用os.Create+json.Encoder(JSON)/fmt.Fprintf(CSV)+ 共用io.Writer接口。