Go语言没有类,但结构体配合方法接收者可以实现面向对象的大部分能力,而指向结构体的指针则是绕不开的话题。方法要用指针接收者还是值接收者、结构体字段该用值还是指针、初始化时new和取地址有什么区别,这些问题都直接关系到代码的正确性和性能。本文把Golang里初始化结构体指针的常见方式做一个系统总结,并分析各种方式的适用场景和容易踩的坑。

一、初始化结构体指针的四种常见方式
Go语言初始化一个指向结构体的指针,最典型的写法有四种:new(T)、&T{}、先声明变量再取地址、以及在函数返回时构造。四种方式最终得到的效果非常接近,但细节上有差异。
第一种是使用内置函数new。new(User)会分配一块能容纳User结构体的内存,把所有字段置为零值,然后返回指向这块内存的指针*User。注意new返回的永远是指针,不是值。第二种是使用取地址操作符配合结构体字面量,写起来更直观,还能在构造时直接给字段赋初值,这也是实际项目中最常用的写法。
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
// 方式1:new,字段全部为零值
u1 := new(User)
fmt.Println(u1.Name == "") // true
// 方式2:字面量取地址,可同时初始化字段
u2 := &User{Name: "tom", Age: 18}
// 方式3:先声明值变量,再取地址
u3 := User{Name: "jerry"}
p3 := &u3
// 方式4:函数内部分配并返回指针
u4 := newUser("spike", 20)
fmt.Println(u1, u2, p3, u4)
}
func newUser(name string, age int) *User {
return &User{Name: name, Age: age}
}值得一提的是,Go编译器会做逃逸分析。当局部变量的地址被返回到函数外部时,变量会逃逸到堆上,所以第三种和第四种写法在u3、局部结构体离开作用域后依然安全,不需要像C语言那样手动malloc。这一点是Go和C在指针使用上最大的安全差异。
二、new与取地址写法的对比与选择
既然new(User)和&User{}都能得到*User,日常写代码该选哪个?从功能上说,&User{}是new的超集,因为字面量可以顺带初始化字段,而new只能得到零值结构体。Go官方的代码风格也基本倾向后者,标准库源码里new的使用频率远低于取地址写法。
new的合理使用场景是:当你只知道类型、暂时拿不到具体字段值,或者类型是通过泛型参数传入时。比如在泛型函数中,你无法直接写&T{},但可以写new(T)来获得一个*T类型的零值指针。
func zeroOf[T any]() *T {
return new(T) // 泛型场景下无法写 &T{},new 是唯一选择
}还有一种容易混淆的写法:对一个已经存在的指针使用取地址,会得到二级指针**User。除非你要实现链表节点交换或者修改指针本身,否则一般不需要二级指针。如果代码里出现了**User,先确认一下是否是自己想要的类型。
三、nil指针的隐患与防御性初始化
结构体指针最经典的事故就是nil指针解引用。一个*User类型的零值是nil,如果直接访问它的字段或者调用其指针接收者方法,而方法内部又访问了字段,程序就会panic。
var u *User // 只声明,未初始化,u == nil // fmt.Println(u.Name) // panic: runtime error: invalid memory address u.Name = "tom" // 同样 panic,因为 nil 指针没有可写的内存
规避nil指针有几个常用手段。第一,声明时顺手初始化:var u = &User{},这样即使后面忘了赋值,访问字段也只是得到零值而不会panic。第二,在使用前显式判空,尤其是map里存指针、切片里存指针的场景,取出元素后先判断是否为nil再访问。第三,借助构造函数约定:凡是需要外部创建的类型都通过类似newUser的函数返回初始化好的指针,避免外部直接声明零值指针。
另外要注意,调用nil指针的方法是否panic取决于方法内部是否访问字段。如果指针接收者方法内部完全没有解引用接收者,那么nil指针调用它是合法的。标准库里sync.Mutex零值可用就是这个思路的体现,但依赖这种特性会让代码变得脆弱,普通业务代码还是应该保证指针非nil再使用。
四、结构体嵌套指针与字段初始化技巧
当结构体字段本身也是指针时,初始化要格外小心。比如一个用户结构体内嵌一个地址指针,如果只初始化外层而不初始化内层,访问内层字段照样会panic。逐层初始化是最稳妥的做法。
type Address struct {
City string
}
type Account struct {
Name string
Addr *Address
}
// 正确:内外层一起初始化
a := &Account{
Name: "tom",
Addr: &Address{City: "Beijing"},
}
// 危险:内层指针为 nil
b := &Account{Name: "jerry"}
// fmt.Println(b.Addr.City) // panic关于结构体字段到底该用值还是指针,可以遵循一个简单原则:小且不需要区分零值的字段用值;需要表达“不存在”语义、体积较大、或者需要被多方共享修改的字段用指针。用指针字段表达可空语义是很自然的做法,判断if a.Addr == nil就能知道用户有没有填写地址,而值字段无法区分“零值”和“没填”。
最后提一个性能细节:取地址构造结构体时,如果整个结构体的地址发生逃逸,整个结构体都会分配到堆上。对于频繁创建的小对象,可以考虑让函数返回值而非指针,由调用方决定是否取地址,减少不必要的堆分配,这在高性能场景下是常见的优化手段。
总结
Golang初始化结构体指针的核心方式就两种:new函数和字面量取地址,前者适合泛型和零值场景,后者是日常首选。写代码时要养成三个习惯:声明指针时顺手初始化、访问指针字段前判空、通过构造函数封装复杂的初始化逻辑。理解逃逸分析和nil指针的触发条件,能帮助你避开Go指针使用中绝大多数的坑。
Golang指针结构体初始化struct pointer修改时间:2026-09-15 12:48:34