Go 按值传递 vs 按引用传递
最后修改于 2025 年 5 月 16 日
在本教程中,我们将探讨 Go 如何处理复制语义,包括值类型、引用类型和指针。我们将讨论值类型和引用类型之间的区别、Go 如何复制值以及如何有效使用指针。我们还将研究 Go 中的参数传递,包括如何将值和指针传递给函数。
Go 中的值类型 vs 指针
Go 在内存管理方面采用了一种简单而强大的方法
- 值类型:所有基本类型(int、float、bool、string、array、struct)都是值类型
- 引用类型:切片、映射、通道、函数和指针是引用类型
- 指针:可以使用显式指针与任何类型进行交互
| 特征 | 值类型 | 引用类型 |
|---|---|---|
| 赋值行为 | 按值复制(完全复制) | 按引用复制(共享数据) |
| 内存位置 | 栈(通常) | 堆(通常) |
| 示例 | int、struct、[3]int |
[]int、map、*int |
按值复制示例
Go 默认按值复制。将一个变量赋给另一个变量会为值类型创建独立的副本。这意味着对复制变量的更改不会影响原始变量。
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
// Primitive types
a := 10
b := a // Copy by value
b = 20
fmt.Printf("a = %d, b = %d\n", a, b)
// Structs are value types
p1 := Point{1, 2}
p2 := p1 // Copy by value
p2.X = 10
fmt.Printf("p1 = %v, p2 = %v\n", p1, p2)
// Arrays are value types
arr1 := [3]int{1, 2, 3}
arr2 := arr1 // Copy by value
arr2[0] = 99
fmt.Printf("arr1 = %v, arr2 = %v\n", arr1, arr2)
}
在示例中,变量 a 被复制到 b。更改 b 不会影响 a。对于结构体 p1 和数组 arr1,情况也是如此。对 p2 和 arr2 的更改不会影响原始变量 p1 和 arr1。
$ go run main.go
a = 10, b = 20
p1 = {1 2}, p2 = {10 2}
arr1 = [1 2 3], arr2 = [99 2 3]
引用类型示例
切片、映射和通道在 Go 中是引用类型。赋值它们会复制引用,而不是底层数据。
package main
import "fmt"
func main() {
// Slices are reference types
slice1 := []int{1, 2, 3}
slice2 := slice1 // Copies the slice header
slice2[0] = 99
fmt.Printf("slice1 = %v, slice2 = %v\n", slice1, slice2)
// Maps are reference types
map1 := map[string]int{"a": 1, "b": 2}
map2 := map1 // Copies the reference
map2["a"] = 100
fmt.Printf("map1 = %v, map2 = %v\n", map1, map2)
}
在示例中,切片 slice1 被复制到 slice2。因此,当我们更改 slice2 时,它也会影响 slice1。对于映射 map1 和 map2,情况也是如此。对 map2 的更改也会影响 map1。
$ go run main.go slice1 = [99 2 3], slice2 = [99 2 3] map1 = map[a:100 b:2], map2 = map[a:100 b:2]
Go 中的指针
Go 提供了显式指针,用于当你需要任何类型的引用语义时。指针是存储另一个变量内存地址的变量。您可以使用地址运算符 & 创建指针,并使用星号运算符 * 解除指针。
package main
import "fmt"
func main() {
// Creating pointers
a := 10
ptr := &a // Address-of operator
fmt.Printf("a = %d, *ptr = %d\n", a, *ptr)
*ptr = 20 // Dereference and modify
fmt.Printf("a = %d, *ptr = %d\n", a, *ptr)
// Pointer to struct
p := Point{1, 2}
pPtr := &p
pPtr.X = 10
fmt.Printf("p = %v, *pPtr = %v\n", p, *pPtr)
}
在示例中,我们创建了一个指向变量 a 的指针 ptr。我们可以通过该指针修改 a 的值。对于结构体 p,情况也是如此。我们创建了一个指向结构体 p 的指针 pPtr。我们可以通过该指针修改结构体的字段。
$ go run main.go
a = 10, *ptr = 10
a = 20, *ptr = 20
p = {10 2}, *pPtr = {10 2}
参数传递
Go 始终通过值传递参数,但您可以使用指针来实现引用语义。当您将指针传递给函数时,函数可以修改原始值。这对于大型数据结构非常有用,或者当您想避免复制数据时。
package main
import "fmt"
func modifyValue(x int) {
x = 20 // Doesn't affect original
}
func modifyPointer(x *int) {
*x = 20 // Modifies original
}
func modifySlice(s []int) {
s[0] = 99 // Affects original (slices are reference types)
}
func main() {
// Value parameter
a := 10
modifyValue(a)
fmt.Println("a =", a) // 10
// Pointer parameter
modifyPointer(&a)
fmt.Println("a =", a) // 20
// Slice parameter
nums := []int{1, 2, 3}
modifySlice(nums)
fmt.Println("nums =", nums) // [99 2 3]
}
在示例中,我们定义了三个函数:modifyValue、modifyPointer 和 modifySlice。第一个函数按值接收一个整数,因此它不会影响原始变量。第二个函数接收一个整数的指针,允许它修改原始变量。第三个函数接收一个切片,它是一种引用类型,因此它会修改原始切片。
复制数据结构
要创建引用类型的独立副本,您需要显式复制它们。对于切片,您可以使用内置的 copy 函数。对于映射,您需要创建一个新映射并手动复制键值对。
package main
import "fmt"
func main() {
// Copying slices
original := []int{1, 2, 3}
copy1 := make([]int, len(original))
copy(copy1, original) // Built-in copy function
copy1[0] = 99
fmt.Println("original =", original)
fmt.Println("copy1 =", copy1)
// Copying maps
mapOriginal := map[string]int{"a": 1, "b": 2}
mapCopy := make(map[string]int)
for k, v := range mapOriginal {
mapCopy[k] = v
}
mapCopy["a"] = 100
fmt.Println("mapOriginal =", mapOriginal)
fmt.Println("mapCopy =", mapCopy)
}
在示例中,我们使用内置的 copy 函数创建了一个切片的副本。我们还通过迭代原始映射并复制每个键值对来创建了一个映射的副本。对 copy1 和 mapCopy 的更改不会影响原始的 original 和 mapOriginal。
返回指针 vs 值
Go 的逃逸分析决定是将变量分配在栈上还是堆上。返回局部变量的指针通常不安全,因为当函数退出时,该变量可能会被释放。但是,如果变量分配在堆上,则返回指针是安全的。
package main
import "fmt"
func createValue() Point {
return Point{1, 2} // Usually stack allocated
}
func createPointer() *Point {
return &Point{3, 4} // Heap allocated (escapes function)
}
func main() {
val := createValue()
ptr := createPointer()
fmt.Println("val =", val)
fmt.Println("ptr =", *ptr)
}
在示例中,函数 createValue 返回一个值,该值通常分配在栈上。函数 createPointer 返回一个结构体的指针,该结构体分配在堆上。Go 编译器执行逃逸分析来确定变量应该分配在栈上还是堆上。如果变量逃逸出函数,它将被分配在堆上。这对于性能和内存管理很重要。
总结与最佳实践
- Go 默认对所有类型都进行按值传递
- 切片、映射和通道具有引用语义
- 当您需要修改原始值时,请使用指针
- 使用内置的
copy函数来复制切片 - 对于大型结构体,请考虑传递指针以提高效率
- 信任 Go 的逃逸分析来做内存分配决策
- 在您的 API 中明确所有权和变异
理解这些概念对于编写高效、正确的 Go 代码至关重要,这些代码能够正确地管理内存和数据共享。
本教程概述了 Go 的复制语义,包括值类型、引用类型、指针和参数传递。我们还讨论了如何复制数据结构以及返回指针与值的含义。通过理解这些概念,您可以编写高效且正确的 Go 代码,从而正确地管理内存和数据共享。