
本文详解在 go 中如何正确使用 reflect 包,从 interface{} 参数中获取结构体字段的指针(*int 等),避免 `unsafe.pointer` 误用,并提供可直接运行的三种实践方式:类型断言解包指针、直接设值、以及适配 scan 类函数的接口传递。
在 Go 反射中,reflect.Value.Addr() 返回的是一个 新的 reflect.Value,它表示原字段的地址(即指向该字段的指针值),而非 unsafe.Pointer。而 .Pointer() 方法返回的是底层 uintptr,不能直接解引用(*ptr 会导致编译错误或 panic),这是初学者最常见的误区。正确路径是:先用 .Addr().Interface() 将反射值转为 interface{},再通过类型断言获得真实指针。
以下是一个完整、安全、可运行的示例:
package main
import (
"fmt"
"reflect"
)
type Robot struct {
Id int
}
// ✅ 方式一:获取 *int 指针并修改(适用于需传入 &val 的场景,如 database/sql.Scan)
func fWithPointer(i interface{}) {
v := reflect.ValueOf(i).Elem().FieldByName("Id")
ptr := v.Addr().Interface().(*int) // 安全转换:interface{} → *int
*ptr = 100
}
// ✅ 方式二:直接设值(更简洁、推荐用于纯修改场景)
func fDirectSet(i interface{}) {
v := reflect.ValueOf(i).Elem().FieldByName("Id")
v.SetInt(100) // 自动处理可寻址性,无需指针转换
}
// ✅ 方式三:适配 Scan 类函数(如 sql.Rows.Scan)——直接传递 interface{}
func fForScan(i interface{}) {
v := reflect.ValueOf(i).Elem().FieldByName("Id")
// 假设 scan 是一个接受 interface{} 的函数(如 db.Scan)
scan(v.Addr().Interface()) // 传入 *int,类型为 interface{},完全兼容
}
// 模拟 Scan 函数(仅作演示)
func scan(dest interface{}) {
if p, ok := dest.(*int); ok {
*p = 200
}
}
func main() {
robot := &Robot{}
fWithPointer(robot)
fmt.Println("After fWithPointer:", robot.Id) // 输出: 100
robot2 := &Robot{}
fDirectSet(robot2)
fmt.Println("After fDirectSet:", robot2.Id) // 输出: 100
robot3 := &Robot{}
fForScan(robot3)
fmt.Println("After fForScan:", robot3.Id) // 输出: 200
}⚠️ 关键注意事项:
- reflect.Value.Addr() 要求原 reflect.Value 必须可寻址(通常来自 &struct 或 &field),否则 panic。因此传入参数必须是结构体指针(如 &Robot{}),而非值类型或接口内嵌值。
- v.Pointer() 返回 uintptr,不可直接解引用;它主要用于与 unsafe 包交互(如 (*int)(unsafe.Pointer(v.Pointer()))),但应尽量避免——除非你明确需要底层内存操作,且已充分理解 unsafe 的风险。
- 若仅需修改字段值,优先使用 v.SetInt() / v.SetString() 等方法,语义清晰、类型安全、性能更好。
- 在数据库扫描等场景中,v.Addr().Interface() 是标准做法,因为 Scan 等函数签名正是 func(...interface{}),接收 *T 类型的 interface{},无需额外断言。
总结:Go 反射的核心原则是「用 Interface() 回到类型安全世界,再用类型断言落地」;避免过早落入 uintptr 和 unsafe 层级。掌握 Addr() → Interface() → type assertion 这一链条,即可稳健实现动态字段指针访问与修改。










