Go 函数指针传参
当你把一个指针传给函数时,你实际上是给了这个函数一张直达原始变量内存地址的“通行证”。这使得函数能够直接修改外部的原始数据,这与普通的“值传递”(函数只拿到数据的复印件)有着本质的区别。
本章我们将深入剖析在 Go 语言中传递指针的运行机制、巨大优势,以及需要注意的潜在陷阱。
1. 理解函数参数中的指针
当一个函数接收指针作为参数时,它拿到的是目标变量的内存地址,而不是该变量内部所存数值的拷贝。这一底层差异对函数如何与原始数据互动产生了深远的影响。
1.1 值传递 vs. 指针传递
为了更直观地感受指针传递的威力,我们先把它和常规的“值传递”做个对比。
值传递 (Pass by Value): 当你按值传递一个变量时,函数会收到该变量值的一份全新拷贝。在函数内部对这个拷贝做的任何修改,都绝对不会影响到函数外面的原始变量。
package main
import "fmt"
func modifyValue(x int) {
x = 10 // 这里修改的只是 x 的复印件
}
func main() {
x := 5
modifyValue(x)
fmt.Println(x) // 输出: 5 (外面的 x 依然没变)
}指针传递 (Pass by Pointer): 当你传递一个指针时,函数收到的是该变量的内存地址。函数通过这个地址顺藤摸瓜,对其指向的数据所做的任何修改,都会直接改变函数外部的原始变量。
package main
import "fmt"
func modifyValue(x *int) {
*x = 10 // 顺着内存地址找过去,直接修改那个位置上的值
}
func main() {
x := 5
modifyValue(&x) // 传递 x 的真实内存地址
fmt.Println(x) // 输出: 10 (外面的 x 被成功修改了!)
}在第二个例子中,&x 获取了 x 的内存地址,而函数内部的 *x 则对该指针进行了解引用操作,从而赋予了函数直接修改原始变量 x 的能力。
1.2 声明接收指针的函数
要声明一个接收指针作为参数的函数,你只需要在参数的数据类型前面加上一个 * 符号。
func myFunction(ptr *int) {
// ptr 是一个指向 int 整数的指针
}
func processString(strPtr *string) {
// strPtr 是一个指向 string 字符串的指针
}1.3 在函数内部解引用指针
在函数体内部,你必须使用 * 运算符对指针进行解引用,才能读取或修改它所指向的具体数值。
package main
import "fmt"
func increment(numPtr *int) {
*numPtr = *numPtr + 1 // 读取原值加 1,再存回原地址
}
func main() {
number := 20
increment(&number)
fmt.Println(number) // 输出: 21
}在这里,*numPtr 访问了 numPtr 里存放的那个地址上的真实数据,这使得 increment 函数能够跨越作用域,直接修改 main 函数里的 number 变量。
2. 指针传参的实战应用
让我们看看在实际开发中,把指针传给函数通常用来解决哪些问题。
2.1 修改结构体字段 (Struct Fields)
将结构体的指针传给函数是非常高频的操作。这不仅能避免拷贝庞大结构体带来的性能损耗,还能让函数直接更新原结构体的信息。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func updateAge(p *Person, newAge int) {
p.Age = newAge // Go 语言的语法糖:自动隐式解引用 (*p).Age
}
func main() {
person := Person{Name: "Alice", Age: 30}
fmt.Println(person) // 输出: {Alice 30}
updateAge(&person, 31) // 传入 person 的指针
fmt.Println(person) // 输出: {Alice 31} (年龄被成功更新)
}2.2 交换变量的值 (Swapping Values)
通过指针交换两个变量的值,是指针教学中最经典的例子。
package main
import "fmt"
func swap(a *int, b *int) {
temp := *a
*a = *b
*b = temp
}
func main() {
x := 10
y := 20
fmt.Println("交换前:", x, y) // 输出: 交换前: 10 20
swap(&x, &y)
fmt.Println("交换后:", x, y) // 输出: 交换后: 20 10
}swap 函数接收两个整数的指针,通过解引用它们并利用一个临时变量 temp,完美实现了真实数据的互换。
2.3 修改切片 (Slices)
虽然 Go 语言中的切片本身就是一种引用类型,但在函数中如果需要修改切片的“头部信息”(比如改变它的长度 Length 或容量 Capacity,通常发生在使用 append 时),传递切片指针有时会被用到。不过,直接返回修改后的新切片是更推荐、更符合 Go 风格的做法。 我们来对比一下:
方案 1:返回修改后的切片(极其推荐,符合 Go 惯用法)
package main
import "fmt"
func appendValue(slice []int, value int) []int {
slice = append(slice, value)
return slice // 返回追加后的新切片
}
func main() {
mySlice := []int{1, 2, 3}
fmt.Println("追加前:", mySlice) // 输出: 追加前: [1 2 3]
mySlice = appendValue(mySlice, 4) // 用返回值覆盖原变量
fmt.Println("追加后:", mySlice) // 输出: 追加后: [1 2 3 4]
}方案 2:传递切片的指针(相对少见,用于原位修改头部)
package main
import "fmt"
func modifySliceHeader(slicePtr *[]int) {
slice := *slicePtr // 先解引用拿到切片本身
slice = append(slice, 5)
*slicePtr = slice // 用扩容后的新切片覆盖掉原来的旧切片头部
}
func main() {
mySlice := []int{1, 2, 3}
fmt.Println("修改前:", mySlice) // 输出: 修改前: [1 2 3]
modifySliceHeader(&mySlice) // 传入切片的指针
fmt.Println("修改后:", mySlice) // 输出: 修改后: [1 2 3 5]
}方案 2 虽然能达到目的,但代码阅读起来明显更加绕脑,容易出错。在绝大多数情况下,请坚持使用方案 1。
3. new 函数与指针
我们在上一章简单提过,new 是一个内置函数,专门用于为特定类型的新变量分配内存。它分配内存后,会返回指向这块全新内存的指针。
package main
import "fmt"
func main() {
// 为一个整数分配内存,并获取它的指针
ptr := new(int)
// 这块内存已经被初始化为 int 的零值 (0)
fmt.Println("初始值:", *ptr) // 输出: 初始值: 0
// 往这块内存里写入数据
*ptr = 42
fmt.Println("修改后的值:", *ptr) // 输出: 修改后的值: 42
}3.1 使用 new 创建结构体指针
new 最常用于直接创建一个结构体的指针实例。
package main
import "fmt"
type Book struct {
Title string
Author string
}
func main() {
// 为 Book 结构体分配内存,并返回指针
bookPtr := new(Book)
// 通过指针直接访问和修改字段 (无需手动加 * 解引用)
bookPtr.Title = "Go 语言程序设计"
bookPtr.Author = "Kernighan 和 Donovan"
fmt.Println(*bookPtr) // 输出: {Go 语言程序设计 Kernighan 和 Donovan}
}4. 潜在陷阱与避坑指南
指针虽然强大,但稍有不慎就会引爆程序。
4.1 致命的 Nil 指针
一个未初始化的指针它的值是 nil(什么都没指)。如果你敢去解引用一个 nil 指针,你的程序会当场表演一个“原地爆炸”(Runtime Panic)。永远记得在解引用前检查指针是否为空。
package main
import "fmt"
func process(ptr *int) {
if ptr == nil { // 救命的判空拦截!
fmt.Println("警告:指针是空的 (nil)!")
return
}
fmt.Println(*ptr)
}
func main() {
var ptr *int // 此时 ptr 默认就是 nil
process(ptr) // 输出: 警告:指针是空的 (nil)!
// 给它分配一块真实内存以避免崩溃
num := 10
ptr = &num
process(ptr) // 输出: 10
}4.2 悬挂指针 (Dangling Pointers)
悬挂指针是指向一块“已经被释放或回收的内存”的指针。访问悬挂指针会导致不可预知的灾难。幸运的是,Go 语言强大的自动垃圾回收 (GC) 和逃逸分析 (Escape Analysis) 机制,在极大程度上帮你杜绝了悬挂指针的产生。但在与 C 语言代码交互 (cgo) 或使用 unsafe 包时,仍需极其小心。
4.3 意外的副作用 (Side Effects)
因为传递指针允许函数内部随意篡改外部数据,这有时会导致“意外修改”。为了代码的可维护性,请确保函数对指针的修改在业务逻辑上是明确预期且被良好记录的。