本文介绍一种简洁、类型安全且无需修改数据库或冗余 sql 的 go 语言方案:使用指针字段配合 database/sql 扫描,自动兼容 null,并原生支持 json 序列化输出。
本文介绍一种简洁、类型安全且无需修改数据库或冗余 sql 的 go 语言方案:使用指针字段配合 database/sql 扫描,自动兼容 null,并原生支持 json 序列化输出。
在构建面向遗留数据库的 Go API 时,SQL NULL 值常成为开发者的痛点——Go 的标准 sql.Scan 无法将 NULL 直接映射到基础类型(如 string、int),而强制使用 sql.NullString 等类型又导致结构体臃肿、重复判断、JSON 输出不自然。幸运的是,一个被低估但高度实用的解决方案早已内置于 Go 标准库中:使用指向基础类型的指针作为扫描目标。
该方案的核心原理在于:database/sql 对 *string、*int、*float64、*bool 等指针类型具有原生 NULL 感知能力。当数据库字段为 NULL 时,扫描会将对应指针置为 nil;当值存在时,则分配内存并写入解码后的值。更重要的是,Go 的 json 包对 nil 指针的序列化行为天然符合 REST API 需求:*string 为 nil → JSON 中输出 null;非 nil → 输出字符串字面量。
以下是一个完整可运行的示例:
func queryFamilyRecords(db *sql.DB, surname string) ([]byte, error) {
rows, err := db.Query(
"SELECT mothername, fathername, surname FROM fams WHERE surname = ?",
surname,
)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
defer rows.Close()
// 使用指针字段定义结构体 —— 关键设计
type record struct {
Mname *string `json:"mothername"`
Fname *string `json:"fathername"`
Surname *string `json:"surname"`
}
var records []record
for rows.Next() {
var r record
// 注意:直接传入 r.Mname 等(已是 *string),无需 &r.Mname
if err := rows.Scan(&r.Mname, &r.Fname, &r.Surname); err != nil {
return nil, fmt.Errorf("scan failed: %w", err)
}
records = append(records, r)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return json.Marshal(records)
}
// 调用示例
data, err := queryFamilyRecords(db, "Nullfather")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
// 输出:[{"mothername":"Mary","fathername":null,"surname":"Nullfather"}]
✅ 优势总结:
- 零侵入数据库:无需 COALESCE 或表结构调整;
- 零冗余类型声明:避免数百处 sql.NullString 和 Value()/Valid 判断;
- JSON 友好:nil 指针自动转 null,前端 JavaScript 可直接用 ?. 或 == null 处理;
- 类型安全:编译期检查,无反射或 interface{} 带来的运行时风险;
- 内存高效:仅对实际为 NULL 的字段分配指针,非 nil 时才分配底层值内存。
⚠️ 注意事项:
- 扫描时必须传递指针的地址(如 &r.Mname),因为 rows.Scan 需要修改指针本身(使其指向新分配的字符串);
- 若需在业务逻辑中安全取值,应显式判空:if r.Mname != nil { name := *r.Mname };
- 对于数值类型,*int64 同样适用,但注意 nil 解引用会 panic,务必先检查;
- 此方案不适用于需要将 NULL 统一转换为默认值(如 ""、0)的场景——此时应选用自定义扫描器或 ORM 层拦截,而非指针方案。
综上,利用 Go 指针与 database/sql 的深度集成,是处理遗留数据库 NULL 值最轻量、最符合 Go 语言哲学的方案。它不依赖第三方 ORM,不引入魔法行为,以最小的认知成本换取最大实用性,真正践行了 “less is more” 的工程信条。











