指针

指针是 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

读作:指向 int 的指针。

三、通过指针取值 *

age := 18
p := &age

fmt.Println(*p) // 18

*p 表示“取出 p 指向的那个值”。

这里:

表达式含义
age普通变量,值是 18
&ageage 的地址
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
}

这里发生了什么:

  1. &ageage 的地址传给函数。
  2. change 的参数类型是 *int,能接收这个地址。
  3. 函数里通过 *age = 20 修改地址对应的值。
  4. 外面的 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 自动解引用结构体字段

严格来说,结构体指针访问字段应该这样:

(*user).Age++

但 Go 允许你简写成:

user.Age++

所以这段代码是合法的:

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

如果前端没传年龄,Age0;如果前端真的传了 0,也是 0。你分不清。

使用指针后:

JSONGo 里
{}Age == nil
{"age":0}Age != nil && *Age == 0
{"age":18}Age != nil && *Age == 18

示例:

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
}

判断是否需要指针,可以先问三个问题:

  1. 函数或方法是否要修改外部对象?
  2. 这个值是否可能不存在,需要用 nil 表达?
  3. 结构体是否较大,或者包含锁、连接等不应该复制的字段?

如果答案都是否,普通值通常更清楚。

十四、切片、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 语言那种指针运算。

不能写:

// p++
// p + 1

这让 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 用来表达“修改原值”和“值可能不存在”的重要工具。