
在 Go 中使用 rows.Scan() 读取多行 SQL 数据时,若重复复用同一 []string 切片并追加到结果集合中,会导致所有行数据最终指向同一内存地址,从而出现“后一行覆盖前一行”的现象——根本原因在于切片的底层指针特性。
在 go 中使用 `rows.scan()` 读取多行 sql 数据时,若重复复用同一 `[]string` 切片并追加到结果集合中,会导致所有行数据最终指向同一内存地址,从而出现“后一行覆盖前一行”的现象——根本原因在于切片的底层指针特性。
这是一个在 Go 数据库编程中高频出现、却容易被忽视的典型陷阱:切片不是值类型,而是包含底层数组指针、长度和容量的结构体。当你反复修改同一个 result []string 并执行 results = append(results, result) 时,实际追加的是对同一底层数组的多个引用。后续 rows.Scan() 覆盖 rawResult → 转换赋值到 result[i] → 所有已追加的 result 实例同步“看到”最新值,最终 results 中所有子切片内容完全一致。
✅ 正确做法:为每一行创建独立的字符串切片
关键修复点在于:每次循环迭代都应分配全新的 []string 实例,而非复用旧变量。以下是重构后的健壮实现:
var results [][]string
for rows.Next() {
// ✅ 每行独立分配 rawResult 和 result
rawResult := make([]interface{}, len(cols))
dest := make([]interface{}, len(cols))
for i := range rawResult {
dest[i] = &rawResult[i]
}
if err := rows.Scan(dest...); err != nil {
log.Fatal("Failed to scan row:", err)
}
// ✅ 每行新建 result 切片(关键!)
result := make([]string, len(cols))
for i, raw := range rawResult {
switch v := raw.(type) {
case int64:
result[i] = strconv.FormatInt(v, 10)
case float64:
result[i] = strconv.FormatFloat(v, 'f', -1, 64)
case bool:
result[i] = strconv.FormatBool(v)
case []byte:
result[i] = string(v) // 避免直接转换 raw.([]byte) —— 类型断言更安全
case string:
result[i] = v
case time.Time:
result[i] = v.String()
case nil:
result[i] = ""
default:
log.Fatalf("Unexpected type %T in column %d", v, i)
}
}
results = append(results, result) // 此时 result 指向唯一底层数组
}⚠️ 补充注意事项
- 避免提前声明 result 在循环外:这是本问题的根源。切片变量本身可复用,但其底层数组必须隔离。
- dest 切片可复用(但需谨慎):dest 仅用于传递扫描地址,只要其元素始终指向当前 rawResult 的有效地址,复用是安全的;但若 rawResult 生命周期或作用域变化,则不可复用。
- 考虑使用结构体替代泛型切片:对于固定表结构,定义 type User struct { ID int; Name string; CreatedAt time.Time } 并用 rows.Scan(&u.ID, &u.Name, &u.CreatedAt) 更安全、高效、可读。
- 及时调用 rows.Err():循环结束后检查 rows.Err() 是否非 nil,确保扫描过程无隐式错误。
? 一句话总结
Go 切片的“引用语义”决定了:追加切片到父切片时,追加的是头信息(指针+len+cap),而非数据副本;要保存多行独立数据,就必须为每一行分配新的底层数组——最直接的方式就是 make([]string, len(cols)) 放在 for rows.Next() 循环体内。










