Go中没有“指针数组”类型,日常应使用[]T切片;正确取地址需用&data[i]或循环内声明新变量;需修改切片头时才传[]*T。

Go里根本没有“指针数组”这种类型,别被C语言带偏了
Go语言不提供像C那样的 [N]*T(固定长度的指针数组)作为独立类型。你声明 var arr [3]*int 是合法的,但它只是个普通数组,每个元素是 *int;它不能动态增长,也不具备切片的语义。真正日常用、符合Go习惯的,是 []*T——即“元素为指针的切片”。它才是管理动态指针集合的标准方式。
- 想存一堆结构体地址并随时增删?用
[]*User,不是[100]*User - 需要精确控制内存布局或对接C?才考虑
*[N]T(指向固定数组的指针),但这是少数场景,比如CGO或unsafe操作 -
[]*T的底层是动态分配的堆内存,GC能正确追踪每个指针所指向的对象,只要切片还活着,对象就不会被回收
如何安全地构造 []*int:循环取地址的致命陷阱
最常见错误:在 for range 中直接对循环变量取地址,结果所有指针都指向同一个栈地址,值被反复覆盖。
data := []int{10, 20, 30}
var ptrs []*int
for i := range data {
ptrs = append(ptrs, &i) // ❌ 错!i 是同一个变量,地址不变
}
// ptrs 全部指向 i 的最终值(2),解引用全是 2
正确做法只有两种:
-
取原切片元素地址:确保
data是可寻址的变量(不能是字面量[]int{1,2,3}直接传参),然后用索引:&data[i] - 在循环内声明新变量:让每次迭代都有独立生命周期的变量
data := []int{10, 20, 30}
var ptrs []*int
for i := range data {
v := data[i] // ✅ 每次创建新变量 v
ptrs = append(ptrs, &v)
}
// 或更简洁:
for i := range data {
ptrs = append(ptrs, &data[i]) // ✅ data 是变量,&data[i] 安全
}
什么时候必须传 *[]*T?只有一种情况
传 []*T 就够用了——你能修改每个指针所指向的值,也能遍历、排序、过滤。但如果你的函数要改变切片本身的头信息(比如 append 后容量变了、底层数组换了地址),而希望调用方看到这个变化,就必须传指针:*[]*T。
立即学习“go语言免费学习笔记(深入)”;
- 典型场景:封装一个“安全追加”函数,内部可能扩容,且必须影响原始变量
- 不传指针时,
append返回新切片,原变量不变;传了就得手动解引用赋值:*s = append(*s, newPtr) - 绝大多数时候,更推荐函数返回新切片(
func add(s []*T, x *T) []*T),语义清晰,无副作用
func appendUser(users *[]*User, u *User) {
*users = append(*users, u) // ✅ 必须解引用再赋值
}
func main() {
var users []*User
u := &User{Name: "Alice"}
appendUser(&users, u) // 传地址
fmt.Println(len(users)) // 1
}
修改和共享:为什么改 *ptrs[0] 会影响原数据
因为 []*T 本质是两层间接:切片头 → 指针数组 → 实际对象。只要指针没变,解引用就始终访问同一块内存。
- 多个切片共享同一
[]*T?它们的指针元素地址相同,*ptrs[i]修改的是同一个对象 - 子切片如
sub := ptrs[1:3],里面的指针仍指向原对象,不是副本 - 风险点:跨 goroutine 无同步读写同一指针目标,会引发竞态;传给不可信函数可能导致意外修改
若需隔离,别试图复制指针切片本身(copy(dst, src) 只复制指针值),而是确保指针指向的对象是独立的——或者干脆不用指针,用值拷贝。
Go中真正难的不是语法,而是判断“这个值该不该被共享”。[]*T 把所有权交给你,也把责任一起交了过来:谁分配、谁释放、谁修改、谁同步,都得你自己画清楚边界。
