
本文揭示了 Go 中因循环变量地址复用导致的指针误用问题,并提供安全、可复用的解决方案,帮助开发者正确构建 []interface{} 切片以配合 sql.Row.Scan() 将查询结果映射到结构体字段。
本文揭示了 go 中因循环变量地址复用导致的指针误用问题,并提供安全、可复用的解决方案,帮助开发者正确构建 []interface{} 切片以配合 sql.row.scan() 将查询结果映射到结构体字段。
在使用 Go 的 database/sql 包进行数据库查询时,一个常见需求是将 SELECT 返回的列值自动填充到结构体字段中。为此,开发者常借助反射库(如 github.com/fatih/structs)获取字段名,再构造 []interface{} 切片,每个元素为对应字段的地址,最后传入 row.Scan()。
但上述代码存在一个典型陷阱:
userLoginKeys := structs.Names(UserLogin{}) // 返回 []string{"Username", "Password"}
keys := make([]interface{}, len(userLoginKeys))
for i, val := range userLoginKeys {
keys[i] = &val // ⚠️ 错误!val 是循环变量,每次迭代都复用同一内存地址
}由于 val 是 for range 循环中的单个变量副本,其地址在整个循环中保持不变(即 &val 始终指向同一个栈位置),因此 keys[0] 和 keys[1] 实际存储的是同一个指针地址。当 row.Scan() 写入数据时,两次写入均作用于该地址,最终两个字段被赋予相同值——这正是日志中 0xc4201ca2c0 重复出现、且 v1 == v2 的根本原因。
✅ 正确做法是取数组中每个元素本身的地址:
for i := range userLoginKeys {
keys[i] = &userLoginKeys[i] // ✅ 正确:取切片第 i 个字符串的地址
}但需注意:structs.Names() 返回的是字段名字符串切片([]string),而 Scan() 需要的是目标结构体字段的地址,不是字段名的地址。因此,仅修正指针地址仍无法达成“映射到 UserLogin 结构体”的原始目标——上述代码实际只是把查询结果存进了 userLoginKeys 这个临时字符串切片,而非 UserLogin{} 实例。
? 真正健壮的解决方案应直接操作结构体实例:
func Login() error {
row := sql.QueryRow("SELECT username, password FROM users WHERE username = ?", "golang")
var user UserLogin
// 直接传递结构体字段地址(推荐)
err := row.Scan(&user.Username, &user.Password)
if err != nil {
return err
}
fmt.Printf("Login success: %+v\n", user)
return nil
}若需完全动态(例如字段数未知或结构体类型由配置决定),应基于结构体实例反射获取字段地址:
func scanToStruct(row *sql.Row, dest interface{}) error {
v := reflect.ValueOf(dest)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
return fmt.Errorf("dest must be a pointer to struct")
}
sv := v.Elem()
numField := sv.NumField()
values := make([]interface{}, numField)
for i := 0; i < numField; i++ {
field := sv.Field(i)
if !field.CanAddr() {
return fmt.Errorf("field %d is not addressable", i)
}
values[i] = field.Addr().Interface()
}
return row.Scan(values...)
}
// 使用方式:
// var user UserLogin
// scanToStruct(row, &user)⚠️ 注意事项:
- 永远避免对循环变量取地址(
&val),应取容器索引项地址(&slice[i]); -
structs.Names()返回字段名,不提供字段地址,不可直接用于Scan; - 生产环境优先使用显式字段赋值(
&u.Username, &u.Password),清晰、高效、零反射开销; - 若必须反射,确保目标字段可寻址(非嵌入只读字段、非未导出字段需谨慎)。
通过理解 Go 中变量生命周期与地址语义,即可规避此类“指针幻影”问题,写出安全可靠的数据库映射逻辑。


















