因为sql.rows是游标式状态机,不能直接marshal;必须先用rows.columns()和rows.scan()提取列名与字段值,再归一化类型(如[]byte→string、time.time→rfc3339字符串、sql.null解包),最后构造[]map[string]interface{}交由json.marshal序列化。

为什么直接用 json.Marshal 会失败
Go 的 database/sql 返回的 *sql.Rows 是游标式结构,不能直接传给 json.Marshal —— 它既不是 slice 也不是 struct,而是一个需要手动遍历的状态机。常见错误是写 json.Marshal(rows),结果得到空对象或 panic:json: unsupported type: *sql.Rows。
真正要序列化的,是每一行的字段值([]interface{})及其列名([]string),且需处理 nil、[]byte、时间类型等 Go 特有类型。
如何安全读取 *sql.Rows 并提取结构化数据
必须调用 rows.Columns() 获取列名,再用 rows.Scan 遍历每行。关键点在于:所有字段必须用 []interface{} 接收,否则类型不匹配会导致 panic(比如用 *string 接 NULL 字段)。
- 先调用
rows.ColumnTypes()或rows.Columns()拿列名,避免硬编码 - 为每行分配一个
[]interface{}切片,每个元素指向一个变量(用&v形式传给Scan) -
Scan后需检查err == sql.ErrNoRows(无数据)和err != nil(类型不匹配/驱动问题) - 注意:
sql.NullString等类型需显式解包,否则 JSON 会输出整个 struct 而非原始值
如何处理常见 SQL 类型到 JSON 的映射
数据库类型和 Go 类型之间没有一一对应关系,不同驱动行为也不同(如 pgx 和 mysql 对 TIMESTAMP 的默认返回类型不同)。最稳妥的方式是统一转成 interface{},并在序列化前做类型归一化:
使用 JSON Schema 验证 JSON 数据,从示例 JSON 生成 schema,并将其转换为 TypeScript 接口、Python 数据类或 Markdown 文档。
-
[]byte→string(通常是 TEXT/BLOB 字段,直接string(b)即可) -
time.Time→string(建议用t.Format(time.RFC3339),避免时区丢失) -
sql.Null*类型 → 检查.Valid,true时取.Value,否则设为nil -
int64/float64等基本类型保持原样,json.Marshal能正确处理
示例片段:
value := row[i]
switch v := value.(type) {
case []byte:
result[i] = string(v)
case time.Time:
result[i] = v.Format(time.RFC3339)
case sql.NullString:
if v.Valid { result[i] = v.String } else { result[i] = nil }
default:
result[i] = v
}
为什么不要自己拼 JSON 字符串
有人用 fmt.Sprintf 或字符串拼接生成 JSON,这看似简单,但极易出错:字段名含引号、值含换行、数字被当字符串、null 写成 "null" 字面量……这些都会让前端解析失败。
正确做法始终是构造 Go 原生数据结构(如 []map[string]interface{}),再交由 json.Marshal 处理。它自动转义、区分类型、处理 nil 为 null,且兼容标准 JSON 规范。
真正容易被忽略的是内存和性能:如果查询返回 10 万行,构造中间 []map[string]interface{} 会吃掉大量堆内存。生产环境应考虑流式写入(json.Encoder 直接写 response body),而非全量构建后再 marshal。










