JSON Formatter Go — MarshalIndent() 指南
直接在浏览器中使用免费的 JSON格式化工具,无需安装。
在线试用 JSON格式化工具 →在开发 Go 微服务时,每当需要排查 API 响应或配置文件,压缩的 JSON 总是第一个拦路虎—— 一行塞满数百个嵌套字段的内容,一眼根本看不出任何信息。要在 Go 中 格式化 JSON,标准库已经提供了一切: json.MarshalIndent 内置于 encoding/json, 随每个 Go 安装包一起提供,零第三方依赖。本指南涵盖完整内容:struct tags、自定义 MarshalJSON() 实现、用于重新格式化原始字节的 json.Indent、 使用 json.Decoder 流式处理大型文件、何时选用 go-json 处理高吞吐量场景,以及在终端快速格式化的 CLI 一行命令。 所有示例均使用 Go 1.21+。
- ✓json.MarshalIndent(v, "", "\t") 是标准库——零依赖,随每个 Go 安装包一起提供。
- ✓struct tags json:"field_name,omitempty" 控制序列化键名,并从输出中忽略零值字段。
- ✓为任意类型实现 MarshalJSON(),可完全控制其 JSON 表示形式。
- ✓json.Indent() 可在不重新解析 struct 的情况下重新格式化已序列化的 []byte——对原始字节处理更快。
- ✓处理大型文件(>100 MB)时:使用带 Token() 的 json.Decoder 进行流式处理,无需将全部内容加载到内存。
- ✓go-json 是 encoding/json 的直接替代品,在高吞吐量 API 场景下速度提升 3–5 倍。
什么是 JSON 格式化?
JSON 格式化(也称为 pretty-printing)将紧凑的 JSON 字符串转换为具有一致缩进和换行的 人类可读布局。底层数据完全相同,只有空白符发生变化。紧凑 JSON 适合每个字节都至关重要的 网络传输;格式化 JSON 则更适合调试、代码审查、日志检查和配置文件编写。Go 的 encoding/json 包通过单次函数调用即可处理两种模式——在 json.Marshal 和 json.MarshalIndent 之间切换,即可在紧凑输出和缩进输出之间自由转换。
{"service":"payments","port":8443,"workers":4}{
"service": "payments",
"port": 8443,
"workers": 4
}json.MarshalIndent() — 标准库方式
json.MarshalIndent 位于 encoding/json 包中,该包是 Go 标准库的一部分——无需 go get。 其函数签名为 MarshalIndent(v any, prefix, indent string) ([]byte, error):prefix 字符串会追加到每行输出的开头(几乎始终留空), 而 indent 每个嵌套层级重复一次。传入 "\t" 使用制表符,或传入 " " 使用两个空格。
package main
import (
"encoding/json"
"fmt"
"log"
)
type ServiceConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Workers int `json:"workers"`
TLSEnabled bool `json:"tls_enabled"`
AllowedIPs []string `json:"allowed_ips"`
}
func main() {
cfg := ServiceConfig{
Host: "payments-api.internal",
Port: 8443,
Workers: 8,
TLSEnabled: true,
AllowedIPs: []string{"10.0.0.0/8", "172.16.0.0/12"},
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Fatalf("marshal config: %v", err)
}
fmt.Println(string(data))
}
// {
// "host": "payments-api.internal",
// "port": 8443,
// "workers": 8,
// "tls_enabled": true,
// "allowed_ips": [
// "10.0.0.0/8",
// "172.16.0.0/12"
// ]
// }制表符还是空格的选择,主要取决于团队约定。许多 Go 项目倾向于使用制表符,因为 gofmt (格式化 Go 源码的工具)也使用制表符。当 JSON 的目标消费方是 JavaScript 或 Python 时,两个空格或四个空格缩进更为常见。以下是同一个 struct 采用两种缩进风格的对比:
// 制表符缩进——Go 原生工具链偏好
data, _ := json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// 两空格缩进——适合 JS/Python 消费方的 API
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// 四空格缩进
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }json.Marshal(v)。 它接受相同的值参数,具有相同的错误语义, 但输出的是没有任何空白符的单行 JSON。Struct Tags — 控制字段名与 Omitempty
Go struct tags 是放在字段声明后的字符串字面量,用于告诉encoding/json如何序列化每个字段。有三个关键指令: json:"name" 在输出中重命名字段, omitempty 在字段持有其类型零值时忽略该字段,以及 json:"-" 完全排除该字段——适用于密码、内部标识符,或任何不应越过服务边界的字段。
type UserProfile struct {
ID string `json:"id"`
Email string `json:"email"`
DisplayName string `json:"display_name,omitempty"` // 为空字符串时忽略
AvatarURL *string `json:"avatar_url,omitempty"` // 为 nil 指针时忽略
IsAdmin bool `json:"is_admin,omitempty"` // 为 false 时忽略
passwordHash string // 未导出——自动排除
}
// 所有可选字段均已填充的用户
full := UserProfile{
ID: "usr_7b3c", Email: "ops@example.com",
DisplayName: "运维团队", IsAdmin: true,
}
// {
// "id": "usr_7b3c",
// "email": "ops@example.com",
// "display_name": "运维团队",
// "is_admin": true
// }
// 没有可选字段的用户——它们被完全忽略
minimal := UserProfile{ID: "usr_2a91", Email: "dev@example.com"}
// {
// "id": "usr_2a91",
// "email": "dev@example.com"
// }json:"-" tag 适合那些必须无条件排除的字段——无论其值为何——通常是密钥、内部追踪字段, 或在内存中正确存储但绝不应序列化到任何外部系统的数据。
type AuthToken struct {
TokenID string `json:"token_id"`
Subject string `json:"sub"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
SigningKey []byte `json:"-"` // 永远不序列化
RefreshToken string `json:"-"` // 永远不序列化
}
tok := AuthToken{
TokenID: "tok_8f2a", Subject: "usr_7b3c",
IssuedAt: 1741614120, ExpiresAt: 1741617720,
SigningKey: []byte("secret"), RefreshToken: "rt_9e4f",
}
data, _ := json.MarshalIndent(tok, "", " ")
// {
// "token_id": "tok_8f2a",
// "sub": "usr_7b3c",
// "iat": 1741614120,
// "exp": 1741617720
// }
// SigningKey 和 RefreshToken 永远不会出现encoding/json 排除,无论是否有 tag。 无需为未导出字段添加 json:"-"——排除是自动的,且无法被覆盖。自定义 MarshalJSON() — 处理非标准类型
任何 Go 类型都可以通过定义 MarshalJSON() ([]byte, error) 方法来实现 json.Marshaler 接口。当 encoding/json 遇到此类类型时,会调用该方法而非默认的基于反射的序列化。这是 Go 处理领域类型需要特定 wire 表示的规范模式——货币值、状态枚举、自定义时间格式,或任何存储方式与序列化方式不同的类型。
自定义类型——Money 的分转元转换
package main
import (
"encoding/json"
"fmt"
"log"
)
type Money struct {
Amount int64 // 以分为单位存储,避免浮点数精度问题
Currency string
}
func (m Money) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Display string `json:"display"`
}{
Amount: float64(m.Amount) / 100,
Currency: m.Currency,
Display: fmt.Sprintf("%s %.2f", m.Currency, float64(m.Amount)/100),
})
}
type Invoice struct {
ID string `json:"id"`
Subtotal Money `json:"subtotal"`
Tax Money `json:"tax"`
Total Money `json:"total"`
}
func main() {
inv := Invoice{
ID: "inv_9a2f91bc",
Subtotal: Money{Amount: 19900, Currency: "CNY"},
Tax: Money{Amount: 1592, Currency: "CNY"},
Total: Money{Amount: 21492, Currency: "CNY"},
}
data, err := json.MarshalIndent(inv, "", " ")
if err != nil {
log.Fatalf("marshal invoice: %v", err)
}
fmt.Println(string(data))
}
// {
// "id": "inv_9a2f91bc",
// "subtotal": { "amount": 199, "currency": "CNY", "display": "CNY 199.00" },
// "tax": { "amount": 15.92, "currency": "CNY", "display": "CNY 15.92" },
// "total": { "amount": 214.92, "currency": "CNY", "display": "CNY 214.92" }
// }状态枚举——字符串表示
type OrderStatus int
const (
StatusPending OrderStatus = iota
StatusPaid
StatusShipped
StatusCancelled
)
var orderStatusNames = map[OrderStatus]string{
StatusPending: "pending",
StatusPaid: "paid",
StatusShipped: "shipped",
StatusCancelled: "cancelled",
}
func (s OrderStatus) MarshalJSON() ([]byte, error) {
name, ok := orderStatusNames[s]
if !ok {
return nil, fmt.Errorf("unknown order status: %d", s)
}
return json.Marshal(name)
}
type Order struct {
ID string `json:"id"`
Status OrderStatus `json:"status"`
}
o := Order{ID: "ord_3c7f", Status: StatusShipped}
data, _ := json.MarshalIndent(o, "", " ")
// {
// "id": "ord_3c7f",
// "status": "shipped"
// }MarshalJSON() 和 UnmarshalJSON()。如果只实现序列化, 将类型通过 JSON 进行往返处理(序列化→存储→反序列化)会静默丢失结构或返回错误类型。 这两个方法共同构成了类型可以经受 JSON 往返的契约。UUID——序列化为字符串
Go 标准库没有 UUID 类型。最常用的选择是 github.com/google/uuid, 它已经实现了 MarshalJSON() 并序列化为带引号的 RFC 4122 字符串。如果你使用原始的 [16]byte 或自定义 ID 类型,请自行实现该接口,以避免 JSON 输出中出现 base64 编码的二进制数据。
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
)
type AuditEvent struct {
EventID uuid.UUID `json:"event_id"` // 序列化为 "550e8400-e29b-41d4-a716-446655440000"
SessionID uuid.UUID `json:"session_id"`
Action string `json:"action"`
ActorID string `json:"actor_id"`
OccuredAt string `json:"occurred_at"`
}
event := AuditEvent{
EventID: uuid.New(),
SessionID: uuid.MustParse("550e8400-e29b-41d4-a716-446655440000"),
Action: "user.password_changed",
ActorID: "usr_7f3a91bc",
OccuredAt: "2026-03-10T14:22:00Z",
}
data, _ := json.MarshalIndent(event, "", " ")
fmt.Println(string(data))
// {
// "event_id": "a4b2c1d0-...",
// "session_id": "550e8400-e29b-41d4-a716-446655440000",
// "action": "user.password_changed",
// "actor_id": "usr_7f3a91bc",
// "occurred_at": "2026-03-10T14:22:00Z"
// }[16]byte 而不使用自定义类型,encoding/json 会将其编码为 base64 字符串—— 例如 "VQ6EAOKbQdSnFkRmVUQAAA=="。 务必使用正确的 UUID 类型或实现 MarshalJSON() 以输出标准的带连字符的字符串格式。json.MarshalIndent() 参数参考
函数签名为 func MarshalIndent(v any, prefix, indent string) ([]byte, error)。prefix 和 indent 均为字符串字面量——没有像 Python 的 indent=4 那样的数字简写。
常用 struct tag 选项:
json.Indent() — 重新格式化已有的 JSON 字节
当你已经有了一个 []byte 的 JSON——比如来自 HTTP 响应体、Postgres jsonb 列,或用 os.ReadFile 读取的文件——不需要先定义 struct 并反序列化再格式化输出。 json.Indent 直接将原始字节重新格式化,并将缩进输出写入 bytes.Buffer。
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
func main() {
// 模拟来自上游服务的原始 JSON payload
raw := []byte(`{"trace_id":"tr_9a2f","service":"checkout","latency_ms":342,"error":null}`)
var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
}
// {
// "trace_id": "tr_9a2f",
// "service": "checkout",
// "latency_ms": 342,
// "error": null
// }我在微服务中常用的一个模式是在写入结构化日志之前调用 json.Indent—— 额外开销可以忽略不计,但在故障处理时日志条目的可读性会大幅提升。该函数对于记录 HTTP 响应、 格式化存储的 JSON 字符串以及无法获取 struct 定义的按需格式化场景特别有用。
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
var pretty bytes.Buffer
if err := json.Indent(&pretty, body, "", " "); err != nil {
// 响应体不是有效的 JSON——直接记录原始内容
logger.Debug("upstream response", "status", statusCode, "body", string(body))
return
}
logger.Debug("upstream response", "status", statusCode, "body", pretty.String())
}json.Indent 不会对 JSON 进行完整验证—— 它只检查插入空白符所需的结构。如需完整语法验证, 请先调用 json.Valid(data), 并在结果为 false 时进行处理,再尝试缩进。从文件和 HTTP 响应格式化 JSON
Go 服务中最常见的两个实际场景是:格式化从磁盘文件读取的 JSON(配置文件、测试夹具、 迁移种子数据),以及对 HTTP 响应体进行格式化以供调试日志或测试断言使用。 两者遵循相同的模式:读取字节,调用 json.Indent 或反序列化后再调用 json.MarshalIndent, 然后写回或记录日志。
读取文件 → 格式化 → 写回
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
)
func formatJSONFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
if !json.Valid(data) {
return fmt.Errorf("invalid JSON in %s", path)
}
var buf bytes.Buffer
if err := json.Indent(&buf, data, "", " "); err != nil {
return fmt.Errorf("indent %s: %w", path, err)
}
if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
return nil
}
func main() {
if err := formatJSONFile("config/database.json"); err != nil {
log.Fatalf("format config: %v", err)
}
fmt.Println("config/database.json 格式化成功")
}HTTP 响应 → 解码 → 格式化输出用于调试日志
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type HealthResponse struct {
Status string `json:"status"`
Version string `json:"version"`
Checks map[string]string `json:"checks"`
UptimeSec int64 `json:"uptime_seconds"`
}
func main() {
resp, err := http.Get("https://api.payments.internal/v2/health")
if err != nil {
log.Fatalf("health check: %v", err)
}
defer resp.Body.Close()
var result HealthResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
log.Fatalf("decode health response: %v", err)
}
pretty, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatalf("marshal health response: %v", err)
}
fmt.Printf("健康检查(%d):
%s
", resp.StatusCode, string(pretty))
}
// 健康检查(200):
// {
// "status": "ok",
// "version": "1.4.2",
// "checks": {
// "database": "ok",
// "cache": "ok",
// "queue": "degraded"
// },
// "uptime_seconds": 172800
// }原始响应体 → json.Indent(无需 Struct)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
func main() {
resp, err := http.Get("https://api.payments.internal/v2/health")
if err != nil {
log.Fatalf("request: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("read body: %v", err)
}
var buf bytes.Buffer
if err := json.Indent(&buf, body, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
}在 Go 中从 HTTP 响应格式化输出 JSON
上述两种方式涵盖了最常见的场景:解码到有类型的 struct 后调用 json.MarshalIndent (最适合需要验证或检查字段的情况),或用 io.ReadAll 读取原始响应体字节后直接调用 json.Indent (最适合没有 struct 定义时快速进行调试日志记录)。 原始字节方式更简单,但无法提供 Go 类型安全或字段访问——它纯粹是一种显示变换。 只要完整的响应体能放入内存,两种方式都能正确处理大型响应体。
// 模式 A:有类型解码 → MarshalIndent // 需要检查或验证特定字段时使用 var result map[string]any json.NewDecoder(resp.Body).Decode(&result) pretty, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(pretty)) // 模式 B:原始字节 → json.Indent // 用于快速调试日志——无需 struct 定义 body, _ := io.ReadAll(resp.Body) var buf bytes.Buffer json.Indent(&buf, body, "", " ") fmt.Println(buf.String())
Go 项目中的命令行 JSON 格式化
有时你需要直接在终端格式化 JSON payload,无需编写 Go 程序。 这些一行命令是我在开发和故障响应中随时都会用到的。
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
# "service": "payments",
# "port": 8443,
# "workers": 4
# }# 仅格式化 cat api-response.json | jq . # 提取嵌套字段 cat api-response.json | jq '.checks.database' # 过滤数组 cat audit-log.json | jq '.[] | select(.severity == "error")'
# main.go:从 stdin 读取,格式化,写入 stdout
cat <<'EOF' > /tmp/fmt.go
package main
import ("bytes";"encoding/json";"io";"os")
func main() {
b,_:=io.ReadAll(os.Stdin)
var buf bytes.Buffer
json.Indent(&buf,b,""," ")
os.Stdout.Write(buf.Bytes())
}
EOF
echo '{"port":8080,"debug":true}' | go run /tmp/fmt.gogofmt 用于格式化 Go 源代码,而非 JSON。 不要将 JSON 文件通过管道传给 gofmt——它要么报错,要么产生难以辨认的输出。 格式化 JSON 文件请使用 jq . 或 python3 -m json.tool。高性能替代方案 — go-json
对绝大多数 Go 服务来说, encoding/json 的速度已经足够。但如果 JSON 序列化出现在性能分析报告中——这在高吞吐量 REST API 或每次请求都要输出大量结构化日志行的服务中很常见—— go-json 库是一个直接替代品,API 完全相同,速度提升 3–5 倍。
go get github.com/goccy/go-json
package main
import (
// 将这行:
// "encoding/json"
// 替换为这行——API 完全相同,无需修改其他代码:
json "github.com/goccy/go-json"
"fmt"
"log"
)
type AuditEvent struct {
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
Action string `json:"action"`
ResourceID string `json:"resource_id"`
IPAddress string `json:"ip_address"`
DurationMs int `json:"duration_ms"`
}
func main() {
event := AuditEvent{
RequestID: "req_7d2e91", UserID: "usr_4421",
Action: "invoice.download", ResourceID: "inv_9a2f",
IPAddress: "203.0.113.45", DurationMs: 23,
}
data, err := json.MarshalIndent(event, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// 输出与 encoding/json 完全相同——只有速度有差异
}github.com/bytedance/sonic 是目前最快的 Go JSON 库,但它只能在 amd64 和 arm64 上运行(它使用 JIT 编译)。需要跨平台直接替代品时使用 go-json; 确定运行架构且热路径需要极致性能时使用 sonic。
处理大型 JSON 文件
json.MarshalIndent 和 json.Indent 都需要将整个 payload 加载到内存中。对于超过 100 MB 的文件—— 数据导出、审计日志、Kafka 消费者 payload——使用 json.Decoder 流式处理输入并逐条处理记录。
使用 json.Decoder 流式处理大型 JSON 数组
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
type AuditEvent struct {
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
Action string `json:"action"`
Severity string `json:"severity"`
DurationMs int `json:"duration_ms"`
}
func processAuditLog(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
defer file.Close()
dec := json.NewDecoder(file)
// 读取开头的 '['
if _, err := dec.Token(); err != nil {
return fmt.Errorf("read opening token: %w", err)
}
var processed int
for dec.More() {
var event AuditEvent
if err := dec.Decode(&event); err != nil {
return fmt.Errorf("decode event %d: %w", processed, err)
}
// 每次处理一条记录——内存占用恒定
if event.Severity == "error" {
fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
}
processed++
}
fmt.Printf("已处理 %d 条审计事件
", processed)
return nil
}
func main() {
if err := processAuditLog("audit-2026-03.json"); err != nil {
log.Fatalf("process audit log: %v", err)
}
}NDJSON — 每行一个 JSON 对象
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
)
type LogLine struct {
Timestamp string `json:"ts"`
Level string `json:"level"`
Service string `json:"service"`
Message string `json:"msg"`
TraceID string `json:"trace_id"`
DurationMs int `json:"duration_ms,omitempty"`
}
func main() {
file, err := os.Open("service-2026-03.ndjson")
if err != nil {
log.Fatalf("open log: %v", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 每行 1 MB
for scanner.Scan() {
var line LogLine
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
continue // 跳过格式错误的行
}
if line.Level == "error" {
fmt.Printf("%s [%s] %s trace=%s
",
line.Timestamp, line.Service, line.Message, line.TraceID)
}
}
if err := scanner.Err(); err != nil {
log.Fatalf("scan: %v", err)
}
}os.ReadFile 加载一个 500 MB 的 JSON 文件 会在堆上分配整个缓冲区,触发 GC 压力,并可能在内存受限的容器中导致 OOM。常见错误
问题: 用 _ 丢弃错误意味着不可序列化的值(channel、函数、复数)会静默产生 nil 输出,或在下游调用 string(nil) 时引发 panic。
解决: 始终检查错误。如果你序列化的类型应该始终成功,一个会 panic 的 log.Fatalf 比静默数据丢失要好得多。
data, _ := json.MarshalIndent(payload, "", " ") fmt.Println(string(data)) // 如果序列化失败则输出空字符串
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))问题: fmt.Println(string(data)) 在 JSON 之后追加了一个换行符,这会破坏将输出视为原始字节的管道——例如,通过管道传给 jq 或写入二进制协议时。
解决: 使用 os.Stdout.Write(data) 获得干净的二进制输出。如果人类显示需要末尾换行,则显式追加。
data, _ := json.MarshalIndent(cfg, "", " ") fmt.Println(string(data)) // 末尾会多出一个换行符
data, _ := json.MarshalIndent(cfg, "", " ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // 仅在需要时显式添加换行符问题: 不加 omitempty 时,nil *string 或 *int 指针会序列化为 "field": null。这会暴露内部字段名,并可能破坏消费方严格的 JSON schema 验证器。
解决: 对希望在输出中缺失(而非 null)的指针字段添加 omitempty。带 omitempty 的 nil *T 在 JSON 中完全不会产生对应的键。
type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg"` // nil 时输出为 null
}
// {"event_id":"evt_3c7f","error_msg":null}type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg,omitempty"` // nil 时被忽略
}
// {"event_id":"evt_3c7f"}问题: 反序列化到 map[string]any 会丢失类型信息,需要手动类型断言,并产生不确定的键顺序——使 JSON diff 和日志对比更加困难。
解决: 定义带正确 json tag 的 struct。Struct 类型安全、序列化更快、字段顺序与 struct 定义一致且确定,代码也更自文档化。
var result map[string]any json.Unmarshal(body, &result) port := result["port"].(float64) // 需要类型断言,类型错误时会 panic
type ServiceStatus struct {
Service string `json:"service"`
Port int `json:"port"`
Healthy bool `json:"healthy"`
}
var result ServiceStatus
json.Unmarshal(body, &result)
port := result.Port // 有类型,安全,快速encoding/json 与替代方案对比
凡是你控制 struct 定义并需要格式化输出的场景——配置文件、调试日志、测试夹具、 API 响应日志——都使用 json.MarshalIndent。 当你已经有原始字节并且只需要添加空白符,无需经过 Go 类型往返处理时,使用 json.Indent。 只有在性能分析确认 JSON 序列化是可测量的瓶颈后,才切换到 go-json 或 sonic—— 对大多数服务来说,标准库已绰绰有余。
常见问题
如何在 Go 中格式化输出 JSON?
调用 encoding/json 包中的 json.MarshalIndent(v, "", "\t")——第二个参数是每行的前缀(通常为空),第三个参数是每个层级的缩进。传入 "\t" 使用制表符,或传入 " " 使用两个空格。无需外部库;encoding/json 随 Go 标准库一同提供。
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
config := map[string]any{
"service": "payments-api",
"port": 8443,
"region": "cn-north-1",
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// {
// "port": 8443,
// "region": "cn-north-1",
// "service": "payments-api"
// }
}json.Marshal 和 json.MarshalIndent 有什么区别?
json.Marshal 生成紧凑的单行 JSON,没有任何空白——适合对每个字节都要计较的网络传输。json.MarshalIndent 需要额外两个字符串参数(prefix 和 indent),输出带缩进的人类可读结果。两个函数接受相同的值类型并返回 ([]byte, error)。MarshalIndent 的唯一代价是输出字节略多,以及插入空白所需的极少量额外 CPU 时间。
import "encoding/json"
type HealthCheck struct {
Status string `json:"status"`
Version string `json:"version"`
Uptime int `json:"uptime_seconds"`
}
h := HealthCheck{Status: "ok", Version: "1.4.2", Uptime: 86400}
compact, _ := json.Marshal(h)
// {"status":"ok","version":"1.4.2","uptime_seconds":86400}
pretty, _ := json.MarshalIndent(h, "", " ")
// {
// "status": "ok",
// "version": "1.4.2",
// "uptime_seconds": 86400
// }如何在不反序列化 struct 的情况下格式化 JSON []byte?
使用 json.Indent(&buf, src, "", "\t")。该函数接受已有的 JSON []byte,并将缩进后的版本写入 bytes.Buffer——无需定义 struct,无需类型断言,也不需要经过 Go 类型的往返处理。当你已经拥有原始 JSON 字节时(如来自 HTTP 响应体或数据库列),这是最快的选择。
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
raw := []byte(`{"endpoint":"/api/v2/invoices","page":1,"per_page":50,"total":312}`)
var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
// {
// "endpoint": "/api/v2/invoices",
// "page": 1,
// "per_page": 50,
// "total": 312
// }为什么 json.MarshalIndent 会返回错误?
当值无法表示为 JSON 时,encoding/json 会返回错误。最常见的原因有:序列化 channel、函数或复数(这些类型没有对应的 JSON 表示);struct 实现了返回错误的 MarshalJSON();或者使用了非字符串键的 map。需要注意的是,序列化含有未导出字段或 nil 指针字段的 struct 不会导致错误——它们会被直接忽略。
import (
"encoding/json"
"fmt"
)
// 这会返回错误——channel 无法序列化为 JSON
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", " ")
fmt.Println(err)
// json: unsupported type: chan int
// 这没有问题——带 omitempty 的 nil 指针字段会被静默忽略
type Profile struct {
ID string `json:"id"`
Avatar *string `json:"avatar,omitempty"`
}
p := Profile{ID: "usr_7b3c"}
data, _ := json.MarshalIndent(p, "", " ")
// {"id": "usr_7b3c"} — avatar 被忽略,无报错如何在 Go 中排除 JSON 输出的某个字段?
有三种方式。第一,使用 json:"-" struct tag——无论字段值如何,该字段始终被排除。第二,使用 omitempty——仅当字段持有其类型的零值时(nil 指针、空字符串、0、false)才被排除。第三,未导出(小写字母开头)的字段会被 encoding/json 自动排除,无需任何 tag。
type PaymentMethod struct {
ID string `json:"id"`
Last4 string `json:"last4"`
ExpiryMonth int `json:"expiry_month"`
ExpiryYear int `json:"expiry_year"`
CVV string `json:"-"` // 始终排除
BillingName string `json:"billing_name,omitempty"` // 为空时排除
internalRef string // 未导出——自动排除
}
pm := PaymentMethod{
ID: "pm_9f3a", Last4: "4242",
ExpiryMonth: 12, ExpiryYear: 2028,
CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", " ")
// CVV、BillingName(空值)和 internalRef 不会出现在输出中如何在 JSON 序列化中处理 time.Time?
encoding/json 默认将 time.Time 序列化为 RFC3339Nano 格式(如 "2026-03-10T14:22:00Z"),与 ISO 8601 兼容。如果需要不同格式——比如给遗留 API 使用的 Unix 时间戳整数,或自定义的仅日期字符串——可以实现一个嵌入 time.Time 并返回所需格式的包装类型的 MarshalJSON() 方法。
import (
"encoding/json"
"fmt"
"time"
)
// 默认行为——RFC3339Nano,无需自定义代码
type AuditEvent struct {
Action string `json:"action"`
OccurredAt time.Time `json:"occurred_at"`
}
e := AuditEvent{
Action: "invoice.paid",
OccurredAt: time.Date(2026, 3, 10, 14, 22, 0, 0, time.UTC),
}
data, _ := json.MarshalIndent(e, "", " ")
// {
// "action": "invoice.paid",
// "occurred_at": "2026-03-10T14:22:00Z"
// }
// 自定义:Unix 时间戳(整数)
type UnixTime struct{ time.Time }
func (u UnixTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%d", u.Unix())), nil
}相关工具
James is a systems engineer and Go enthusiast who focuses on high-performance microservices, command-line tooling, and infrastructure automation. He enjoys the simplicity and explicitness of Go and writes about building fast, reliable backend systems. When not coding he explores distributed systems concepts and contributes to open-source Go libraries.
Tobias is a platform engineer who builds developer tooling and internal infrastructure in Go. He has authored several open-source CLI tools and contributes to the Go toolchain ecosystem. He writes about the cobra and urfave/cli frameworks, cross-platform binary distribution, configuration management, and the patterns that make Go an ideal language for building reliable, self-contained command-line utilities.