go语言指针常见操作方法
Go指针通过&取址、*解引用操作内存地址,用于函数间修改原值或避免大结构体拷贝。指针未初始化时为nil,解引用会引发崩溃。需注意常量等无法取址,切片等引用类型通常无需指针。适度使用指针,避免滥用导致性能问题或内存风险。
聊到Go语言,指针总是一个绕不开的话题。它不像C语言里那样让人望而生畏,但也绝非可以掉以轻心的存在。今天,我们就来把Go指针的那些关键操作、使用场景和注意事项,掰开揉碎了讲清楚。
什么是指针?
简单来说,指针就是一个“地址记录员”。它不存储实际的数据,只负责记住另一个变量在内存中的“门牌号”。声明一个指针变量,格式是这样的:

// ptr是指针变量名,*表示是指针类型,*int表示它记录的是一个int类型变量的地址 var ptr *int
1、关键操作符
用好指针,全靠两个符号:& 和 *。它们就像一把钥匙的正反两面。
| 符号 | 名称 | 作用 | 示例 |
& |
取址符 | 获取变量在内存中的地址,返回指针类型 | ptr := &num (获取 num 的地址) |
* |
解引用符 |
1. 声明时:表示指针类型(如 *int) 2. 使用时:获取或修改指针指向的具体值 |
|
2、常见操作
光说不练假把式,来看一段代码,把声明、取址、解引用的流程走一遍:
package main
import "fmt"
func main() {
// 1. 先定义一个普通变量
num := 100
fmt.Printf("普通变量值: %d, 地址: %p\n", num, &num)
// 2. 声明一个指针,并让它指向num的地址
var ptr *int = &num // 现在ptr记住了num住在哪里
// 3. 通过指针“遥控”原始变量
fmt.Printf("指针指向的值: %d\n", *ptr) // 输出 100,读取原值
*ptr = 200 // 修改指针指向的那个内存单元里的值
fmt.Printf("修改后原始变量值: %d\n", num) // 输出 200,原值确实被改了
}
3、空指针
刚声明还没初始化的指针,默认值是nil。你可以把它理解成一个指向“虚无”的指针。
Go语言在底层把nil当作地址为0的特殊值。所以,判断ptr == nil会得到true,打印它的地址会显示0x0。
这里有个大坑: 试图对一个nil指针进行解引用(比如执行*ptr),程序会立刻崩溃,并报错“panic: invalid memory address or nil pointer dereference”。这就好比拿着一个空地址去找房子,结果当然是找不到。
package main
import "fmt"
func main() {
var ptr *int // 未初始化,ptr是nil
if ptr == nil {
fmt.Printf("%p", ptr) // 输出 0x0
}
// *ptr = 5 // 如果执行这行,程序会panic
}
4、指针的使用场景
4.1、在函数间共享数据并修改原值
Go语言的函数参数默认是值传递,也就是说,传进去的是个副本。如果你想在函数内部修改外部的那个“原件”,就必须传递指针。
func increment(ptr *int) {
*ptr++ // 直接修改指针指向的那个原始值
}
func main() {
count := 10
increment(&count) // 把count的地址传进去
fmt.Println(count) // 输出 11,原值被成功修改
}
4.2、避免大型数据结构的拷贝开销
想象一下,你有一个包含成千上万个元素的结构体或数组。如果按值传递,整个数据都会被复制一份,既费内存又耗CPU。传递指针就高效多了,它只复制一个内存地址(在64位系统上就8个字节)。
type BigData struct {
Items [10000]int
}
// 使用指针接收,避免拷贝10000个int的巨大开销
func process(data *BigData) {
// 在这里处理数据
data.Items[0] = 1 // 修改会直接影响外部的原始结构体
}
5、注意事项
指针虽好,但用起来也得讲究章法,否则容易踩坑。
1. 不可取地址的情况
不是所有东西都能用&取地址。以下几种情况,编译器会直接报错“cannot take the address of ...”:
- 字面量/常量:比如
&42、&"hello"。数字42和字符串“hello”本身没有固定的内存地址。 - Map 的值:比如
&m["key"]。因为Map底层结构可能会动态调整,其值的位置不固定,所以禁止直接取址。如果非要用,得先赋值给一个临时变量。 - 函数返回值:一般情况下,不能直接对函数返回值取地址(除非函数返回的就是指针或可寻址的变量)。
2. 切片、Map 和 Channel 的特殊性
这是Go语言设计上的一个巧妙之处。
- 切片(Slice)、Map和Channel,它们本身就被设计为引用类型。你可以把它们理解成自带了一个“内部指针”。
- 无需传指针:在函数间传递这些类型时,直接传值就行。函数内部对元素内容的修改(比如修改切片某个索引的值,或给Map新增键值对),会直接反映到外部的原变量上。
- 例外:如果你需要在函数里改变切片的长度或容量(比如
append导致底层数组换了),或者想替换掉整个Map对象,那还是需要传递指针(如*[]int)。不过这种场景在日常开发中比较少见。
3. 内存逃逸与性能
- 栈与堆:Go编译器很聪明,会做“逃逸分析”。如果一个局部变量的地址被返回,或者传给了其他goroutine,它就会被“赶到”堆内存上分配。这会增加垃圾回收(GC)的压力。
- 适度使用:别滥用指针。对于像
int、bool或者很小的结构体这类“轻量级”数据,值传递往往更快。因为指针操作多了一次“寻址”的开销,还可能引发不必要的堆分配。
4. 原子指针与 unsafe
sync/atomic.Pointer:这是为高并发场景准备的“武器”,提供了无锁的原子指针操作,适合做状态切换。unsafe.Pointer:这是一把“双刃剑”。它能让你绕过Go的类型系统,直接进行指针转换,极其危险。它不参与GC的生命周期管理,用不好很容易导致“悬垂指针”(访问已释放内存)或内存破坏。除非你在写底层系统库、驱动或者追求极致性能的框架,否则在业务代码里请远离它。
总的来说,Go语言的指针在提供强大能力的同时,也通过语言设计规避了许多传统指针的陷阱。理解它的原理,明确它的适用场景和边界,你就能在需要的时候,游刃有余地使用这个工具。


































