go

golang的值传递

介绍

在golang中只有值传递,没有引用传递。是哪个传递其实就是看值类型变量和引用类型变量作为函数参数时,修改形参是否会影响到实参。这里涉及到几个概念,下面分别介绍一下:

  • 值传递:指在调用函数时将实参复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实参。
  • 引用传递:指在调用函数时将实参的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实参。
  • 值类型:变量直接存储值,内存通常在栈上分配,栈在函数调用完会被释放。比如:int、float、bool、string、array、sturct 等。
  • 引用类型:变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,通过GC回收。比如:指针,slice,map,channel,interface,func 等。
  • 深拷贝:值类型作为参数时,称为深拷贝,形参改变,实参不变,因为传递的是值的副本,形参会新开辟一块空间,与实参指向不同。如果希望值类型数据在修改形参时实参跟随变化,可以把参数设置为指针类型。
  • 浅拷贝:引用类型作为参数时,称为浅拷贝,形参改变,实参跟随变化。因为传递的是地址,形参和实参都指向同一块地址。

在官方文档中有关于值传递的解释:https://go.dev/doc/faq#pass_by_value

与 C 家族中的所有语言一样,Go 中的所有内容都是按值传递的。也就是说,函数总是获取所传递内容的副本,就好像有一个赋值语句将值分配给参数一样。例如,将 int 值传递给函数会生成 int 的副本,传递指针值会生成指针的副本,但不会复制它指向的数据。

映射和切片值的行为类似于指针:它们是包含指向底层映射或切片数据的指针的描述符。复制map或切片的值不会复制它指向的数据。复制接口的值会复制存储在接口值中的内容。如果接口值包含一个结构体,则复制接口值会生成该结构体的副本。如果接口值包含一个指针,则复制接口值会复制该指针,但不会复制它所指向的数据。

示例

下面分别用示例演示。

值类型

基本类型

package main

import "fmt"

func modInt(x int) {
    fmt.Printf("形参地址为%p,值是%v\n", &x, x)
    x = 30
}

func main() {
    a := 20
    fmt.Printf("实参地址为%p,值是%v\n", &a, a)
    modInt(a)
}

实参地址为0xc00001a098,值是20
形参地址为0xc00001a0d0,值是20

modInt 函数试图修改变量 x 的值,但是因为 x 是通过值传递的,所以它只是 a 的一个副本,a 的值在函数外部不会被改变,地址也不一样。

复合类型

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func modStruct(p Person) {
    p.Age = 30
}

func main() {
    p := Person{Name: "Tom", Age: 20}
    fmt.Printf("实参地址为%p,值是%v\n", &p, p)
    modStruct(p)
}

实参地址为0xc000008078,值是{Tom 20}
形参地址为0xc0000080a8,值是{Tom 20}

引用类型

切片

package main

import "fmt"

func modSlice(x []int) {
    fmt.Printf("形参切片地址为%p,值是%v\n", &x, x)
    fmt.Printf("形参切片第一个元素地址为%p,值是%v\n", &x[0], x[0])
    x[0] = 30
}

func main() {
    s := []int{10, 20, 30}
    fmt.Printf("切片地址为%p,值是%v\n", &s, s)
    fmt.Printf("切片第一个元素地址为%p,值是%v\n", &s[0], s[0])
    modSlice(s)
    fmt.Println(s)
}

切片地址为0xc000008078,值是[10 20 30]
切片第一个元素地址为0xc000010120,值是10
形参切片地址为0xc0000080a8,值是[10 20 30]
形参切片第一个元素地址为0xc000010120,值是10
[30 20 30]

虽然 modSlice 函数看似接收的是一个值(切片的副本),但是切片内部包含了指向底层数组的指针。slice本身是个结构体,但它内部第一个元素是一个指针类型,指向底层的具体数组,slice在传递时,形参是拷贝的实参这个slice,但他们底层指向的数组是一样的,拷贝slice时,其内部指针的值也被拷贝了,也就是说指针的内容一样,都是指向同一个数组,所以函数内部的修改会影响到原始的切片。

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

map

package main

import "fmt"

func modMap(m map[string]int) {
    fmt.Printf("形参地址为%p,值是%v\n", &m, m)
    m["Tom"] = 30
}

func main() {
    m := map[string]int{"Tom": 20}
    fmt.Printf("实参地址为%p,值是%v\n", &m, m)
    modMap(m)
    fmt.Println(m)
}

实参地址为0xc00000a028,值是map[Tom:20]
形参地址为0xc00000a038,值是map[Tom:20]
map[Tom:30]

可以看到map中实参和形参的地址都不一样,说明传递的也是一份拷贝。

指针

package main

import "fmt"

func modPointer(x *int) {
    fmt.Printf("形参地址为%p,值是%v\n", &x, *x)
    *x = 30
}

func main() {
    a := 20
    fmt.Printf("实参地址为%p,值是%v\n", &a, a)
    modPointer(&a)
}

实参地址为0xc00001a098,值是20
形参地址为0xc00000a030,值是20
30

可以看到指针中实参和形参的地址都不一样,说明传递的也是一份拷贝。输出30,因为a的值通过指针被修改了。

总结

无论是值类型还是引用类型,在函数调用时都是通过值传递的方式传递参数。

  • 对于值类型,函数接收的是实际参数值的副本;

  • 对于切片、映射、通道等引用类型,虽然看起来是值传递,但由于它们内部包含指向数据结构的指针,所以它们的行为更像是引用传递。函数接收的是指向实际参数值的指针的副本。这样可以保证函数的执行不会对原始参数产生副作用,并提供了更好的安全性和可预测性。

分类: go
0 0 投票数
文章评分
订阅评论
提醒
guest

0 评论
内联反馈
查看所有评论

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部
0
希望看到您的想法,请您发表评论x