You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何sql.Scan未在应返回时抛出ErrNoRows错误?

问题:sqlc生成的GetAccount函数未返回ErrNoRows错误,而是返回零值Account实例

当调用sqlc生成的GetAccount函数传入不存在的ID时,预期返回sql.ErrNoRows错误,但实际返回带有零值的Account实例且无错误。相关代码如下:

GetAccount实现代码

const getAccount = `-- name: GetAccount :one
SELECT id, owner, balance, currency, created_at
FROM accounts
WHERE id = $1
`
func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) {
    row := q.db.QueryRowContext(ctx, getAccount, id)
    var i Account
    err := row.Scan(
      &i.ID,
      &i.Owner,
      &i.Balance,
      &i.Currency,
      &i.CreatedAt,
    )
    return i, err
}

调用代码(Gin请求处理器)

type getAccountRequest struct {
    ID int64 `uri:"id" binding:"required,min=1"`
}

func (server *Server) getAccount(ctx *gin.Context) {
    var request getAccountRequest
    err := ctx.ShouldBindUri(&request)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, errorResponse(err))
        return
    }

    account, err := server.store.GetAccount(ctx, request.ID) // 调用GetAccount
    if err == sql.ErrNoRows {
        ctx.JSON(http.StatusNotFound, errorResponse(err))
        return
    } else if err != nil {
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }
    ctx.JSON(http.StatusOK, account)
}

根据row.Scan官方文档:

func (r *Row) Scan(dest ...any) error
Scan将匹配行的列复制到dest指向的值中。详情请参阅Rows.Scan的文档。如果查询匹配多行,Scan使用第一行并丢弃其余行。如果无匹配行,Scan返回ErrNoRows。


原因分析与解决方案

1. 数据库驱动的ErrNoRows与标准库sql.ErrNoRows不匹配

这是最常见的原因。部分数据库驱动(如pgx)会自定义专属的ErrNoRows错误实例,而非直接复用标准库的sql.ErrNoRows。此时直接用err == sql.ErrNoRows做等值比较会失败,导致你误以为没有返回错误。

解决方案:
使用标准库的errors.Is函数判断错误类型,它会递归检查错误链中的匹配项:

account, err := server.store.GetAccount(ctx, request.ID)
if errors.Is(err, sql.ErrNoRows) {
    ctx.JSON(http.StatusNotFound, errorResponse(err))
    return
} else if err != nil {
    ctx.JSON(http.StatusInternalServerError, errorResponse(err))
    return
}

2. 查询语句意外返回NULL行

虽然你的查询是WHERE id = $1,但如果存在逻辑错误(比如误写为LEFT JOIN其他表,或者输入ID被意外转换为NULL),可能导致查询返回一行全为NULL的结果,此时row.Scan不会返回ErrNoRows,而是将零值写入Account实例。

解决方案:

  • 直接在数据库中执行SELECT id, owner, balance, currency, created_at FROM accounts WHERE id = [不存在的ID],确认是否真的没有返回行。
  • 检查输入的ID是否被正确传递到查询中,避免出现ID被意外转换为NULL的情况。

3. sqlc代码被修改或中间层包装了错误

如果GetAccount函数被手动修改过,或者server.store是经过包装的实例,可能存在错误被吞掉或修改的情况。

解决方案:

  • 确认GetAccount函数严格由sqlc生成,未被手动修改。
  • 在GetAccount函数中添加日志,打印err的具体值:
    func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) {
        row := q.db.QueryRowContext(ctx, getAccount, id)
        var i Account
        err := row.Scan(
          &i.ID,
          &i.Owner,
          &i.Balance,
          &i.Currency,
          &i.CreatedAt,
        )
        log.Printf("GetAccount id=%d, err=%v", id, err) // 添加日志
        return i, err
    }
    
    调用后查看日志,确认是否真的没有返回错误,或者错误的具体类型。

内容的提问来源于stack exchange,提问作者SSMSJM

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 00:55:54