背景与动机
Go 里有指针,但 Go 并没有把指针设计成“底层内存自由操作工具”,而是把它收敛成一种更安全、更偏工程语义的能力:你可以通过指针共享对象、避免大对象拷贝、修改原值,但不能像 C 那样拿指针做任意算术运算。
学习这一节,重点不是记住 * 和 & 两个符号,而是理解 4 个问题:
- 指针类型到底表示什么,零值是什么。
&和 分别发生在什么阶段,语义有什么不同。- Go 的指针和 C 的指针最本质的差异是什么。
new和make都像“分配”,但为什么适用对象完全不同。
以下说明以 Go 1.22+ 语义为准,核心规则可对照 Go 语言规范、Effective Go、Go for C++ Programmers。
核心原理拆解
1. 指针类型是什么:它保存“某个变量的地址”
Go 的指针类型写作:
*T意思是:“指向一个 T 类型变量的指针”。
例如:
var p *int这里 p 不是 int,而是“指向 int 的指针”。如果它还没指向任何变量,那么它的零值就是:
nil这点很重要:Go 的未初始化指针不是野指针语义,而是明确的 nil。
2. & 是取地址, 是解引用
&x:取变量地址
对于一个可取地址的值 x,&x 会得到一个 *T 类型指针。
x := 10p := &x此时:
x的类型是intp的类型是intp保存的是x的地址
Go 规范要求 & 的操作数通常必须是“可寻址的”对象,比如:
- 变量
- 可寻址结构体字段
- 可寻址数组元素
- 切片元素
此外,复合字面量是个特例,也可以直接取地址:
u := &User{Name: "Tom"}p:通过指针访问或修改它指向的值
x := 10p := &xfmt.Println(*p) // 10*p = 20fmt.Println(x) // 20这里 *p 的含义不是“指针类型”,而是“取出指针指向的那个变量”。
也就是说:
- 在类型位置,
T表示“指向 T 的指针类型” - 在表达式位置,
p表示“解引用指针 p”
3. Go 指针的核心价值:共享与修改,而不是手工走内存

Go 里使用指针,通常是为了这几件事:
- 让函数修改调用方对象
- 避免大结构体频繁拷贝
- 表达“可选值 / 可能不存在”
- 让方法作用于同一个对象实例
例如:
type User struct { Age int}
func grow(u *User) { u.Age++}这里传 *User 的意义,不是“更底层”,而是“修改原对象”。
4. Go 和 C 的关键区别:有指针,但没有指针算术
这是最重要的边界。
在 C 里,指针常常可以:
p + 1p - 1- 指针间求差
- 直接按内存步进访问连续区域
而在 Go 里,这些都不允许。Go 官方文档对 C/C++ 程序员的说明很明确:Go has pointers but not pointer arithmetic。
也就是说,下面这种写法在 Go 里不成立:
p = p + 1原因不是“语法还没支持”,而是语言设计上刻意不支持。Go 的取舍是:
- 让切片承担绝大多数“连续数据遍历”需求
- 让编译器和运行时更容易保证内存安全
- 避免把业务代码写成地址跳跃和偏移计算
这和 C 的差异不只是一条语法限制
它反映的是两种语言对指针定位完全不同:
- C:指针是底层内存操作核心工具
- Go:指针是受限引用机制,默认安全优先
顺带一提,Go 也不需要你手动 malloc/free、new/delete 配对管理生命周期,内存回收由垃圾回收器负责。这也是 Go 指针“能引用但不鼓励裸奔操作”的背景。
5. new:分配一个零值变量,并返回它的指针
new 的规则很简单:
p := new(T)含义是:
- 分配一个类型为
T的零值变量 - 返回它的地址
- 结果类型是
T
例如:
p := new(int)fmt.Println(*p) // 0*p = 42这里的 new(int) 等价于“拿到一个指向零值 int 的指针”。
new 适合什么场景
new 适合:
- 你明确需要一个
T - 想获得某个零值对象的指针
- 配合结构体指针使用
例如:
type Config struct { Port int}
c := new(Config)这时 c 的类型是 *Config,且内部字段都为零值。
new 不是构造器
new 不会执行“初始化逻辑”,只会给你一个零值对象指针。它不是 Java/C++ 意义上的构造函数。
6. make:初始化 slice、map、channel 这三种内建引用型结构
make 和 new 最大区别不是“一个返回指针,一个不返回”,而是它们作用对象不同。
make 只用于:
slicemapchan
例如:
s := make([]int, 3, 5)m := make(map[string]int)ch := make(chan int, 2)make 返回的不是指针,而是“已经初始化好、可以直接使用的值”。
为什么这三种类型必须用 make
因为它们不是“一个纯值盒子”那么简单,它们背后都关联运行时内部数据结构:
- slice 需要底层数组与长度/容量信息
- map 需要哈希表结构
- channel 需要通信队列与同步结构
所以:
new([]int)得到的是[]int,指向一个零值切片,里面还是nilmake([]int, 3)得到的是可直接用的切片值
这两者语义完全不同。

7. new 和 make 的使用边界
new 常见理解方式
new(T) 可以先粗暴记成:
- “我要一个
T” - “这个
T先是零值”
make 常见理解方式
make(T, ...) 可以记成:
- “我要一个可直接用的 slice/map/channel 值”
- “这类值的内部结构要先建好”
对 slice 的区别尤其容易混淆
p := new([]int)s := make([]int, 3)这里:
p的类型是[]intp的零值是nil slices的类型是[]ints已经可以直接按索引访问0..2
这正是面试和实践里的高频考点。
8. 指针不是“万能优化”,切片/map/channel 也不是“都用指针包一下”
很多初学者会误以为:
- “用了指针就一定更高效”
- “所有大对象都该到处传指针”
- “slice/map/channel 再套一层指针会更底层”
这些都不准确。
需要注意:
- slice、map、channel 本身已经带引用语义特征,很多场景没必要再传
[]T、map[K]V - 指针会引入共享可变状态,代码理解成本会增加
- 传指针是为了语义正确或避免不必要拷贝,不是默认选项
最小可运行代码示例
package main
import "fmt"
type User struct { Name string Age int}
func grow(u *User) { u.Age++}
func main() { x := 10 p := &x fmt.Println(x, *p)
*p = 20 fmt.Println(x, *p)
n := new(int) *n = 42 fmt.Println(*n)
s := make([]int, 3) s[0] = 1 fmt.Println(s)
m := make(map[string]int) m["go"] = 1 fmt.Println(m)
u := &User{Name: "Tom", Age: 18} grow(u) fmt.Println(u.Age)}这个例子同时覆盖了:
&取地址- 解引用与修改原值
new返回零值对象指针make初始化 slice、map- 指针参数修改原结构体
常见陷阱与错误示例
1. 对 nil 指针直接解引用
错误示例:
var p *intfmt.Println(*p)这会触发运行时 panic。Go 指针零值是 nil,不是可直接访问的有效对象。
2. 把 new(map[string]int) 当成“可直接写入的 map”
错误示例:
p := new(map[string]int)(*p)["x"] = 1这里 *p 仍然是 nil map,直接写会 panic。真正要可写,应使用:
m := make(map[string]int)3. 把 new([]int) 当成“长度已经初始化好的切片”
错误示例:
p := new([]int)fmt.Println(len(*p)) // 0(*p)[0] = 1 // panicnew([]int) 只是得到一个指向零值切片的指针,不会创建底层数组。
4. 误以为 Go 指针可以像 C 一样做算术
错误理解:
p = p + 1Go 不支持这类操作。遍历连续数据应使用:
- 数组索引
- 切片索引
range
5. 给 slice/map/channel 再套一层指针当默认风格
例如随手写:
func f(s *[]int)func g(m *map[string]int)多数场景并不需要。因为它们本身已经足够表达共享底层状态,除非你明确需要“修改切片头本身”或“重置调用方变量绑定”。
性能影响或设计取舍
1. Go 保留指针,但禁止指针算术,是安全性和可维护性的取舍
好处:
- 减少越界和非法地址操作
- 降低代码审查难度
- 让切片成为更高层的顺序数据抽象
代价:
- 不能像 C 那样直接写极底层偏移运算
- 某些底层场景要借助
unsafe
2. new 非常轻语义,但不等于“更高级的初始化”
它适合拿到零值对象指针,优点是简单直接;代价是如果你真正需要的是可用的 slice/map/channel,new 反而会把事情搞复杂。
3. make 体现的是“初始化内部结构”,不是“返回地址”
这让 slice/map/channel 的使用更自然,但也要求你先理解它们不是普通纯值,否则容易把 new 和 make 混用。
4. 指针能减少大对象拷贝,但会引入共享可变状态
优点:
- 可能减少复制成本
- 可以原地修改对象
代价:
- 谁改了对象变得不那么直观
- 并发场景更容易引入数据竞争
- API 语义更重
面试高频问题
1. Go 的 & 和 分别是什么
&x 是取地址,生成 *T 类型指针;*p 是解引用,访问或修改指针 p 指向的那个值。* 在类型位置和表达式位置含义不同,但都和“指针”有关。
2. Go 的指针和 C 的指针最大区别是什么
Go 有指针,但不支持指针算术,不能像 C 一样通过 p+1、p-1 这类方式遍历内存。Go 更强调安全引用,连续数据遍历通常交给切片和索引完成。
3. new 和 make 的区别是什么
new(T) 分配一个 T 的零值对象并返回 *T;make 只用于 slice、map、channel,负责初始化其内部结构,返回的是可直接使用的值,不是指针。
4. 为什么 new(map[string]int) 不能直接写入
因为 new(map[string]int) 返回的是 *map[string]int,而其中的 map 零值仍是 nil,没有初始化底层哈希表。真正可写的 map 要靠 make(map[string]int)。
5. new([]int) 和 make([]int, 10) 有什么本质区别
前者得到的是 *[]int,指向一个零值切片;后者得到的是长度为 10、可直接使用的 []int。前者没有初始化底层数组,后者已经初始化好了。
6. 什么时候应该使用指针
当你需要修改原对象、避免大对象复制、表达可选值,或方法需要绑定到同一实例状态时使用指针。不是所有类型、所有参数都默认该用指针。
一句话总结
Go 的指针可以概括为:& 负责取地址,* 负责通过地址访问原值,指针主要用来共享和修改对象;它和 C 最大的区别是 Go 明确禁止指针算术,而 new 用来拿到某个零值对象的指针,make 则专门初始化 slice、map、channel 这三类带内部运行时结构的值。