目录

Go JSON格式化输出到文件

在做配置文件导出功能时,直接用json.Marshal把数据转成JSON后写入文件,结果打开文件一看全是压缩成一行的乱码

实际可以用json.MarshalIndent做格式化缩进,还要指定编码避免乱码。

今天把总结的JSON格式化输出到文件的方法分享出来,希望对大家有所帮助。

先明确核心前提:

Go的encoding/json包是JSON处理的核心,Marshal生成压缩JSON,MarshalIndent生成带缩进的格式化JSON;

写入文件时需注意编码(默认UTF-8)和文件权限,否则易出现乱码或写入失败

一、简单数据格式化输出

如果要输出的是map、切片等基础数据类型,核心是用json.MarshalIndent生成带缩进的JSON字符串,再通过os.WriteFile写入文件。这种方式最简洁,适合配置文件、简单数据备份场景。

以导出系统配置(包含端口、超时时间、开关等)为例,实现格式化输出到文件:

package main

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

func main() {
    // 1. 准备要输出的基础数据(Map类型,模拟系统配置)
    config := map[string]interface{}{
        "server": map[string]interface{}{
            "port":     8080,
            "timeout":  30,
            "ssl":      true,
            "allowedIPs": []string{"127.0.0.1", "192.168.1.0/24"},
        },
        "log": map[string]interface{}{
            "level": "info",
            "path":  "./logs/app.log",
        },
    }

    // 2. JSON格式化处理:MarshalIndent(数据, 前缀, 缩进符)
    // 前缀用"",缩进符用"  "(两个空格,也可用"\t"制表符)
    jsonData, err := json.MarshalIndent(config, "", "  ")
    if err != nil {
        panic(fmt.Sprintf("JSON格式化失败:%v", err))
    }

    // 3. 写入文件:WriteFile(路径, 数据, 权限)
    // 权限0644表示:所有者读写,其他只读(适合配置文件)
    err = os.WriteFile("config.json", jsonData, 0644)
    if err != nil {
        panic(fmt.Sprintf("写入文件失败:%v", err))
    }

    fmt.Println("JSON格式化输出到文件成功!")
}
  • 格式化核心json.MarshalIndent(config, "", " ")的三个参数分别是“待处理数据”“每行前缀”“缩进字符”,用两个空格缩进生成的JSON结构清晰,符合通用阅读习惯。
  • 文件权限:0644是配置文件常用权限,避免因权限过高导致安全风险,也防止权限不足无法读取;如果是可执行脚本,可改用0755。
  • 编码问题json.MarshalIndent生成的是UTF-8编码字节流,os.WriteFile默认按UTF-8写入,Windows下打开也不会乱码(避免用记事本直接打开,建议用VS Code)。

二、结构体数据格式化输出

业务开发中更常见的是将结构体数据(如用户信息、订单数据)转成JSON输出到文件,这就需要注意结构体字段导出Tag配置——字段未导出会导致JSON为空,Tag用于指定JSON键名和忽略空值等。

以导出用户列表为例,结构体字段加Tag控制JSON键名、忽略空值,实现格式化输出:

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "time"
)

// User 业务结构体(模拟用户数据)
// Tag含义:json:"键名,omitempty" 键名指定,空值忽略
type User struct {
    Username string    `json:"username"`       // 必选字段,指定键名
    Age      int       `json:"age,omitempty"`  // 可选字段,空值不输出
    Email    string    `json:"email"`          // 必选字段
    RegisterAt time.Time `json:"register_at"`  // 时间字段自动转ISO格式
    IsVip    bool      `json:"is_vip"`         // 布尔字段
}

func main() {
    // 1. 准备结构体数据(用户列表)
    users := []User{
        {
            Username: "zhangsan",
            Age:      25,
            Email:    "zhangsan@example.com",
            RegisterAt: time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC),
            IsVip:    true,
        },
        {
            Username: "lisi",
            // Age留空,Tag指定omitempty,JSON中不输出该字段
            Email:    "lisi@example.com",
            RegisterAt: time.Date(2024, 2, 20, 14, 10, 0, 0, time.UTC),
            IsVip:    false,
        },
    }

    // 2. JSON格式化:缩进用"\t"制表符,结构更整齐
    jsonData, err := json.MarshalIndent(users, "", "\t")
    if err != nil {
        panic(fmt.Sprintf("结构体JSON格式化失败:%v", err))
    }

    // 3. 写入文件:覆盖写入(若文件存在则替换)
    err = os.WriteFile("user_list.json", jsonData, 0644)
    if err != nil {
        panic(fmt.Sprintf("写入用户列表失败:%v", err))
    }

    fmt.Println("结构体JSON格式化输出成功!")
}
  • 字段必须导出:结构体字段首字母必须大写(如Username),否则json包无法访问,输出的JSON会是空对象{}
  • Tag配置技巧:常用Tag参数有omitempty(空值忽略)、"-"(不输出该字段)、"key,omitempty"(指定键名+空值忽略),比如Age int json:“user_age,omitempty”``。`
  • 时间字段处理time.Time类型会自动转成ISO 8601格式(如"2024-01-15T10:30:00Z"),若需自定义格式,需实现json.Marshaler接口。

当然,如果想让时间字段输出为"2024-01-15 10:30:00"格式,自定义时间类型并实现MarshalJSON方法:

// 自定义时间类型
type CustomTime time.Time

// 实现json.Marshaler接口的MarshalJSON方法
func (ct CustomTime) MarshalJSON() ([]byte, error) {
    // 自定义时间格式
    formatted := time.Time(ct).Format("2006-01-02 15:04:05")
    // 包裹成JSON字符串(需加引号)
    return []byte(fmt.Sprintf("\"%s\"", formatted)), nil
}

// 结构体中使用自定义时间类型
type User struct {
    Username   string      `json:"username"`
    RegisterAt CustomTime  `json:"register_at"`
}

// 使用时转换time.Time为CustomTime
users := []User{
    {
        Username: "zhangsan",
        RegisterAt: CustomTime(time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)),
    },
}

进阶:批量/大JSON数据输出

如果要输出十万条以上的批量数据(如日志导出、数据备份),直接用MarshalIndent会把所有数据加载到内存,导致内存溢出。此时需用“流式写入”——逐行写入JSON数组,降低内存占用。

流式写入实现代码(批量用户数据)

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "time"
)

type User struct {
    Username string    `json:"username"`
    Age      int       `json:"age"`
    RegisterAt time.Time `json:"register_at"`
}

func main() {
    // 1. 创建文件并打开(读写模式,不存在则创建,存在则清空)
    file, err := os.OpenFile("batch_users.json", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
    if err != nil {
        panic(fmt.Sprintf("创建文件失败:%v", err))
    }
    // 关键:延迟关闭文件,避免资源泄漏
    defer func() {
        if err := file.Close(); err != nil {
            fmt.Printf("关闭文件失败:%v", err)
        }
    }()

    // 2. 写入JSON数组开头
    _, err = file.WriteString("[\n")
    if err != nil {
        panic(fmt.Sprintf("写入数组开头失败:%v", err))
    }

    // 3. 模拟批量数据(10万条,实际场景从数据库查询)
    batchSize := 100000
    for i := 0; i < batchSize; i++ {
        user := User{
            Username: fmt.Sprintf("user_%d", i),
            Age:      18 + i%20,
            RegisterAt: time.Now().AddDate(0, -i%12, 0),
        }

        // 格式化单条数据,前缀加制表符,缩进用两个空格
        jsonData, err := json.MarshalIndent(user, "\t", "  ")
        if err != nil {
            panic(fmt.Sprintf("格式化第%d条数据失败:%v", i, err))
        }

        // 写入单条数据
        _, err = file.Write(jsonData)
        if err != nil {
            panic(fmt.Sprintf("写入第%d条数据失败:%v", i, err))
        }

        // 除最后一条外,末尾加逗号和换行
        if i != batchSize-1 {
            _, err = file.WriteString(",\n")
        } else {
            _, err = file.WriteString("\n")
        }
        if err != nil {
            panic(fmt.Sprintf("写入分隔符失败:%v", err))
        }
    }

    // 4. 写入JSON数组结尾
    _, err = file.WriteString("]")
    if err != nil {
        panic(fmt.Sprintf("写入数组结尾失败:%v", err))
    }

    fmt.Printf("成功输出%d条数据到文件!", batchSize)
}

核心优势:每次只处理一条数据并写入,内存占用始终保持在低水平,即使处理百万条数据也不会内存溢出。

常见问题

Q1. 问题:JSON文件打开后中文乱码?

最常见的坑!原因是Windows系统默认用GBK编码打开文件,而Go默认按UTF-8写入,编码不匹配导致乱码。

解决:

① 写入时指定UTF-8 BOM头(让Windows记事本识别UTF-8);

② 用VS Code、Notepad++等编辑器打开(自动识别UTF-8)。

示例代码:

// 写入UTF-8 BOM头(解决Windows记事本乱码)
_, err = file.WriteString("\xEF\xBB\xBF")
if err != nil {
    panic(err)
}
// 再写入JSON数据
_, err = file.Write(jsonData)

Q2. 问题:结构体转JSON后字段缺失或为空?

90%是因为结构体字段未导出(首字母小写),json包无法访问该字段;另外Tag配置错误(如键名拼写错误)也会导致字段缺失。

解决:

① 所有需要输出的字段首字母必须大写;

② 检查Tag配置,确保键名正确,避免多写或少写字符(如json:"user_name"不要写成json:"username")。

Q3. 问题:格式化后的JSON有多余空格或换行?

原因是MarshalIndent的前缀或缩进符配置不当,或写入时手动添加了多余的分隔符。

解决:

① 前缀用"""\t",缩进符统一用" "(两个空格)或"\t",不要混合使用;

② 批量写入时严格控制逗号和换行,最后一条数据不加逗号。

Q4. 问题:大JSON数据写入时内存溢出?

原因是直接用MarshalIndent处理整个批量数据,把所有数据加载到内存导致溢出。

解决:

① 采用“流式写入”方案,逐行处理单条数据并写入文件,如前面进阶场景的示例代码;

② 如果是JSON数组,手动拼接数组开头、单条数据、分隔符和数组结尾。

Q5. 问题:写入文件时提示“permission denied”?

原因是文件路径不存在(如写入./logs/data.json但logs目录未创建),或当前用户没有写入权限。

解决:

① 写入前检查目录是否存在,不存在则创建;

② 配置正确的文件权限(如0644、0755)。

示例代码:

import "os"

// 检查并创建目录(递归创建多级目录)
err = os.MkdirAll("./logs", 0755)
if err != nil {
    panic(fmt.Sprintf("创建目录失败:%v", err))
}

// 再写入文件(路径存在,权限正确)
err = os.WriteFile("./logs/data.json", jsonData, 0644)

总结

  1. 简单数据场景:直接用json.MarshalIndent + os.WriteFile,代码最简洁,适合配置文件、小数据备份;
  2. 结构体场景:定义结构体并配置Tag,用MarshalIndent格式化,控制字段导出和键名,适合业务数据导出;
  3. 批量/大数据场景:用os.OpenFile创建文件,逐行流式写入JSON数组,避免内存溢出,适合日志、海量数据备份。

最后注意: 结构体字段首字母大写,中文乱码加UTF-8 BOM头,大数据用流式写入,写入前检查目录权限。

如果大家在处理特殊JSON格式(如嵌套JSON、JSON Lines)时遇到问题,欢迎在评论区交流~

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-json-indent/

备用原文链接: https://blog.fiveyoboy.com/articles/go-json-indent/