如何安全检查 MySQL 查询返回的结构体数组是否为空

在 go 中使用 database/sql 查询单行数据时,若误用 query 而非 queryrow,可能导致对空结果集调用 len() 或遍历时 panic;正确做法是优先使用 queryrow 处理单行预期场景,或通过 rows.next() 循环配合标志位判断结果是否存在。

当你执行类似 SELECT id, secret, shortname FROM beehives WHERE shortname = ? 这类预期最多返回一行的查询时,应避免使用 db.Query() 返回 *sql.Rows 后直接对底层切片做 len(rows) 检查——因为 *sql.Rows 并非 Go 原生切片类型(它是一个封装结构体),其本身不支持 len() 操作,强行调用会触发 runtime panic。同理,rows == nil 也无效,因为 db.Query() 即使无结果也会返回一个非 nil 的 *sql.Rows 实例。

推荐方案:使用 db.QueryRow()(适用于单行场景)
QueryRow 是专为“至多一行”设计的 API,它始终返回非 nil 的 *sql.Row,并将错误(如无匹配行)延迟到 .Scan() 时抛出:

var id int
var secret, shortname string
err := db.QueryRow(
    "SELECT id, secret, shortname FROM beehives WHERE shortname = ?",
    beehive,
).Scan(&id, &secret, &shortname)

switch {
case err == sql.ErrNoRows:
    log.Printf("蜂箱 %s 未找到", beehive)
case err != nil:
    log.Fatal("查询失败:", err)
default:
    fmt.Printf("查得蜂箱: ID=%d, 密钥=%s, 简称=%s\n", id, secret, shortname)
}

⚠️ 注意:务必使用 ? 占位符进行参数化查询,而非字符串拼接(防止 SQL 注入),原示例中 '%s' 的写法存在严重安全风险。

❌ 若必须使用 db.Query()(例如需兼容多行但当前逻辑只取首行),则需通过迭代判断:

rows, err := db.Query(
    "SELECT id, secret, shortname FROM beehives WHERE shortname = ?",
    beehive,
)
if err != nil {
    log.Fatal("查询执行失败:", err)
}
defer rows.Close()

found := false
for rows.Next() {
    var id int
    var secret, shortname string
    if err := rows.Scan(&id, &secret, &shortname); err != nil {
        log.Fatal("扫描行失败:", err)
    }
    // 处理第一行后即可 break(若仅需首行)
    fmt.Printf("匹配到蜂箱: %s (ID=%d)\n", shortname, id)
    found = true
    break // 可选:仅处理首行
}

if !found {
    log.Printf("蜂箱 %s 不存在", beehive)
}

// 必须检查 rows.Err() —— 迭代结束后确认无底层错误
if err := rows.Err(); err != nil {
    log.Fatal("遍历结果集时出错:", err)
}

? 关键要点总结:

  • len() 和 == nil 对 *sql.Rows 无效,因其不是切片;
  • 单行查询首选 QueryRow + Scan,语义清晰且自动处理 ErrNoRows;
  • 多行场景下,用 rows.Next() 循环驱动,并以布尔标志(如 found)记录是否进入循环体;
  • 每次使用 *sql.Rows 后必须调用 rows.Close()(建议 defer),并检查 rows.Err() 确保无迭代异常;
  • 坚决使用参数化查询(? 占位符),杜绝字符串拼接 SQL。