
本文揭示 go 中使用 database/sql 查询 mysql 时返回全空结构体的典型原因——数据库连接在 rows 被消费前已被关闭,导致扫描失败且错误被忽略。
在 Go 的 database/sql 包中,*sql.Rows 是一个惰性迭代器,它本身不持有数据快照,而是依赖底层数据库连接持续拉取结果。一旦连接关闭,rows.Next() 将立即返回 false(且后续调用 rows.Err() 会返回类似 "sql: Rows are closed" 的错误),但若未显式检查错误,程序会静默跳过所有循环体,最终返回一个长度正确但元素全为零值的切片——这正是你观察到“空 JSON 数组(实际长度非零,但每个 Party 字段均为零值)”的根本原因。
? 问题定位:defer con.Close() 的作用域陷阱
你的 getRowsFromSql 函数存在关键缺陷:
func getRowsFromSql(query string) *sql.Rows {
con, err := sql.Open("mysql", dbConnectString)
if err != nil {
panic(err)
}
defer con.Close() // ❌ 错误:函数返回前即关闭连接!
rows, err2 := con.Query(query)
if err != nil { // ⚠️ 这里应检查 err2,而非 err
panic(err2)
}
return rows // 此时 con 已被 defer 关闭!rows 不可用
}defer con.Close() 在 getRowsFromSql 函数返回时执行,而 rows 仅是对该连接的引用。当 scanForParties(rows) 开始调用 rows.Next() 时,连接早已释放,rows.Scan() 实际未写入任何值(字段保持 Go 默认零值:0, "", nil 等),却因未检查 rows.Err() 和 rows.Scan() 的返回错误而无法察觉。
✅ 对比你“旧版能工作”的代码:连接 con 在 getParties 函数作用域内保持活跃,rows 生命周期与连接一致,故可安全遍历。
✅ 正确实践:连接生命周期必须覆盖 rows 消费全程
推荐重构为 连接与查询在同一作用域管理,并严格校验每一步错误:
func getParties(w http.ResponseWriter, r *http.Request) {
con, err := sql.Open("mysql", dbConnectString)
if err != nil {
http.Error(w, "DB connection failed: "+err.Error(), http.StatusInternalServerError)
return
}
defer con.Close() // ✅ 在 handler 结束时关闭,确保 rows 可用
rows, err := con.Query("SELECT id, name, author, datetime, datetime_to, host, location, description, longitude, latitude, primary_image_id FROM parties")
if err != nil {
http.Error(w, "Query failed: "+err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close() // ✅ 显式关闭 rows(虽非必须,但属最佳实践)
var parties []Party
for rows.Next() {
var p Party
// 注意:字段顺序、类型必须与 SELECT 和 struct 完全匹配!
// 表中字段名为 'longitude'/'latitude',非 'longtitude'/'latitude'(原代码拼写错误!)
err := rows.Scan(
&p.Id,
&p.Name,
&p.Author,
&p.Datetime,
&p.Datetime_to,
&p.Host,
&p.Location,
&p.Description,
&p.Longtitude, // ⚠️ 修正:对应表中 'longitude'
&p.Latitude, // ⚠️ 修正:对应表中 'latitude'
&p.PrimaryImgId,
)
if err != nil {
http.Error(w, "Scan failed: "+err.Error(), http.StatusInternalServerError)
return
}
parties = append(parties, p)
}
// 检查 rows 遍历是否因错误中断
if err := rows.Err(); err != nil {
http.Error(w, "Rows iteration error: "+err.Error(), http.StatusInternalServerError)
return
}
jsonBytes, err := json.Marshal(parties)
if err != nil {
http.Error(w, "JSON marshal failed: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(jsonBytes)
}⚠️ 其他关键注意事项
- 字段名拼写一致性:你的 Party 结构体中 Longtitude(多了一个 t)与数据库字段 longitude 不匹配,Scan 会静默失败。务必保持一致(推荐使用 db 标签或重命名结构体字段)。
- *避免 `select **:显式列出字段(如示例所示),防止表结构变更导致Scan` 参数错位;同时提升可读性与性能。
-
使用 sql.NullString 等处理 NULL:MySQL 中 author、datetime 等允许 NULL,而 string 类型无法接收 NULL。应改用 sql.NullString 并在赋值时判断:
var author sql.NullString // ... Scan(&author, ...) p.Author = author.String // author.Valid 为 true 时才有效
- 连接池复用:sql.Open 返回的是连接池,不应在每次请求时新建;应在应用启动时初始化一次,并全局复用 *sql.DB。
总结
Go 中 database/sql 的核心原则是:*`sql.Rows的有效性完全依赖于其来源连接的存活状态**。将defer con.Close()放在rows被完整消费之后的作用域(如 HTTP handler 函数末尾),并始终检查rows.Err()和Scan()` 错误,是避免“空结构体”陷阱的黄金法则。同时,结构体字段与数据库列的精确映射、NULL 值安全处理,共同构成健壮数据库交互的基础。










