
Go语言json.Marshal返回空JSON对象{},根本原因在于结构体字段未导出(首字母小写),导致encoding/json包通过反射无法访问这些私有字段——这是Go语言导出规则与标准库设计的严格约束,而非bug。
go语言`json.marshal`返回空json对象`{}`,根本原因在于结构体字段未导出(首字母小写),导致`encoding/json`包通过反射无法访问这些私有字段——这是go语言导出规则与标准库设计的严格约束,而非bug。
在Go中,encoding/json包完全依赖反射机制对结构体进行序列化。而Go的反射规则明确规定:只有导出字段(即首字母大写的字段)才能被外部包访问。这意味着,无论你是否添加了json:"xxx"标签,只要字段名以小写字母开头(如isbn、title),它就被视为包级私有字段,json.Marshal会静默忽略该字段——既不报错,也不输出任何内容,最终导致整个结构体序列化为{}或嵌套位置出现null。
以你提供的Book结构体为例:
type Book struct {
isbn string // ❌ 未导出 → 被json包完全忽略
title string // ❌ 同上
author string // ❌
price float32 // ❌
}
尽管rows.Scan能成功将数据库值写入这些字段(因Scan在同一包内操作,不受导出限制),但后续调用json.Marshal(b)时,反射无法读取任何字段,结果必然是空对象数组:[{}, {}, {}]——这正是你在浏览器中看到[{}, {}, {}]、而终端却打印出完整结构体内容(因fmt.Println可直接访问私有字段)的根本矛盾所在。
✅ 正确做法是:将所有需序列化的字段名首字母大写,并配合json标签控制JSON键名,保持API友好性:
type Book struct {
Isbn string `json:"isbn"`
Title string `json:"title"`
Author string `json:"author"`
Price float32 `json:"price"`
}
同时,务必同步更新rows.Scan的参数顺序与字段名:
for rows.Next() {
var bk Book // 直接声明值类型,更简洁安全(避免指针悬空)
if err := rows.Scan(&bk.Isbn, &bk.Title, &bk.Author, &bk.Price); err != nil {
log.Fatal(err)
}
bks = append(bks, bk)
}
⚠️ 关键注意事项:
- 嵌套结构体同理:若Book内嵌其他结构体(如Metadata),其内部字段也必须全部导出;
- *避免使用`[]Book**:除非必要,优先使用[]Book;[]*Book`中若存在nil指针或未初始化结构体,仍可能触发空对象输出;
- 不可序列化类型需规避:func、chan、unsafe.Pointer、含循环引用的指针等类型会导致json.Marshal返回错误,应提前清理或封装;
- 时间类型处理:若结构体含time.Time,确保数据库驱动已启用parseTime=true(如PostgreSQL的?sslmode=disable&parseTime=true),并为其添加合适标签:CreatedAt time.Timejson:"created_at"`。
最后,建议在HTTP handler中增加基础错误处理与Content-Type头:
func getBooksFromDB(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
response, err := json.Marshal(b)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(response)
}
遵循“字段导出是前提,标签控制是优化”这一原则,即可彻底解决json.Marshal返回空对象的问题——这不是调试陷阱,而是深入理解Go语言导出机制与标准库协作逻辑的关键一课。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











