值传递、引用传递、址传递

本文深入探讨了Go语言中的值传递、址传递(指针传递)和引用传递的区别。值传递会创建参数副本,而址传递通过指针传递,允许函数内部修改实参。Go语言虽然没有真正的引用传递,但通过指针实现类似效果。文章通过实例展示了不同类型参数在函数调用时的副本创建过程,并给出了选择T或*T作为参数的考虑因素。最后,讨论了何时会发生副本创建及不同类型的副本创建成本。
摘要由CSDN通过智能技术生成

分类

从类型的角度来讲 , 类型可以分为值类型和引用类型

从传递的角度来讲,分为值传递、址传递、引用传递。传递是在函数或者方法调用时才会提到的概念

从参数的角度来讲,分为实参和形参。

实参和形参

实参 , 是调用时真实传递给函数或者方法的参数 , 如 add(1,2) , 1和2就是实参

形参 , 是定义函数或者方法时设立的参数,实际不存在,主要是为了接受主调函数传入的数据,如 func add(a ,b int){} a,b就是形参。

引用

值是一个宽泛的范围 , 一切皆值 。

值传递 :实参copy自身内容传递给形参,那么转到形参的角度来讲 ,得到了一个副本。

 

地址 , 指的是内存地址 , 指代一块内存 , 我们可以通过地址 ,访问内存中的数据

址传递:值传递中的特殊情况 , 实参是指针类型,同样也是copy一份传递给形参 , 但是这个有区别的一点就是不管是指针本身还是副本 , 都指向同一块内存。

 

引用 , 是某块内存的一个别名 , 比如我们给小明起个别名:明明 , 那么明明 就是小明

引用传递:实参的地址传递给形参,函数内部针对形参的修改,影响到实参,则认为是引用传递。

 

go语言的值传递

go中虽然有指针和引用 , 但是go中只有值传递。

为什么go没有址传递?

指针也是值的一种特殊形式,当我们传递指针类型的实参给形参时 , 也会copy自身一份,但是实参的内容是一样。举例如下:

 

func main(){
   f := Person{}
   fmt.Printf("实参的地址:%p\n",&f)
   test(&f)
}
func test(f *Person){
   fmt.Printf("形参的指针类型的地址:%p\n形参的内容:%p",&f,f)
}

实参的地址:0xc000088230 形参的指针类型的地址:0xc0000d8020 形参的内容:0xc000088230

实参的地址 , copy自身,座位形参变量的内容。

 

副本的创建

T的副本创建

首先看一下 参数类型为T的函数调用的情况:

 

package main
​
import "fmt"
​
type Bird struct {
    Age  int
    Name string
}
​
func passV(b Bird) {
    b.Age++
    b.Name = "Great" + b.Name
    fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p\n", b, &b)
}
​
func main() {
    parrot := Bird{Age: 1, Name: "Blue"}
    fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p\n", parrot, &parrot)
    passV(parrot)
    fmt.Printf("调用后原始的Bird:\t %+v, \t\t内存地址:%p\n", parrot, &parrot)
}

运行后输入结果(每次运行指针的值可能不同):

原始的Bird:         {Age:1 Name:Blue},         内存地址:0xc420012260
传入修改后的Bird:  {Age:2 Name:GreatBlue},    内存地址:0xc4200122c0
调用后原始的Bird:  {Age:1 Name:Blue},         内存地址:0xc420012260

可以看到,在T类型作为参数的时候,传递的参数parrot会将它的副本(内存地址0xc4200122c0)传递给函数passV,在这个函数内对参数的改变不会影响原始的对象。

*T的副本创建

修改上面的例子,将函数的参数类型由T改为*T:

package main
​
import "fmt"
​
type Bird struct {
    Age  int
    Name string
}
​
func passP(b *Bird) {
    b.Age++
    b.Name = "Great" + b.Name
    fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *b, b, &b)
}
​
func main() {
    parrot := &Bird{Age: 1, Name: "Blue"}
    fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p, 指针的内存地址: %p\n", *parrot, parrot, &parrot)
    passP(parrot)
    fmt.Printf("调用后原始的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *parrot, parrot, &parrot)
}
原始的Bird:         {Age:1 Name:Blue},         内存地址:0xc420076000, 指针的内存地址: 0xc420074000
传入修改后的Bird:  {Age:2 Name:GreatBlue},    内存地址:0xc420076000, 指针的内存地址: 0xc420074010
调用后原始的Bird:  {Age:2 Name:GreatBlue},    内存地址:0xc420076000, 指针的内存地址: 0xc420074000

 

可以看到在函数passP中,参数p是一个指向Bird的指针,传递参数给它的时候会创建指针的副本(0xc420074010),只不过指针0xc4200740000xc420074010都指向内存地址0xc420076000。 函数内对*T的改变显然会影响原始的对象,因为它是对同一个对象的操作。

当然,一位对Go有深入了解的读者都已经对这个知识有所了解,也明白了T*T作为参数的时候副本创建的不同。

如何选择 T*T

1、copy副本的成本

2、是否希望实参被改变

3、函数域内的参数,定义成T , 会分配到栈上

 

什么时候发生副本创建

1、变量赋值

2、map、slice和数组

3、for - range ,会生成副本 给 v

4、channel 发送和接收数据

5、形参和返回值

6、方法接受者 func (b B) test(){}

 

不同类型的副本创建

1、bool、数值、指针 , 对象很小创建副本成本小,可以直接使用T

2、数组是值类型,大数组要慎重

3、map、slice、channel是有一个指针指向底层的数据结构,副本生成成本小

4、string 的任何操作都会生成新串

5、将一个函数复制给变量时 ,创建了一次函数对象的指针。

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值