
在 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, 简称=%sn", 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。