
在 go 中使用 database/sql 扫描多行数据时,若重复复用同一字符串切片变量并追加到二维切片中,会导致所有行引用同一底层数组,从而出现“后一行覆盖前一行”的现象——根本原因在于 go 切片的引用语义与内存共享机制。
在 go 中使用 database/sql 扫描多行数据时,若重复复用同一字符串切片变量并追加到二维切片中,会导致所有行引用同一底层数组,从而出现“后一行覆盖前一行”的现象——根本原因在于 go 切片的引用语义与内存共享机制。
这是一个在 Go 数据库编程中高频出现却容易被忽视的典型陷阱:切片的引用特性导致多行数据相互污染。
在您提供的代码中,result := make([]string, len(cols)) 仅在循环外声明一次,随后每次 results = append(results, result) 实际上是将同一个切片头(slice header)的副本追加进了 results。而 Go 的切片包含三个字段:指向底层数组的指针、长度(len)和容量(cap)。当所有 result 共享同一底层数组时,后续 rows.Scan() 循环中对 result[i] 的赋值会不断覆盖该数组中的内容——最终 results 中所有子切片都指向相同的数据,只保留最后一行的值。
✅ 正确做法:为每一行创建独立的、内存隔离的字符串切片。
以下是修复后的完整示例(含错误预防与类型安全增强):
// 假设 cols 已通过 rows.Columns() 获取,且 rows 为 *sql.Rows
cols, err := rows.Columns()
if err != nil {
log.Fatal("Failed to get columns:", err)
}
nCols := len(cols)
var results [][]string
for rows.Next() {
// ✅ 关键修复:每行新建独立切片
result := make([]string, nCols)
rawResult := make([]interface{}, nCols)
dest := make([]interface{}, nCols)
// 构造 scan 目标指针数组(指向 rawResult 元素)
for i := range rawResult {
dest[i] = &rawResult[i]
}
if err := rows.Scan(dest...); err != nil {
log.Fatal("Failed to scan row:", err)
}
// 类型转换:安全映射 rawResult → result
for i, v := range rawResult {
switch x := v.(type) {
case nil:
result[i] = ""
case []byte:
result[i] = string(x) // 避免直接引用原始字节切片
case string:
result[i] = x
case int64:
result[i] = strconv.FormatInt(x, 10)
case float64:
result[i] = strconv.FormatFloat(x, 'f', -1, 64)
case bool:
result[i] = strconv.FormatBool(x)
case time.Time:
result[i] = x.Format(time.RFC3339) // 推荐比 .String() 更可控
default:
log.Printf("Warning: unsupported type %T at column %d", x, i)
result[i] = fmt.Sprintf("%v", x)
}
}
// ✅ 追加的是新分配的独立切片(值拷贝 slice header,但底层数组互不干扰)
results = append(results, result)
}
if err := rows.Err(); err != nil {
log.Fatal("Rows iteration error:", err)
}? 关键要点总结:
- ❌ 错误模式:result 复用 + append(results, result) → 所有子切片共享底层数组;
- ✅ 正确模式:result := make([]string, nCols) 置于 for rows.Next() 内部 → 每次迭代分配新底层数组;
- ? 补充建议:可考虑使用结构体(struct)替代 []string 存储行数据,提升可读性与类型安全性;若需高性能大批量处理,建议结合 sql.RawBytes 或流式处理避免全量内存驻留;
- ⚠️ 注意:[]byte 转 string 时,string(x) 会复制字节,确保不意外持有数据库驱动内部缓冲区引用(某些驱动可能复用底层 buffer)。
掌握 Go 切片的内存模型,是写出健壮数据库交互代码的基石。每一次 make([]T, N) 都应明确其生命周期边界——尤其在循环中,独立分配 = 数据安全。










