在Golang的开发过程中,理解指针赋值、指针拷贝以及值传递和地址传递的区别,是写出符合预期逻辑代码的基础,很多常见的bug都源于对这些概念的混淆。

Golang基础类型回顾
要理解指针相关操作,首先需要明确Golang中的值类型和引用类型的区别。值类型包括int、float、string、struct等,这类类型的变量直接存储值,赋值时会直接拷贝整个值内容。引用类型包括slice、map、channel等,这类类型的变量存储的是指向底层数据结构的指针,赋值时会拷贝这个指针,而不是底层数据本身。
指针赋值与指针拷贝
指针赋值
指针赋值是指将一个指针变量的值(也就是某个变量的内存地址)赋给另一个指针变量,此时两个指针会指向同一个内存地址,修改其中一个指针指向的内容,另一个指针访问到的值也会发生变化。
下面的示例展示了指针赋值的效果:
package main
import "fmt"
func main() {
// 定义一个int类型变量
var num int = 10
// 定义指针p1指向num
var p1 *int = &num
// 指针赋值,p2也指向num的内存地址
var p2 *int = p1
fmt.Println("修改前:")
fmt.Printf("num的值:%d, p1指向的值:%d, p2指向的值:%dn", num, *p1, *p2)
fmt.Printf("p1的地址:%p, p2的地址:%pn", p1, p2)
// 通过p1修改指向的值
*p1 = 20
fmt.Println("通过p1修改后:")
fmt.Printf("num的值:%d, p1指向的值:%d, p2指向的值:%dn", num, *p1, *p2)
}
运行上述代码可以看到,p1和p2的地址相同,通过p1修改值后,num和p2指向的值都变成了20,这就是指针赋值的特点。
指针拷贝
指针拷贝通常是指拷贝指针指向的内存地址对应的内容,生成一份新的数据,让新的指针指向这份新数据,此时两个指针指向不同的内存地址,修改其中一个指针指向的内容不会影响另一个。
以下是指针拷贝的示例:
package main
import "fmt"
func main() {
var num int = 10
var p1 *int = &num
// 指针拷贝:先拷贝num的值,再让p2指向新的变量
newNum := *p1
var p2 *int = &newNum
fmt.Println("拷贝后初始状态:")
fmt.Printf("p1指向的值:%d, p2指向的值:%dn", *p1, *p2)
fmt.Printf("p1的地址:%p, p2的地址:%pn", p1, p2)
// 修改p1指向的值
*p1 = 30
fmt.Println("修改p1指向的值后:")
fmt.Printf("p1指向的值:%d, p2指向的值:%dn", *p1, *p2)
}
运行结果可以看到,p1和p2的地址不同,修改p1指向的值后,p2指向的值依然是10,说明两者指向的是不同的内存空间。
值传递与地址传递的区别
值传递
值传递是指在函数调用时,将实参的副本传递给函数的形参,函数内部对形参的修改不会影响外部的实参。Golang中默认的参数传递方式就是值传递,无论是值类型还是引用类型,传递的都是对应变量的副本。
值传递的示例:
package main
import "fmt"
// 值传递函数,修改形参的值
func modifyByValue(num int) {
num = 100
fmt.Println("函数内部修改后的值:", num)
}
func main() {
var num int = 10
fmt.Println("调用函数前的num值:", num)
modifyByValue(num)
fmt.Println("调用函数后的num值:", num)
}
运行后可以看到,函数内部修改了num的值,但外部的num依然是10,说明值传递不会影响外部实参。
地址传递
地址传递是指传递变量的内存地址给函数,函数内部通过指针操作修改该地址对应的内容,会直接影响外部的实参。本质上传地址也是值传递的一种,传递的是地址这个值的副本,但因为两个地址指向同一个内存空间,所以修改地址对应的内容会影响外部变量。
地址传递的示例:
package main
import "fmt"
// 地址传递函数,接收指针参数
func modifyByAddress(p *int) {
*p = 100
fmt.Println("函数内部修改后的值:", *p)
}
func main() {
var num int = 10
fmt.Println("调用函数前的num值:", num)
modifyByAddress(&num)
fmt.Println("调用函数后的num值:", num)
}
运行后可以看到,函数内部修改指针指向的值后,外部的num变成了100,说明地址传递可以修改外部实参的值。
两者的适用场景对比
我们可以通过表格清晰对比值传递和地址传递的特点:
| 对比项 | 值传递 | 地址传递 |
|---|---|---|
| 传递内容 | 实参的副本 | 实参的内存地址副本 |
| 对外部实参的影响 | 无影响 | 可以修改实参内容 |
| 内存开销 | 值类型开销小,大结构体开销大 | 仅传递地址,开销固定 |
| 适用场景 | 不需要修改外部变量,小数据类型 | 需要修改外部变量,大结构体或需要共享数据 |
常见误区说明
很多开发者会误以为slice、map作为参数传递时是地址传递,实际上Golang中只有值传递,slice和map作为引用类型,传递的是slice结构体的副本,这个结构体里包含了指向底层数组的指针,所以修改slice的元素会影响外部,但修改slice本身的len、cap属性不会影响外部,因为传递的是结构体副本。
以下是验证slice传递特性的示例:
package main
import "fmt"
// 尝试修改slice的长度
func modifySlice(s []int) {
s = append(s, 4)
fmt.Println("函数内部slice:", s)
}
func main() {
s := []int{1, 2, 3}
fmt.Println("调用函数前的slice:", s)
modifySlice(s)
fmt.Println("调用函数后的slice:", s)
}
运行后可以看到,函数内部append后的slice有4个元素,但外部的slice依然是3个元素,说明传递的slice结构体是副本,修改结构体的属性不会影响外部。
总结
掌握Golang的指针赋值、指针拷贝以及值传递和地址传递的区别,核心是要理解内存地址的指向关系。指针赋值是让两个指针指向同一个地址,指针拷贝是生成新的数据和新的指针。值传递传递的是实参副本,不影响外部;地址传递传递的是地址副本,通过地址可以修改外部实参内容。实际开发中可以根据需求选择合适的传递方式,避免因为概念混淆导致程序逻辑错误。