
本文详解如何在 Go 中正确将数据库查询结果序列化为结构清晰、符合 API 规范的 JSON 数据,重点解决因字段未导出导致 json.Marshal 返回空对象 [{},{},{}] 的典型问题,并提供完整可运行示例与最佳实践。
本文详解如何在 Go 中正确将数据库查询结果序列化为结构清晰、符合 API 规范的 JSON 数据,重点解决因字段未导出导致 `json.Marshal` 返回空对象 `[{},{},{}]` 的典型问题,并提供完整可运行示例与最佳实践。
在构建 RESTful API 时,将 SQL 查询结果封装为标准 JSON 响应是后端开发的核心能力之一。Go 语言因其高性能与简洁语法成为 API 开发的热门选择,但初学者常因 Go 的导出规则(Export Rule) 踩坑:若结构体字段首字母小写(如 name),则该字段对 encoding/json 包不可见,导致序列化后为空对象 {"title":"","created_by":"","created_at":"0001-01-01T00:00:00Z"} 或整个数组呈现为 [{},{}]。
✅ 正确做法:所有需 JSON 序列化的字段必须首字母大写(即导出),并配合 json 标签精确控制键名:
type Post struct {
Title string `json:"title"` // ✅ 导出字段 + 显式标签
CreatedBy string `json:"created_by"` // ✅ 驼峰转下划线
CreatedAt time.Time `json:"created_at"` // ✅ time.Time 可直接序列化(需 parseTime=true)
}同时,为满足常见 API 响应规范(如 { "posts": [...] }),建议定义响应包装结构体,而非直接序列化切片:
type PostsResponse struct {
Posts []Post `json:"posts"`
}
// 使用示例:
response := PostsResponse{Posts: posts}
data, err := json.Marshal(response)
if err != nil {
http.Error(w, "JSON marshal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(data)? 关键注意事项:
- 数据库驱动参数:MySQL 连接字符串中务必包含 parseTime=true(如 "root:1234@/blog?parseTime=true"),否则 time.Time 字段将无法正确解析;
- 错误处理需严谨:rows.Scan() 后应检查 err,且循环结束后需调用 rows.Err() 判断扫描过程是否发生错误;
- 资源释放:使用 defer rows.Close() 是良好习惯,但注意它仅在函数返回时执行,若循环中 panic 可能跳过;更健壮的做法是在 for rows.Next() 后显式检查 rows.Err();
- SQL 注入防护:本例为静态查询,若涉及用户输入,请始终使用参数化查询(db.Query("SELECT ... WHERE id = ?", id)),杜绝字符串拼接。
? 完整修复版示例(含 HTTP Server):
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}
data, err := json.Marshal(response)
if err != nil {
http.Error(w, "JSON marshal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data)
}
func main() {
http.HandleFunc("/post", getPostsHandler)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}? 进阶建议:
- 对于高并发场景,推荐使用连接池(db.SetMaxOpenConns() / db.SetMaxIdleConns());
- 生产环境应将数据库配置外置(如通过环境变量或配置文件);
- 可结合 sqlx 库简化结构体扫描(支持按字段名自动绑定);
- 若需分页、排序、搜索等能力,应在 SQL 层添加 LIMIT/OFFSET 和 WHERE 子句,避免全量加载。
掌握导出规则与结构体设计,是 Go Web 开发的基石。一次正确的字段命名,胜过十次调试空 JSON —— 简洁、安全、可维护,正是 Go 哲学的最佳体现。


















