
本文详解 Go 中如何正确将数据库查询结果序列化为标准 JSON 格式,重点解决因结构体字段未导出导致 json.Marshal 返回空对象的常见错误,并提供完整可运行示例、最佳实践与 API 封装建议。
本文详解 go 中如何正确将数据库查询结果序列化为标准 json 格式,重点解决因结构体字段未导出导致 `json.marshal` 返回空对象的常见错误,并提供完整可运行示例、最佳实践与 api 封装建议。
在 Go 语言中,将 SQL 查询结果转换为 JSON 是构建 RESTful API 的核心环节。但新手常遇到一个“静默失败”问题:结构体字段值能正常打印,json.Marshal 却输出 [{},{}] —— 这几乎总是由字段未导出(即小写首字母) 引起。Go 的 encoding/json 包仅能访问并序列化首字母大写的导出字段(exported fields),私有字段(小写开头)会被忽略,无论是否设置了 json tag。
✅ 正确结构体定义:首字母必须大写
请将你的 Post 结构体修改为:
type Post struct {
Title string `json:"title"` // ✅ 导出字段,对应数据库列 title
CreatedBy string `json:"created_by"` // ✅ 导出字段
CreatedAt time.Time `json:"created_at"` // ✅ 导出字段
}
⚠️ 注意:name → Title(映射 title 列)、created_by → CreatedBy(映射 created_by 列)。Go 不支持自动蛇形转驼峰,必须显式匹配列名或在 Scan 时按顺序赋值。
✅ 完整修复版示例(含错误处理与 HTTP 服务封装)
以下是一个生产就绪的简化版本,已修复所有关键问题:
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
_ "github.com/go-sql-driver/mysql"
)
// ✅ 导出字段 + 明确 JSON 键名
type Post struct {
Title string `json:"title"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
}
// ✅ 封装响应结构,符合 REST API 惯例
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
// ✅ 按 SELECT 列顺序 Scan:title → Title, created_by → CreatedBy, created_at → CreatedAt
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")
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, "JSON encode failed", http.StatusInternalServerError)
return
}
}
func main() {
http.HandleFunc("/posts", getPostsHandler)
fmt.Println("Server running on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
? 关键要点总结
- 导出规则是铁律:所有需 JSON 序列化的字段必须首字母大写(如 Title),否则 json 包完全不可见;
- Scan 顺序必须严格匹配 SQL SELECT 列序:rows.Scan(&p.Title, &p.CreatedBy, &p.CreatedAt) 对应 SELECT title, created_by, created_at;
- 使用 json.NewEncoder(w).Encode() 替代 json.Marshal() + fmt.Fprintf:更安全(自动处理流、避免中间字节切片)、支持 HTTP 流式响应;
- 务必检查 rows.Err():for rows.Next() 循环后需验证是否有扫描错误(如类型不匹配);
- HTTP 响应头不可省略:Content-Type: application/json; charset=utf-8 确保前端正确解析中文与特殊字符。
? 进阶建议(生产环境)
- 使用连接池配置(db.SetMaxOpenConns, db.SetMaxIdleConns);
- 将数据库初始化提取为独立函数,避免每次请求重建连接;
- 引入 sqlx 库支持结构体自动映射(sqlx.Select(&posts, query, args));
- 对敏感字段(如密码)添加 json:"-" 忽略 tag;
- 添加日志与监控(如 log.Printf("Fetched %d posts", len(posts)))。
遵循以上规范,你就能稳定输出符合前端预期的 JSON 数据,例如:
{
"posts": [
{ "title": "First title", "created_by": "User1", "created_at": "2026-05-15T14:22:33Z" },
{ "title": "My second post", "created_by": "User1", "created_at": "2026-05-16T09:08:12Z" }
]
}
——简洁、标准、可靠,正是 Go “少即是多”哲学的最佳实践。











