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. 你将学到


2. 一个全栈工程师的真实故事

(1) 痛点:手动拼接 JSON string

Charlie 是全栈工程师,他需要把数据库中的用户数据导出为 JSON 文件供前端使用:

"我不想用第三方库,就用手动拼接 JSON string。结果用户名字段里有 " 符号,JSON 格式直接炸了。100 万行数据导出了一半才报错——回滚又花了 2 小时。"

他打开自己写的代码:

GO
// 坏代码:手动拼接 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

GO
// 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))
}

输出:

TEXT 📖 仅展示
导出成功!
[
  {
    "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 行
💡 提示: 永远不要手动拼 JSON——用 encoding/json package。它自动处理转义、编码、缩进,并且会正确处理 Go 的 UTF-8 字符。


3. os 文件操作

(1) 文件打开与创建

GO
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))
}

输出:

TEXT 📖 仅展示
Hello, Go Files!
Second line
Appended line

(2) os.ReadFile / os.WriteFile(一次性读写)

GO
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)
}

▶ 示例:文件复制

GO
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)
}
▶ 试一试

输出:

TEXT 📖 仅展示
Copied 13 bytes
Content: Hello World!
🔥 易错: os.ReadFile 会把整个文件读入内存——适合小文件(< 100MB)。大文件(如日志 1GB+)必须用 bufioio.Copy 流式处理。


4. bufio 缓冲读写

(1) bufio 逐行读取

GO
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)
}

输出:

TEXT 📖 仅展示
Line 1
Line 2
Line 3
...共 100 行

▶ 示例:bufio 缓冲写入

GO
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

GO
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)
}

输出:

TEXT 📖 仅展示
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{})

GO
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"])
    }
}
▶ 试一试

输出:

TEXT 📖 仅展示
name: Alice
age: 28 (type=float64)
city: Shanghai
💡 提示: JSON 的数字默认解析为 float64——所以 age 显示为 28 但类型是 float64。如果要用 int,要么用 struct 指明类型,要么用 json.Decoder + UseNumber()


6. JSON Encoder / Decoder 流式处理

▶ 示例:Encoder 流式写入文件

GO
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)

GO
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)
    }
}

输出:

TEXT 📖 仅展示
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 选项

GO
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 实战

GO
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))
}
▶ 试一试

输出:

TEXT 📖 仅展示
{
  "code": 200,
  "id": "9876543210123"
}

8. 完整示例:数据库导出 JSON 工具

GO
// 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)
}

预期输出:

TEXT 📖 仅展示
✅ 导出到 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"
    },
    ...
  ]
}
100%
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
🔥 易错: JSON 导出时的 omitempty 对零值时间 time.Time{} 无效——time.Time{} 不是 nil,不会被省略。如果需要省略零值时间,用 *time.Time 指针类型。


❓ 常见问题

Q os.ReadFile 和 ioutil.ReadFile 有什么区别?
A os.ReadFile 是 Go 1.16 新增的替代品,用法相同。ioutil.ReadFile 在 Go 1.19 起已废弃(deprecated)。你应该用 os.ReadFile / os.WriteFile
Q bufio 比 os 直接读写快在哪?
A bufio 在用户态维护缓冲区(默认 4KB),大幅减少系统调用次数。直接 os.File.Read 每次都是 syscall(约 1µs 开销),bufio 一次读 4KB 减少 99.9% 的 syscall。
Q json.Marshal 和 json.Encoder 怎么选?
A 数据量 < 100KB 或需要内存中处理 → Marshal;写入文件/网络流或大文件 → Encoder。Encoder 的优势是流式不占用大内存。
Q struct tag 如何控制 JSON 字段?
A json:"name" 重命名;json:"name,omitempty" 零值省略;json:"-" 跳过;json:"id,string" 数字转字符串。多个选项用逗号隔开。
Q 中文编码在 JSON 中怎么处理?
A Go 的 encoding/json 默认输出 UTF-8——非 ASCII 字符会原样输出。如果需要 ASCII-only(如某些老旧系统要求),用 json.MarshalIndentSetEscapeHTML(false)
Q 大 JSON 文件怎么处理?
Ajson.Decoder 流式解码(decoder.Decode(&v) 逐对象),或 JSON Lines 格式(每行一个 JSON 对象)。不要用 json.Unmarshal 读整个文件。
Q 如何解析未知结构的 JSON?
Amap[string]interface{}json.RawMessage(延迟解析)。map[string]interface{} 灵活但需要类型断言;json.RawMessage 保留原始 JSON 稍后解析。
Q os.Create 和 os.OpenFile 什么区别?
A os.Create(f) 等价于 os.OpenFile(f, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)——如果文件存在就截断(覆盖)。os.OpenFile 支持更多模式(追加/只读/创建新文件等)。

📖 小节


📝 作业

  1. 基础题(难度⭐):写一个程序:用 os.ReadFile 读取一个文本文件,统计行数、单词数、字符数,输出到另一个文件。要求用 strings.Fields + bufio.Scanner + os.WriteFile

  2. 进阶题(难度⭐⭐):实现一个 TodoList 管理器:支持添加/列表/完成/删除操作,用 encoding/json 序列化到文件持久化。要求 JSON tag 控制字段名,omitempty 处理可选字段。

  3. 挑战题(难度⭐⭐⭐):实现一个多格式导出器:从 []Product 数据源同时导出为 JSON、JSON Lines(每行一个 JSON 对象)、CSV 三种格式。要求用 os.Create + json.Encoder(JSON)/ fmt.Fprintf(CSV)+ 共用 io.Writer 接口。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏