
在 Go 中使用 structs.Names() 获取结构体字段名并动态构建 sql.Rows.Scan() 所需的指针切片时,若错误地对循环变量取地址(如 &val),会导致所有指针指向同一内存地址,从而造成数据覆盖。本文详解问题成因与正确实现方式。
在 go 中使用 `structs.names()` 获取结构体字段名并动态构建 `sql.rows.scan()` 所需的指针切片时,若错误地对循环变量取地址(如 `&val`),会导致所有指针指向同一内存地址,从而造成数据覆盖。本文详解问题成因与正确实现方式。
该问题的根本原因在于 Go 的 for range 循环中,循环变量 val 是单个复用的栈变量,每次迭代仅更新其值,而非创建新变量。因此 &val 始终返回同一个地址:
for i, val := range userLoginKeys {
keys[i] = &val // ❌ 错误:所有 &val 指向同一内存位置
}
运行时输出 [0xc4201ca2c0 0xc4201ca2c0] 正是这一行为的直接证据——两个指针地址完全相同,导致 row.Scan() 将查询结果(如 username 和 password)连续写入同一块内存,最终二者值被后者覆盖(本例中均为哈希密码)。
✅ 正确做法是:对切片元素本身取地址,确保每个指针指向独立的字符串值:
userLoginKeys := structs.Names(UserLogin{})
keys := make([]interface{}, len(userLoginKeys))
for i := range userLoginKeys {
keys[i] = &userLoginKeys[i] // ✅ 正确:取切片第 i 个元素的地址
}
但需注意:structs.Names() 返回的是 []string,其元素是不可寻址的临时值(Go 规范限制对 slice 元素取地址需 slice 本身可寻址)。上述写法在某些 Go 版本中可能触发编译错误。
? 更健壮、推荐的解决方案是预先声明字段变量,再取其地址:
func Login() {
row := sql.QueryRow("SELECT username, password FROM users WHERE username=?", "golang")
var username, password string
// 直接传入字段地址(清晰、安全、无反射开销)
err := row.Scan(&username, &password)
if err != nil {
log.Fatal(err)
}
// 映射到结构体实例
user := UserLogin{
Username: username,
Password: password,
}
fmt.Printf("%+v\n", user)
}
若必须动态扫描(例如字段数未知或需泛型适配),应使用 reflect 构建结构体字段指针,而非依赖 structs.Names():
func scanIntoStruct(row *sql.Row, dest interface{}) error {
v := reflect.ValueOf(dest).Elem()
if v.Kind() != reflect.Struct {
return fmt.Errorf("dest must be a pointer to struct")
}
// 构建字段指针切片
values := make([]interface{}, v.NumField())
for i := 0; i <p>? <strong>关键总结</strong>: </p>
-
for range中的val是复用变量,&val永远返回同一地址; - 切片元素
slice[i]仅在 slice 本身可寻址时才可取地址(如局部变量 slice); - 生产环境优先采用显式变量 +
Scan(&a, &b),简洁安全; - 动态场景务必通过
reflect.Value.Field(i).Addr()获取真实字段地址,避免逻辑陷阱。










