在Go语言开发中,结构体是组织复杂数据的常用方式,而指针参数传递可以让我们在函数内部修改外部变量的内容,很多场景下需要通过指针参数初始化结构体指针变量,但是不少开发者会因为对指针机制理解不深出现初始化不符合预期的问题。

Go指针的基础特性
Go语言中的指针存储的是变量的内存地址,我们可以通过<code>*</code>操作符获取指针指向的值,通过<code>&</code>操作符获取变量的地址。需要注意的是,Go中没有指针运算,指针的主要作用就是传递引用,避免大对象拷贝带来的性能损耗,同时允许函数修改外部变量的值。
当我们把一个指针作为参数传递给函数时,传递的是指针的副本,这个副本和原指针指向同一个内存地址,所以修改副本指向的内容,原指针指向的内容也会发生变化,但是如果修改副本本身存储的地址,不会影响原指针。
常见的错误初始化方式
很多开发者会写出下面这样的代码尝试通过指针参数初始化结构体指针变量,但是最终的初始化不会生效:
package main
import "fmt"
type User struct {
Name string
Age int
}
// 错误的初始化方式
func InitUserWrong(u *User) {
// 这里新建了一个User实例,让参数u指向这个新实例
// 但是参数u是外部传入指针的副本,修改u的指向不会影响外部的指针
newUser := &User{Name: "张三", Age: 18}
u = newUser
}
func main() {
var u *User
InitUserWrong(u)
// 此时u仍然是nil,初始化没有生效
if u == nil {
fmt.Println("初始化失败,u为nil")
} else {
fmt.Printf("初始化成功,用户姓名:%s,年龄:%dn", u.Name, u.Age)
}
}
上面的代码中,<code>InitUserWrong</code>函数的参数<code>u</code>是外部<code>u</code>指针的副本,函数内部让<code>u</code>指向了新的<code>User</code>实例,只是修改了副本的指向,外部的<code>u</code>还是原来的nil值,所以初始化不会生效。
正确的初始化实现方式
方式一:修改指针指向的内容
如果外部已经初始化了结构体指针变量,我们可以在函数内部修改指针指向的内容来完成初始化:
package main
import "fmt"
type User struct {
Name string
Age int
}
// 修改指针指向的内容完成初始化
func InitUserByModifyContent(u *User) {
u.Name = "李四"
u.Age = 20
}
func main() {
// 外部先初始化结构体实例
u := &User{}
InitUserByModifyContent(u)
fmt.Printf("初始化成功,用户姓名:%s,年龄:%dn", u.Name, u.Age)
}
这种方式的前提是外部的指针已经指向了一个合法的结构体实例,否则会出现空指针解引用错误。
方式二:传递指针的指针作为参数
如果外部的结构体指针变量还没有初始化,我们希望在函数内部给它分配内存并初始化,需要传递指针的指针作为参数,这样修改指针的指针指向的内容,就可以改变外部指针的指向:
package main
import "fmt"
type User struct {
Name string
Age int
}
// 传递指针的指针,修改外部指针的指向
func InitUserByPointer(p **User) {
*p = &User{Name: "王五", Age: 22}
}
func main() {
var u *User
// 传递u的地址,也就是指针的指针
InitUserByPointer(&u)
fmt.Printf("初始化成功,用户姓名:%s,年龄:%dn", u.Name, u.Age)
}
这种方式可以在函数内部为外部的结构体指针变量分配内存并完成初始化,适合外部指针未初始化的场景。
方式三:函数返回初始化后的指针
除了通过参数传递的方式,我们还可以让初始化函数返回结构体指针,这种方式更加直观,也是Go中常用的初始化方式:
package main
import "fmt"
type User struct {
Name string
Age int
}
// 返回初始化后的结构体指针
func NewUser(name string, age int) *User {
return &User{Name: name, Age: age}
}
func main() {
u := NewUser("赵六", 25)
fmt.Printf("初始化成功,用户姓名:%s,年龄:%dn", u.Name, u.Age)
}
不同方式的适用场景
我们可以根据具体的开发场景选择合适的初始化方式,下面是三种方式的对比:
| 初始化方式 | 适用场景 | 注意事项 |
|---|---|---|
| 修改指针指向的内容 | 外部已经初始化了结构体指针,只需要填充字段值 | 需要确保外部指针不为nil,否则会触发空指针错误 |
| 传递指针的指针 | 外部指针未初始化,需要在函数内部完成内存分配和初始化 | 需要传递外部指针的地址,函数参数类型为对应结构体指针的指针 |
| 函数返回指针 | 所有需要初始化结构体指针的场景,尤其是新建实例的场景 | 是Go中推荐的初始化方式,代码可读性更高 |
注意事项
- 在使用指针参数初始化结构体指针变量时,一定要区分修改指针本身和修改指针指向的内容的区别,避免修改副本指向导致初始化失效。
- 如果不确定外部的指针是否已经初始化,可以在函数内部先做nil判断,避免空指针解引用。
- 优先使用函数返回指针的方式进行初始化,这种方式代码逻辑更清晰,也更符合Go的语言习惯。