
本文详解 Go 中因结构体字段未导出导致 json.Marshal 返回空对象(如 [{},{}])的根本原因,并提供可直接运行的修复方案,涵盖字段命名规范、SQL 扫描安全实践及符合 RESTful 规范的 API 封装示例。
本文详解 go 中因结构体字段未导出导致 `json.marshal` 返回空对象(如 `[{},{}]`)的根本原因,并提供可直接运行的修复方案,涵盖字段命名规范、sql 扫描安全实践及符合 restful 规范的 api 封装示例。
在 Go 中,encoding/json 包只能序列化导出(exported)字段——即首字母必须大写。你代码中 Post 结构体的字段 name、created_by、created_at 全部以小写字母开头,属于未导出字段,因此 json.Marshal 无法访问它们,最终生成空对象 [{},{},{}]。
✅ 正确写法:将字段名改为大驼峰命名,并保持 JSON tag 清晰语义:
type Post struct {
Title string `json:"title"` // 对应数据库列 title
CreatedBy string `json:"created_by"` // 对应数据库列 created_by
CreatedAt time.Time `json:"created_at"` // 对应数据库列 created_at
}
type PostsResponse struct {
Posts []Post `json:"posts"`
}同时需同步更新 rows.Scan 的参数顺序与字段名匹配(注意:SQL 列顺序必须与 Scan 参数顺序严格一致):
for rows.Next() {
var post Post
// ✅ 字段顺序必须与 SELECT 子句列顺序完全一致
if err := rows.Scan(&post.Title, &post.CreatedBy, &post.CreatedAt); err != nil {
panic(err)
}
posts = append(posts, post)
}⚠️ 关键注意事项:
- 字段导出是硬性要求:Go 的反射机制(json 包依赖反射)无法访问小写字段,这不是 bug,而是语言设计原则。
- *避免使用 `Post指针切片**:你提到“尝试用数组指针但结果不对”,是因为[]*Post在Marshal时若指针指向零值或未初始化结构体,仍会输出空对象;直接使用[]Post` 更安全、简洁。
- 时间类型需启用 parseTime=true:你的 DSN 已含 parseTime=true,这是正确做法,确保 time.Time 能被 MySQL DATETIME/TIMESTAMP 正确解析。
- 务必检查 rows.Err():rows.Next() 循环结束后,应调用 rows.Err() 验证扫描过程是否发生错误(如类型不匹配),否则可能静默丢弃最后一批数据。
? 完整可运行示例(含 HTTP 服务封装):
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
_ "github.com/go-sql-driver/mysql"
)
type Post struct {
Title string `json:"title"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
}
type PostsResponse struct {
Posts []Post `json:"posts"`
}
func getPostsHandler(w http.ResponseWriter, r *http.Request) {
db, err := sql.Open("mysql", "root:1234@/blog?parseTime=true")
if err != nil {
http.Error(w, "DB connection failed", http.StatusInternalServerError)
return
}
defer db.Close()
if err := db.Ping(); err != nil {
http.Error(w, "DB ping failed", http.StatusInternalServerError)
return
}
rows, err := db.Query("SELECT title, created_by, created_at FROM post")
if err != nil {
http.Error(w, "Query failed", http.StatusInternalServerError)
return
}
defer rows.Close()
var posts []Post
for rows.Next() {
var p Post
if err := rows.Scan(&p.Title, &p.CreatedBy, &p.CreatedAt); err != nil {
http.Error(w, "Scan failed", http.StatusInternalServerError)
return
}
posts = append(posts, p)
}
if err := rows.Err(); err != nil {
http.Error(w, "Rows iteration error", http.StatusInternalServerError)
return
}
response := PostsResponse{Posts: posts}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(response)
}
func main() {
http.HandleFunc("/posts", getPostsHandler)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}调用 curl http://localhost:8080/posts 即可获得标准 RESTful 响应:
{
"posts": [
{ "title": "First title", "created_by": "User1", "created_at": "2026-05-15T10:30:00Z" },
{ "title": "My second post", "created_by": "User1", "created_at": "2026-05-16T08:12:45Z" }
]
}? 总结:
Go 的 JSON 序列化强依赖字段可见性。只需三步即可解决本问题:
- 导出字段:所有需 JSON 序列化的结构体字段首字母大写;
- 精准映射:Scan 参数顺序、类型、数量必须与 SQL 查询列严格一致;
-
健壮兜底:检查 rows.Err()、db.Ping() 及 HTTP 错误响应,避免静默失败。
掌握这一模式后,你便可轻松扩展支持任意表、任意列的动态查询 API —— 后续进阶可结合 sql.RawBytes 与反射实现无结构体泛型查询,但对初学者而言,明确结构 + 导出字段是最清晰、最可靠的起点。


















