【Go】P13 Go 语言核心概念:指针 (Pointer) 详解
目录
- 前言
- 变量与内存地址
- 什么是指针?
- 指针的核心操作
- 取地址 (&)
- 解引用 (*)
- 通过指针修改值
- 指针的零值:nil
- 内存分配:new 与 make 的区别
- make:用于引用类型的初始化
- new:用于值类型的内存分配
- new 与 make 的核心区别总结
- 为什么 Go 语言需要指针?
- 在函数间共享和修改数据
- 提高性能
- 总结

前言
在 Go 语言的学习中,指针(Pointer)是一个绕不开的核心概念。它既是理解 Go 语言内存管理、函数传参以及性能优化的关键,也是许多初学者容易混淆的难点。
本文将从最基础的变量和内存地址讲起,带你一步步深入理解 Go 语言中指针的定义、使用、以及 new 和 make 这两个与内存分配密切相关的内置函数。
变量与内存地址
在深入指针之前,我们必须先巩固一个基础知识:变量(Variable)。
在 Go 语言中,变量是程序用来存储数据的基本单元。变量的本质,其实是给某块用于存储数据的内存空间起的一个好记的“别名”。
比如,我们定义一个变量 a := 10,程序在运行时会执行以下操作:
- 在内存中寻找一块空闲的空间。
- 将数据
10存储到这块空间中。 - 让变量名
a与这块内存空间(的地址)建立映射关系。
之后,我们就可以通过 a 这个变量名来访问或修改内存中存储的 10 这个值。
在计算机底层,a 变量对应的是一个实实在在的内存地址。Go 语言使用 & (取地址) 操作符来获取一个变量的内存地址。
代码示例 1:查看变量的内存地址
package mainimport "fmt"func main() {a := 10fmt.Printf("变量 a 的值: %d\n", a)// 使用 &a 获取变量 a 在内存中的地址// %p 是一个占位符,专门用于格式化输出指针和内存地址fmt.Printf("变量 a 的内存地址: %p\n", &a)
}
示例1输出样例:
变量 a 的值: 10
变量 a 的内存地址: 0xc00001a0a8
什么是指针?
理解了内存地址,指针就非常容易理解了。
指针(Pointer)也是一个变量,但它是一种特殊的变量。它存储的数据不是一个普通的值(如 10、"hello"),而是另一个变量的内存地址。我们可以说,一个指针“指向”了另一个变量。
代码示例 2:定义和使用指针
package mainimport "fmt"func main() {a := 10 // 这是一个普通的 int 变量// 1. 声明一个指针变量 p// var p *int 表示 p 是一个指针,它专门用来存储 int 类型变量的地址var p *int// 2. 将变量 a 的地址(&a)赋值给指针 pp = &afmt.Printf("变量 a 的值: %d\n", a)fmt.Printf("变量 a 的内存地址: %p\n", &a)fmt.Println("--------------------")fmt.Printf("指针 p 存储的值 (即 a 的地址): %p\n", p)fmt.Printf("指针 p 的类型: %T\n", p) // 类型为 *intfmt.Printf("指针 p 自己的内存地址: %p\n", &p)
}
示例2输出样例:
变量 a 的值: 10
变量 a 的内存地址: 0xc00001a0a8
--------------------
指针 p 存储的值 (即 a 的地址): 0xc00001a0a8
指针 p 的类型: *int
指针 p 自己的内存地址: 0xc000006028
分析:
p的值(0xc00001a0a8)等于a的地址(&a)。这证实了指针存储的就是地址。p的类型是*int(读作 “int pointer” 或 “指向 int 的指针”)。p既然也是一个变量,它自己当然也有一个内存地址(&p),即0xc000006028。
Go 语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string、*MyStruct 等。
指针的核心操作
指针有两个核心操作符:& (取地址) 和 * (解引用)。
取地址 (&)
我们已经在上面用过了。& 放在一个变量前,用于获取该变量的内存地址。
p := &a
p 得到了 a 的地址,或者说,将 a 的地址值赋给指针类型变量 p
解引用 (*)
“解引用”(Dereferencing)也常被俗称为“取值”。* 放在一个指针变量前,用于获取该指针所指向的内存地址中存储的值。
& 和 * 是一对互逆的操作。
代码示例 3:指针的解引用
package mainimport "fmt"func main() {a := 100p := &a // p 指向 a// 使用 *p 来获取 p 指向的地址(即 a 的地址)上存储的值val := *pfmt.Printf("变量 a 的值: %d\n", a)fmt.Printf("通过指针 p 解引用获取的值: %d\n", val)fmt.Printf("val 和 a 是否相等: %v\n", val == a)
}
示例3输出:
变量 a 的值: 100
通过指针 p 解引用获取的值: 100
val 和 a 是否相等: true
通过指针修改值
这才是指针最强大的用途之一。既然指针 p 知道变量 a 的“住址”,它不仅能读取 a 的值,还能修改 a 的值。
我们同样使用 * 操作符,但这次是将它放在赋值操作的左侧。
代码示例 4:通过指针修改变量的值
package mainimport "fmt"func main() {a := 100p := &afmt.Printf("修改前,a 的值: %d\n", a)// *p 代表 a 变量本身// 下面这行代码等价于 a = 200*p = 200fmt.Printf("通过指针 p 修改后,a 的值: %d\n", a)
}
示例4输出:
修改前,a 的值: 100
通过指针 p 修改后,a 的值: 200
分析: 我们没有直接操作 a,而是通过操作 *p 改变了 a 的值。这在函数传参时尤其重要,因为它允许我们在函数内部修改函数外部的变量。
指针的零值:nil
一个指针变量被声明后,如果没有被赋予任何变量的地址,它的默认值是 nil。nil 是 Go 语言中指针、切片、映射、通道、函数和接口类型的“零值”。
一个 nil 指针不指向任何内存地址。对 nil 指针进行解引用(*p)操作会引发一个运行时恐慌(panic),因为你试图访问一个不存在的内存地址。
代码示例 5:nil 指针与恐慌
package mainimport "fmt"func main() {var p *int // p 被声明,但未初始化,其值为 nilfmt.Printf("p 的值: %v\n", p) // 输出: <nil>if p == nil {fmt.Println("p 是一个 nil 指针")}// 对 nil 指针解引用会引发 panic// 下面这行代码如果取消注释,程序将崩溃// fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference
}
高价值提示: 在使用指针之前,尤其是那些可能来自函数返回值或复杂逻辑的指针,最好先检查它是否为 nil。
内存分配:new 与 make 的区别
在 Go 语言中,我们经常需要手动管理内存分配,尤其是对于引用类型和指针。new 和 make 是 Go 提供的两个用于内存分配的内置函数,但它们服务的目的截然不同。
make:用于引用类型的初始化
首先,我们来看你提供的参考示例中提到的问题。
错误示例(未分配内存的 map):
package mainimport "fmt"func main() {var userinfo map[string]string // 只是声明了 map,但它是 niluserinfo["username"] = "张三" // 恐慌!panic: assignment to entry in nil mapfmt.Println(userinfo)
}
上述代码中,userinfo 只是一个 nil 映射,它没有指向任何底层的哈希表数据结构。你不能向一个 nil 映射中添加键值对。
make 的职责 是为切片(slice)、映射(map)和通道(channel) 这三种“引用类型”分配内存并初始化它们。make 返回的是初始化后的类型实例,而不是指针。
正确示例(使用 make 初始化 map):
package mainimport "fmt"func main() {// 使用 make 创建一个 map,分配了底层内存var userinfo = make(map[string]string) userinfo["username"] = "张三" // 现在可以安全地赋值了fmt.Println(userinfo)
}
new:用于值类型的内存分配
new 的职责 是为任意类型(包括 int、struct 等值类型)分配内存空间,并返回一个指向该内存空间的指针(*T)。
这块新分配的内存会被初始化为该类型的零值(zero value)。
new(int)会分配一块内存,存入0,并返回一个*int类型的指针。new(string)会分配一块内存,存入""(空字符串),并返回一个*string类型的指针。new(bool)会分配一块内存,存入false,并返回一个*bool类型的指针。
代码示例 6:使用 new 函数
package mainimport "fmt"func main() {// new(int) 分配了一个 int 的内存空间(值为 0)// a 是一个 *int 类型的指针,它指向这块内存var a = new(int)fmt.Printf("a 的值 (内存地址): %v\n", a)fmt.Printf("a 的类型: %T\n", a)fmt.Printf("a 指针变量对应的值 (零值): %v\n", *a)// 我们可以通过解引用来修改这个值*a = 100fmt.Printf("修改后 a 指针变量对应的值: %v\n", *a)
}
示例6结果样例:
a 的值 (内存地址): 0xc00001a0b0
a 的类型: *int
a 指针变量对应的值 (零值): 0
修改后 a 指针变量对应的值: 100
new 与 make 的核心区别总结
这是一个常见的面试题,我们可以用一个表格来清晰地总结:
| 特性 | new(T) | make(T, ...) |
|---|---|---|
| 作用 | 分配内存,并初始化为零值 | 用于初始化引用类型的数据结构 |
| 使用类型 | 任意类型 T | 仅限:切片(slice)、映射(map) 、通道(channel) |
| 返回值 | *T(指向类型 T 的指针) | T(初始化后的类型实例本身,不是指针) |
简单来说:
- 想得到一个指向零值的指针,用
new。 - 想得到一个初始化后(非
nil)的切片、映射或通道,用make。
为什么 Go 语言需要指针?
最后,我们来谈谈指针的价值。为什么不(像某些语言一样)隐藏指针呢?
在函数间共享和修改数据
Go 语言中所有的函数参数传递都是值传递(Pass by Value)。这意味着当你把一个变量 a 传给一个函数时,函数内部得到的是 a 的一个副本。在函数内修改这个副本,不会影响到函数外部的 a。
如果你希望函数能够修改外部的原始变量,你就必须传递该变量的指针。
代码示例 7:值传递 vs 指针传递
package mainimport "fmt"// 接受 int 值(副本)
func modifyByValue(val int) {val = 100 // 只修改了副本
}// 接受 *int 指针
func modifyByPointer(ptr *int) {*ptr = 100 // 通过解引用,修改了指针指向的原始值
}func main() {// 值传递a := 10modifyByValue(a)fmt.Printf("值传递后,a 的值: %d\n", a) // a 仍然是 10// 指针传递b := 10modifyByPointer(&b) // 传递 b 的地址fmt.Printf("指针传递后,b 的值: %d\n", b) // b 变成了 100
}
示例7输出
值传递后,a 的值: 10
指针传递后,b 的值: 100
提高性能
值传递意味着数据拷贝。如果传递的是一个非常大的结构体(Struct),拷贝它会带来显著的性能开销。
而传递一个指向该结构体的指针,无论结构体有多大,都只是拷贝一个内存地址(在 64 位系统上通常是 8 字节),这非常高效。
总结
指针是 Go 语言中一把强大而锋利的“手术刀”。它让我们能够直接与内存地址打交道,实现高效的数据共享和修改。
&(取地址): 获取变量的内存地址。*(解引用): 获取指针指向的值,或修改指针指向的值。- 指针的零值是
nil,对nil指针解引用会导致panic。 new用于分配任意类型的内存,返回一个指向零值的指针 (*T)。make仅用于初始化切片、映射和通道,返回它们实例 (T)。
希望这篇博文能帮你彻底搞懂 Go 语言的指针!
2025.10.27 G33高铁 前往杭州途中
