
本文讲解 Go 中因循环变量地址复用导致的指针错误,通过修正 &val 为 &userLoginKeys[i],确保每个字段获取独立内存地址,从而安全地将 SQL 查询结果扫描到结构体中。
本文讲解 go 中因循环变量地址复用导致的指针错误,通过修正 `&val` 为 `&userloginkeys[i]`,确保每个字段获取独立内存地址,从而安全地将 sql 查询结果扫描到结构体中。
在 Go 中使用 sql.Rows.Scan() 或 sql.Row.Scan() 时,必须传入指向变量的指针,以便数据库驱动将查询值写入对应内存位置。常见误区是误用循环变量的地址,导致所有指针指向同一内存地址,最终所有字段被写入相同值。
问题代码中的关键错误在于:
for i, val := range userLoginKeys {
keys[i] = &val // ❌ 错误:val 是每次迭代的副本,其地址始终相同
fmt.Println(val)
}val 是 range 循环中每次迭代生成的局部副本,其生命周期覆盖整个循环,且 Go 编译器可能复用其栈空间——因此 &val 在每次迭代中返回的是同一个地址(如输出所示:0xc4201ca2c0 0xc4201ca2c0),导致 Scan() 将两列数据都写入该地址,最终 v1 和 v2 输出完全一致。
✅ 正确做法是取切片元素本身的地址:
for i := range userLoginKeys {
keys[i] = &userLoginKeys[i] // ✅ 正确:取 userLoginKeys 切片中第 i 个元素的地址
}但注意:userLoginKeys 是 []string(字段名列表),而 Scan() 需要的是目标结构体字段的地址,不是字段名的地址!因此上述修正虽解决了地址复用问题,却未达成原始目标——将查询结果映射到 UserLogin 实例的 Username 和 Password 字段。
✅ 推荐实现方式(安全、清晰、可维护)
应直接构造结构体实例,并传递各字段的地址:
func Login() {
row := sql.QueryRow("SELECT username, password FROM users WHERE username=?", "golang")
var user UserLogin
// 直接传入结构体字段地址,无需反射获取字段名
err := row.Scan(&user.Username, &user.Password)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Username: %s, Password: %s\n", user.Username, user.Password)
}若需泛化处理任意结构体(如 ORM 场景),应使用反射操作结构体实例的字段,而非字段名切片:
func ScanToStruct(row *sql.Row, dest interface{}) error {
v := reflect.ValueOf(dest).Elem() // 假设 dest 是 *T
if v.Kind() != reflect.Struct {
return fmt.Errorf("dest must be address of struct")
}
// 获取所有可导出字段的地址
fields := make([]interface{}, v.NumField())
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if !field.CanAddr() {
return fmt.Errorf("field %d is not addressable", i)
}
fields[i] = field.Addr().Interface()
}
return row.Scan(fields...)
}
// 使用示例:
// var user UserLogin
// ScanToStruct(row, &user)⚠️ 注意事项
-
structs.Names()返回的是字段名字符串切片([]string),不能用于接收扫描值,它仅用于元信息获取; - 循环中取
&val永远是危险的,应始终避免对range的第二个变量取地址; - 使用
reflect处理Scan时,务必确保目标字段可寻址(CanAddr()为true)且类型兼容(如*string接收TEXT列); - 生产环境建议优先使用显式字段扫描或成熟 ORM(如
sqlx),兼顾性能与可读性。
通过理解 Go 中变量作用域与地址语义,即可规避此类“指针幻影”陷阱,写出健壮的数据库映射逻辑。


















