golang的参数传递

11

一、总体概述

在 Go 语言中,所有的参数传递都是值传递。也就是说,当我们将一个变量作为参数传递给函数或方法时,实际上是将该变量的副本传递过去。

然而,某些类型的变量在复制时会复制其引用(指针),这使得在函数中对这些变量的修改会影响到原始变量。这种行为类似于引用传递,但实际上仍然是值传递。


二、值类型和引用类型

1. 值类型

  • 定义:值类型的变量直接包含其值,赋值或传参时会复制整个值。

  • 包括

    • 基本类型:intfloatboolstringcomplex 等。

    • 结构体(struct

    • 数组(array

2. 引用类型

  • 定义:引用类型的变量存储的是对底层数据的引用,赋值或传参时复制的是引用(指针)。

  • 包括

    • 切片(slice

    • 映射(map

    • 通道(channel

    • 接口(interface

    • 函数(func

    • 指针(pointer


三、详细说明

1. 值类型

(1)基本类型

  • 特性:赋值和传参时,复制整个值,对副本的修改不会影响原始变量。

示例

func modifyValue(x int) {
    x = 100
}

func main() {
    a := 10
    modifyValue(a)
    fmt.Println(a) // 输出:10
}

  • 解释modifyValue 函数接收的是 a 的副本,对 x 的修改不影响 a

(2)数组(array)

  • 特性:数组是定长的,赋值和传参时会复制整个数组。

示例

func modifyArray(arr [3]int) {
    arr[0] = 100
}

func main() {
    a := [3]int{1, 2, 3}
    modifyArray(a)
    fmt.Println(a) // 输出:[1 2 3]
}

  • 解释modifyArray 中的 arra 的副本,对其修改不影响 a

(3)结构体(struct)

  • 特性:结构体也是值类型,赋值和传参时会复制整个结构体。

示例

type Person struct {
    Name string
    Age  int
}

func modifyPerson(p Person) {
    p.Name = "Bob"
}

func main() {
    person := Person{Name: "Alice", Age: 30}
    modifyPerson(person)
    fmt.Println(person.Name) // 输出:Alice
}

  • 解释modifyPerson 中的 pperson 的副本,对其修改不影响原始 person

2. 引用类型

(1)切片(slice)

  • 特性:切片是一个结构体,包含指向底层数组的指针、长度和容量。赋值和传参时复制的是切片结构体,但其内部指针仍指向同一个底层数组。

示例

func modifySlice(s []int) {
    s[0] = 100
}

func main() {
    a := []int{1, 2, 3}
    modifySlice(a)
    fmt.Println(a) // 输出:[100 2 3]
}

  • 解释:尽管 sa 的副本,但它们共享同一个底层数组,因此对元素的修改会影响原始切片。

(2)映射(map)

  • 特性:映射是引用类型,底层实现为指针。赋值和传参时复制的是指针,对映射的修改会影响原始映射。

示例

g
  • 解释modifyMap 中对 m 的修改会影响 myMap,因为它们指向同一个映射。

(3)通道(channel)

  • 特性:通道是引用类型,底层实现为指针。赋值和传参时复制的是指针。

示例

func sendData(ch chan int) {
    ch <- 100
}

func main() {
    ch := make(chan int, 1)
    sendData(ch)
    fmt.Println(<-ch) // 输出:100
}

  • 解释sendData 中的 chmain 中的 ch 指向同一个通道。

(4)接口(interface)

  • 特性:接口本身是一个包含类型和值的组合。赋值和传参时复制的是接口本身,但接口内部可能包含指向引用类型的值。

示例

type Speaker interface {
    Speak()
}

type Person struct {
    Name string
}

func (p *Person) Speak() {
    fmt.Println("Hello, my name is", p.Name)
}

func modifyInterface(s Speaker) {
    if p, ok := s.(*Person); ok {
        p.Name = "Bob"
    }
}

func main() {
    person := &Person{Name: "Alice"}
    modifyInterface(person)
    person.Speak() // 输出:Hello, my name is Bob
}
  • 解释modifyInterface 中的 smain 中的 person 指向同一个 Person 实例。

(5)指针(pointer)

  • 特性:指针存储的是变量的内存地址。赋值和传参时复制的是指针的值(地址)。

示例

gfunc modifyPointer(p *int) {
    *p = 100
}

func main() {
    a := 10
    modifyPointer(&a)
    fmt.Println(a) // 输出:100
}
  • 解释modifyPointer 中的 p 指向 a 的地址,对其解引用修改会影响 a


四、总结

  1. 值传递

    • 基本类型数组结构体等值类型变量,赋值和传参时会复制整个值,对副本的修改不影响原始变量。

  2. 引用行为的值传递

    • 切片映射通道接口指针函数等引用类型变量,赋值和传参时复制的是引用(指针)的副本。

    • 由于这些引用类型内部包含指针,指向底层数据结构,因此对它们的操作可能影响原始数据。

  3. 注意事项

    • 切片的扩容:当切片发生扩容时,可能会创建新的底层数组,此时新切片与原始切片不再共享底层数组。

    • 映射的并发安全:映射在并发访问时需要加锁保护,否则可能发生竞态条件。

    • 结构体中的引用类型字段:即使结构体是值类型,如果其中包含引用类型的字段,复制时这些字段仍指向相同的底层数据。


五、实用建议

  1. 理解类型特性:在编写代码时,清楚地了解每种类型的传递方式和行为,避免意外修改原始数据。

  2. 必要时使用指针:对于需要在函数中修改原始值类型变量的情况,可以使用指针传递。

  3. 避免不必要的副本:对于大型结构体或数组,频繁的复制会带来性能开销,可以考虑使用指针或切片。

  4. 并发编程注意事项:引用类型在并发环境下需要注意数据的一致性和安全性。


六、总结一句话

在 Go 语言中,所有参数传递都是值传递,但对于包含指向底层数据的引用类型变量(如切片、映射、通道等),由于复制的是指向同一底层数据的引用,所以对它们的操作可能会影响到原始数据,这种行为看起来类似于引用传递。