指针
指针是 Go 入门里非常重要的一关。
很多人刚开始怕指针,是因为把它想得太复杂。
先记住一句话:
指针就是一个变量的地址。
普通变量保存值,指针保存“这个值在哪里”。
一、先用生活例子理解
假设你有一本书。
如果你把书复印一份给别人,别人改复印件,不会影响原书。
如果你把书架位置告诉别人,别人按位置找到原书并修改,会影响原书。
Go 里的值传递和指针传递也是这个逻辑。
二、取地址 &
package main
import "fmt"
func main() {
age := 18
p := &age
fmt.Println(age)
fmt.Println(p)
}
&age 表示取 age 的地址。
p 保存的不是 18,而是 age 这个变量在内存里的位置。
p 的类型是:
读作:指向 int 的指针。
三、通过指针取值 *
age := 18
p := &age
fmt.Println(*p) // 18
*p 表示“取出 p 指向的那个值”。
这里:
四、通过指针修改原变量
package main
import "fmt"
func main() {
age := 18
p := &age
*p = 20
fmt.Println(age) // 20
}
*p = 20 的意思是:
找到 p 指向的那个变量,把它改成 20。
所以 age 也变了。
五、函数参数默认是值拷贝
Go 函数传参默认是值拷贝。
package main
import "fmt"
func change(age int) {
age = 20
}
func main() {
age := 18
change(age)
fmt.Println(age) // 18
}
为什么没有变?
因为 change(age) 传进去的是一份副本。
函数内部改的是副本,不是外面的 age。
六、函数接收指针才能修改外部变量
package main
import "fmt"
func change(age *int) {
*age = 20
}
func main() {
age := 18
change(&age)
fmt.Println(age) // 20
}
这里发生了什么:
&age 把 age 的地址传给函数。
change 的参数类型是 *int,能接收这个地址。
- 函数里通过
*age = 20 修改地址对应的值。
- 外面的
age 被改成了 20。
七、结构体为什么经常用指针
结构体是值类型。
type User struct {
Name string
Age int
}
如果函数参数是 User:
func grow(user User) {
user.Age++
}
调用:
user := User{Name: "张三", Age: 18}
grow(user)
fmt.Println(user.Age) // 18
没有变化,因为传进去的是副本。
改成指针:
func grow(user *User) {
user.Age++
}
调用:
user := User{Name: "张三", Age: 18}
grow(&user)
fmt.Println(user.Age) // 19
结构体指针常用于:
- 修改结构体字段。
- 避免复制较大的结构体。
- 表示对象可能不存在。
八、Go 自动解引用结构体字段
严格来说,结构体指针访问字段应该这样:
但 Go 允许你简写成:
所以这段代码是合法的:
func grow(user *User) {
user.Age++
}
这是 Go 为结构体指针做的语法简化。
九、new 函数
new(T) 会创建一个 T 类型的零值,并返回它的指针。
p := new(int)
fmt.Println(*p) // 0
*p = 10
fmt.Println(*p) // 10
结构体:
user := new(User)
user.Name = "张三"
等价于:
user := &User{}
user.Name = "张三"
实际业务里,结构体更常用 &User{},因为能直接初始化字段:
user := &User{Name: "张三", Age: 18}
Go 1.26 起,new 也可以接收一个表达式,用来创建带初始值的指针:
age := new(int(18))
fmt.Println(*age) // 18
不过在结构体初始化时,仍然优先使用 &User{...}。这比 new 更直观,也更符合多数业务代码的阅读习惯。
十、nil 指针
指针的零值是 nil。
var p *int
fmt.Println(p == nil) // true
不能直接取 nil 指针指向的值:
var p *int
// panic: runtime error
// fmt.Println(*p)
正确做法:
if p != nil {
fmt.Println(*p)
}
结构体指针也一样:
var user *User
if user != nil {
fmt.Println(user.Name)
}
十一、指针表达“可选值”
有时候零值本身也是有效值。
例如更新用户年龄:
type UpdateUserRequest struct {
Age *int `json:"age"`
}
为什么不用 int?
因为 int 的零值是 0。
如果前端没传年龄,Age 是 0;如果前端真的传了 0,也是 0。你分不清。
使用指针后:
示例:
func updateAge(req UpdateUserRequest) {
if req.Age != nil {
fmt.Println("更新年龄为:", *req.Age)
} else {
fmt.Println("没有传年龄,不更新")
}
}
这个场景在 PATCH 更新接口里非常常见。
十二、指针接收者方法
方法如果要修改结构体字段,使用指针接收者。
type User struct {
Name string
Age int
}
func (u *User) Grow() {
u.Age++
}
使用:
user := User{Name: "张三", Age: 18}
user.Grow()
fmt.Println(user.Age) // 19
虽然 Grow 的接收者是 *User,但你可以直接用 user.Grow() 调用,Go 会自动取地址。
十三、值接收者和指针接收者的选择
业务代码里,结构体方法很多时候会用指针接收者。
不要为了“看起来高级”到处用指针
指针有明确用途,但不是越多越好。
下面这种小函数只读取一个整数,用普通值就够了:
func double(n int) int {
return n * 2
}
没必要写成:
func double(n *int) int {
return *n * 2
}
判断是否需要指针,可以先问三个问题:
- 函数或方法是否要修改外部对象?
- 这个值是否可能不存在,需要用
nil 表达?
- 结构体是否较大,或者包含锁、连接等不应该复制的字段?
如果答案都是否,普通值通常更清楚。
十四、切片、map 和指针的区别
切片、map 本身就包含指向底层数据的引用。
func addItem(items []string) {
items[0] = "修改"
}
func main() {
items := []string{"原始"}
addItem(items)
fmt.Println(items[0]) // 修改
}
map 也是:
func update(m map[string]int) {
m["age"] = 20
}
所以初学时容易困惑:
- 结构体传值会复制字段。
- 切片和 map 传值时,内部仍然引用底层数据。
但如果你要在函数里让切片本身重新指向新数组,通常要返回新切片:
func add(items []string) []string {
items = append(items, "新元素")
return items
}
十五、指针不能做运算
Go 有指针,但没有 C 语言那种指针运算。
不能写:
这让 Go 的指针更安全,也更适合日常业务开发。
十六、常见错误
1. 解引用 nil 指针
var user *User
fmt.Println(user.Name) // panic
先判断:
if user != nil {
fmt.Println(user.Name)
}
2. 以为传结构体会修改原对象
func rename(user User) {
user.Name = "李四"
}
要改原对象,传指针:
func rename(user *User) {
user.Name = "李四"
}
3. 返回局部变量指针是否安全
在 Go 里可以返回局部变量指针。
func NewUser(name string) *User {
user := User{Name: name}
return &user
}
这是安全的。Go 编译器会做逃逸分析,必要时把变量分配到堆上。
初学阶段不需要深入堆和栈,只要知道这种写法在 Go 里没问题。
十七、完整示例:更新用户
package main
import "fmt"
type User struct {
ID int64
Name string
Age int
}
type UpdateUserRequest struct {
Name *string
Age *int
}
func UpdateUser(user *User, req UpdateUserRequest) {
if req.Name != nil {
user.Name = *req.Name
}
if req.Age != nil {
user.Age = *req.Age
}
}
func main() {
user := &User{ID: 1, Name: "张三", Age: 18}
name := "李四"
req := UpdateUserRequest{
Name: &name,
}
UpdateUser(user, req)
fmt.Printf("%+v\n", user)
}
这个例子包含了 Go 后端里非常常见的几个点:
- 用结构体表达用户。
- 用指针修改原对象。
- 用指针字段表达可选更新。
- 函数接收结构体指针,避免复制并允许修改。
指针不是为了炫技,它是 Go 用来表达“修改原值”和“值可能不存在”的重要工具。