ZetCode

Go 按值传递 vs 按引用传递

最后修改于 2025 年 5 月 16 日

在本教程中,我们将探讨 Go 如何处理复制语义,包括值类型、引用类型和指针。我们将讨论值类型和引用类型之间的区别、Go 如何复制值以及如何有效使用指针。我们还将研究 Go 中的参数传递,包括如何将值和指针传递给函数。

Go 中的值类型 vs 指针

Go 在内存管理方面采用了一种简单而强大的方法

特征 值类型 引用类型
赋值行为 按值复制(完全复制) 按引用复制(共享数据)
内存位置 栈(通常) 堆(通常)
示例 intstruct[3]int []intmap*int

按值复制示例

Go 默认按值复制。将一个变量赋给另一个变量会为值类型创建独立的副本。这意味着对复制变量的更改不会影响原始变量。

main.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,情况也是如此。对 p2arr2 的更改不会影响原始变量 p1arr1

$ go run main.go
a = 10, b = 20
p1 = {1 2}, p2 = {10 2}
arr1 = [1 2 3], arr2 = [99 2 3]

引用类型示例

切片、映射和通道在 Go 中是引用类型。赋值它们会复制引用,而不是底层数据。

main.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。对于映射 map1map2,情况也是如此。对 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 提供了显式指针,用于当你需要任何类型的引用语义时。指针是存储另一个变量内存地址的变量。您可以使用地址运算符 & 创建指针,并使用星号运算符 * 解除指针。

main.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 始终通过值传递参数,但您可以使用指针来实现引用语义。当您将指针传递给函数时,函数可以修改原始值。这对于大型数据结构非常有用,或者当您想避免复制数据时。

main.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]
}

在示例中,我们定义了三个函数:modifyValuemodifyPointermodifySlice。第一个函数按值接收一个整数,因此它不会影响原始变量。第二个函数接收一个整数的指针,允许它修改原始变量。第三个函数接收一个切片,它是一种引用类型,因此它会修改原始切片。

复制数据结构

要创建引用类型的独立副本,您需要显式复制它们。对于切片,您可以使用内置的 copy 函数。对于映射,您需要创建一个新映射并手动复制键值对。

main.go
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 函数创建了一个切片的副本。我们还通过迭代原始映射并复制每个键值对来创建了一个映射的副本。对 copy1mapCopy 的更改不会影响原始的 originalmapOriginal

返回指针 vs 值

Go 的逃逸分析决定是将变量分配在栈上还是堆上。返回局部变量的指针通常不安全,因为当函数退出时,该变量可能会被释放。但是,如果变量分配在堆上,则返回指针是安全的。

main.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 代码至关重要,这些代码能够正确地管理内存和数据共享。

本教程概述了 Go 的复制语义,包括值类型、引用类型、指针和参数传递。我们还讨论了如何复制数据结构以及返回指针与值的含义。通过理解这些概念,您可以编写高效且正确的 Go 代码,从而正确地管理内存和数据共享。

来源

Go 高效编程:指针 vs 值

Go FAQ:按值传递

作者

我的名字是 Jan Bodnar,我是一名热情的程序员,拥有丰富的编程经验。自 2007 年以来,我一直在撰写编程文章。至今,我已创作了 1400 多篇文章和 8 本电子书。我在编程教学方面拥有超过十年的经验。

列出所有 Go 教程