登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

sqlNull类型处理可空列

来源:17golang原创

时间:2026-11-15 06:12:10 371浏览 收藏

Go 读取 SQL 可空列时,可以把接收变量声明为 sql.Null[string]、sql.Null[int64] 等类型,将地址传给 Scan,先处理错误,再用 Valid 判断是否为 NULL。Valid 为 true 时才读取 V;空字符串、数字零和 false 都可能是有效值,不能代替 NULL 判断。

官方地址:https://pkg.go.dev/database/sql#Null

能力变化:统一为 sql.Null[T]

Go 1.22 引入了泛型 sql.Null[T]。它把已有 NullString、NullInt64 等类型的“值加有效标记”表达方式统一起来。这个变化适合减少数据访问层中可空字段的类型命名差异,并没有改变 SQL 中 NULL 的含义。这里讨论的是这项既有能力的采用方式,不是把它当作刚发布的新功能。

旧项目可以继续使用 sql.NullString。迁移后原来的 String 字段改为 V,Valid 的判定方式保持一致。T 虽然在类型声明里是 any,官方使用约定仍要求选择 driver.Value 接受的类型;常用的 string、int64、float64、bool、time.Time 等更容易与驱动值对应。不能据此把任意业务结构体当作可扫描列。

为什么必须同时保存值与有效标记

假设客户表有一个允许 NULL 的 nickname 列。“尚未设置昵称”可以对应 NULL,“明确设置为空字符串”可以对应空字符串。若业务需要区分二者,读取时使用 COALESCE 把它们统一为一个值,就会丢失这份信息。若业务本来只需要展示兜底文本,则可以有意识地合并它们。

列值接收状态业务含义示例
NULLValid=false尚未设置
空字符串V="",Valid=true已设置为空
普通文本V 为文本,Valid=true已设置内容
SQL可空列与Null值和有效标记的静态关系
图1:可空值结构说明图。列值与 V、Valid 共同构成的状态相对应;图中的关系线不表示执行顺序。

迁移读取代码并处理无行和转换错误

下面的数据访问函数采用 PostgreSQL 的 $1 占位符,并假设调用方已经建立连接池、注册驱动且准备好 customers 表。其他数据库应按驱动要求修改占位符。函数不创建或关闭调用方传入的连接池。

package repository

import (
    "context"
    "database/sql"
    "errors"
    "fmt"
)

// LoadNickname 保留“无此客户”和“客户昵称为 NULL”的区别。
func LoadNickname(ctx context.Context, db *sql.DB, id int64) (sql.Null[string], bool, error) {
    var nickname sql.Null[string]
    err := db.QueryRowContext(ctx,
        "SELECT nickname FROM customers WHERE id = $1", id,
    ).Scan(&nickname)

    if errors.Is(err, sql.ErrNoRows) {
        return sql.Null[string]{}, false, nil
    }
    if err != nil {
        // 包括查询失败和类型转换失败;出错时不消费扫描结果。
        return sql.Null[string]{}, false, fmt.Errorf("读取昵称: %w", err)
    }
    return nickname, true, nil
}

返回的 bool 表示客户行是否存在。行存在且 nickname.Valid 为 false,才表示该行昵称列为 NULL。QueryRowContext 的查询错误会由 Scan 返回,因此错误处理应放在 Scan 后。SQL NULL 被可空类型成功接收,本身不是 ErrNoRows。

迁移旧代码时,除了将类型替换为 sql.Null[string],还要检查所有读取 String 字段的地方,以及结构体字面量的字段名。不要为了统一形式一次性修改稳定接口的所有字段。

写入空值并保留接口模型

sql.Null[T] 同时提供 Value 方法。Valid 为 false 时,它返回表示 SQL NULL 的 nil;Valid 为 true 时,则按该值对应的驱动转换规则提供参数。构造有效的空字符串时,要明确设置 Valid: true。只设置 V 而遗漏 Valid,仍会表示 NULL。

例如 sql.Null[string]{V: "", Valid: true} 表示有效空字符串,sql.Null[string]{} 表示空值。执行更新时应通过占位符绑定参数,并处理 ExecContext 返回的错误。参数类型不能代替数据库约束检查,NOT NULL 列仍不能接受 SQL NULL。

数据库可空类型和业务接口模型的职责结构
图2:接口职责结构说明图。Scan 与读取契约相关,Value 与参数契约相关;API 模型应独立决定对外空值表达。

数据库模型也不宜直接作为对外 JSON 契约。默认 encoding/json 编码会看到导出的 V 与 Valid 字段,不会自动把无效状态变成一个 JSON null。可以在响应层映射为指针或编写明确的序列化逻辑。对外字段是否缺省、是否允许显式清空,是另外一层协议设计;仅用一个空值标记不能表达 PATCH 请求的全部三态语义。

用小样例理解三种状态

以下是无需数据库驱动的独立演示,用 Scan 方法接收 nil、空字符串和文本,再查看相应的 Value。它便于理解类型契约,不能替代目标数据库与驱动的集成检查。这里列出代码和预期判断,没有声称已在本机执行。

package main

import (
    "database/sql"
    "fmt"
    "log"
)

func main() {
    // nil 对应 SQL NULL;后两项都是有效字符串。
    for _, input := range []any{nil, "", "青禾"} {
        var nickname sql.Null[string]
        if err := nickname.Scan(input); err != nil {
            log.Fatal(err)
        }
        value, err := nickname.Value()
        if err != nil {
            log.Fatal(err)
        }
        // 先看 Valid,再解释 V;Value 返回 nil 表示空值参数。
        fmt.Printf("Valid=%t V=%q parameter=%#v\n",
            nickname.Valid, nickname.V, value)
    }
}

按类型契约,三项的 Valid 分别应为 false、true、true;第一项的参数值为 nil,第二项仍为空字符串。实际接入数据库时,还应关注驱动的文本、日期和数值返回类型,以及列值无法转换时的错误处理。

已经使用 NullString,必须迁移吗?

没有这个必要。工具链低于 Go 1.22 时不能采用泛型 Null。满足版本条件后,可以在新代码或局部重构中逐步使用,保留既有业务语义比统一拼写更重要。

Valid 能判断 SQL 查询有没有结果吗?

不能。查询无行应判断 Scan 返回的 sql.ErrNoRows;Valid 描述的是成功接收后某个列值是否为 NULL。

数值为零时应当把 Valid 改为 false 吗?

应当由业务输入是否为空决定。有效的零金额、零数量或者 false 都可以与 Valid=true 同时存在。

延伸阅读:https://go.dev/doc/go1.22;接口与类型约定:https://pkg.go.dev/database/sql#Null。选择可空类型后,最关键的动作仍是保留“值是否存在”这份信息,并在数据库层和接口层分别解释它。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>