【Go语言】深入理解值类型和引用类型(实例分析)

1.实例

今天在刷博客的时候看到了这么一个观点:切片并不是纯引用类型。
给出的证明过程是:

图一:

//(该代码引用于原博主,仅用于在此探讨问题,无恶意)
package main
 
 import "fmt"
 
 func main(){
 
         a :=[...]int{0,1,2,3,4,5,6,7,8,9}
         fmt.Println(a)
 
         s1:=a[:]
         s2:=s1[:]
         fmt.Println(s1)
         fmt.Println(s2)
 
         fmt.Println("")
         fmt.Println("-------------------------------")
         fmt.Println("")
         
         a[9]=10
         s2[0]=100
         fmt.Println(a)
         fmt.Println(s1)
         fmt.Println(s2)
 
 }
 /*input:
 [0 1 2 3 4 5 6 7 8 9]
[0 1 2 3 4 5 6 7 8 9]
[0 1 2 3 4 5 6 7 8 9]

-------------------------------

[100 1 2 3 4 5 6 7 8 10]
[100 1 2 3 4 5 6 7 8 10]
[100 1 2 3 4 5 6 7 8 10]*/

先根据输出结果证明s2改变使得s1和a受到影响,证明了切片为引用类型

图二:

/*//(该代码引用于原博主,仅用于在此讨论问题,无恶意)*/
package main

import "fmt"

func add2(s []int,x int){
    s=append(s,x);
}

func main(){
    
    a :=[...]int{0,1,2,3,4,5,6,7,8,9}
    fmt.Println(a)
    
    s1:=a[5:8]//len=3 cap=5
    fmt.Println(s1)
    
    add2(s1,0)
    fmt.Println(a)
    fmt.Println(s1)
    
    add2(s1,1)
    fmt.Println(a)
    fmt.Println(s1)

}//input
/*[0 1 2 3 4 5 6 7 8 9]
[5 6 7]
[0 1 2 3 4 5 6 7 0 9]
[5 6 7]
[0 1 2 3 4 5 6 7 1 9]
[5 6 7]*/

在根据这个数组的 8依次变成了 0、1,但slice s1始终没变,说明slice并不是纯引用类型。

以上就是证明的过程,看完之后是否也产生了一丝丝疑惑,因为再上一篇文章探讨切片内存结构时我们也说了,数组的空间地址和该数组下标0的头号元素地址一致,而切片就不同!

fmt.Printf("arr1首地址:%p\n,arr1【0】首地址:%p\n,slice首地址:%p\n,slice【0】地址%p\n",&arr_1,&arr_1[0],&slice_1,&slice_1[0])
	/*arr1首地址:0xc0420480a0
     arr1【0】首地址:0xc0420480a0
	 slice首地址:0xc0420443e0
	 slice【0】地址0xc0420820a0

切片本质就是一个结构体,他里面包含三部分:address + len + cap,
address: 就是他指向的内部数组或数组某个地方
len:是当前的元素个数
cap:可容纳元素总容量大小
并且证明了他是引用类型:

func test_3(a []int){
 	fmt.Printf("%p",&a)//0xc042002440
	a[0] = 999
}
 var slice_1 = make([]int,10,10)
 test_3(slice_1)
 fmt.Printf("%p",&slice_1)//0xc042002420
 fmt.Println(slice_1)//[999 0 0 0 0 0 0 0 0 0]

我们可以看到,实参slice_1在函数调用时作为参数传过去了,而a的地址和slice_1地址不同,也就是两个不同的空间,但是对a进行操作后,最终打印slice_1我们发现他的值也变化了,故slice_1空间的内容复制给a空间的是地址值,然后a对该地址指向的空间进行操作也会影响到slice_1,所以是引用传递。

那么为什么上面图二中把s1的值传过去后并append,但是原来的实参s1并没有发生变化呢?
其实从输出的a数组变化我们已经能从侧面证明是引用传递了,主要是解决为啥s1没变这个问题。
归根结底是我们忽略了切片的空间中其他元素和append函数两个因素,我们先摘出来图二中方法调用过程,把无关操作先去除,并添加一些打印语句方便观察

package main

import "fmt"

func add2(s []int,x int){
	fmt.Printf("s地址值:%p\n",&s)//s地址值:0xc042002420,空间地址不同
	fmt.Println(&s[0])//0xc0420120c8,指向数组相同
    s=append(s,x);
}

func main(){
    
    a :=[...]int{0,1,2,3,4,5,6,7,8,9}
    s1:=a[5:8]//len=3 cap=5
	fmt.Printf("s1地址值:%p\n",&s1)//s1地址值:0xc042002400
	fmt.Println(&s1[0])//0xc0420120c8
	
	add2(s1,0)

}

那么这个调用过程干了什么?无非就是把s1赋值给s,即实参s1的值复制一份给形参s,s1和s是两个首地址不同的空间,他俩的相同点就是空间里的内容相同,指向同一个数组,然后对s进行append。
(也就是main方法中调用add方法,add方法栈帧入栈,main栈帧中局部变量表中s1的值复制给add栈帧中s的值,然后s也指向该数组,接下来append方法栈帧入栈,执行在把s复制传过去,最后执行完毕append方法出栈,add方法出栈,main方法也就执行完了main栈帧出栈,主线程main栈空结束)
那我们完全可以把这个过程类比一下,即把s1赋值给s5,然后直接对s5进行append,随后输出比对

 	a :=[...]int{0,1,2,3,4,5,6,7,8,9}
    fmt.Println(a)
    
    s1:=a[5:8]//len=3 cap=5
	s5:=s1
	s5=append(s5,0)
	fmt.Println("a",a)
	fmt.Println("s1:",s1)
	fmt.Println("s5:",s5)
	fmt.Printf("%p %p\n",&s1,&s5)
	fmt.Println(&s1[0],&s5[0])
	fmt.Println(len(s1),len(s5))
	fmt.Println(cap(s1),cap(s5))
	/*input:
	a [0 1 2 3 4 5 6 7 0 9]
	s1: [5 6 7]
	s5: [5 6 7 0]
	0xc0420443c0 0xc0420443e0
	0xc04207c028 0xc04207c028
	3 4
	5 5
*/

从结果可以看出来:
a发生变化了么? 变了(8变为0)
s1发生变化了么?没有(仍然5 6 7)
s1和s5是同一个空间么?不是;(输出地址不同)
s1和s5指向同一个数组地址么?是的;(数组首地址相同)
也就是说s1和s5仍然维护的是同一个数组即指向同一个空间,但是由于s1的len被限制到3了,他的长度只能输出5 6 7 这三个元素,根本显示不了新加入的元素的值,而s5是在不超过cap情况下append了,len输出是4,因此可以显示,我们通过a可以发现值已经变,所以说切片肯定是引用传递
我们只用接下来在后面加上两句,使得s1能够体现出来数组变化,这样就更直观

	s5[0] = 999
	fmt.Println(s1)
	/*s1: [999 6 7]*/

不过要注意的是,这里append没有超过s1中cap的值,不然即使切片是引用传递,也会因为append扩容新创建数组导致两者空间中指向的数组地址不一样

	s2 := make([]int,2,5)
	
	s3 :=s2
	fmt.Println("s3:",s3)
	fmt.Println("s2:",s2)
	fmt.Println(&s3[0]==&s2[0])

	fmt.Println("-------------------------------------------------------------------初始值:")
	fmt.Printf("s3地址:%p, s3[0]地址:%p ,s3的len:%d ,s3的cap:%d\n",&s3,&s3[0],len(s3),cap(s3))
	s3 = append(s3,1)//扩容
	fmt.Println("-------------------------------------------------------------------第1次扩容:")
	fmt.Printf("s3地址:%p, s3[0]地址:%p ,s3的len:%d ,s3的cap:%d\n",&s3,&s3[0],len(s3),cap(s3))
	s3 = append(s3,1)//扩容
	fmt.Println("-------------------------------------------------------------------第2次扩容:")
	fmt.Printf("s3地址:%p, s3[0]地址:%p ,s3的len:%d ,s3的cap:%d\n",&s3,&s3[0],len(s3),cap(s3))
	s3 = append(s3,1)//扩容
	fmt.Println("-------------------------------------------------------------------第3次扩容:")
	fmt.Printf("s3地址:%p, s3[0]地址:%p ,s3的len:%d ,s3的cap:%d\n",&s3,&s3[0],len(s3),cap(s3))
	s3 = append(s3,1)//扩容
	fmt.Println("-------------------------------------------------------------------第4次扩容:")
	fmt.Printf("s3地址:%p, s3[0]地址:%p ,s3的len:%d ,s3的cap:%d\n",&s3,&s3[0],len(s3),cap(s3))
	s3 = append(s3,1)//扩容
	fmt.Println("-------------------------------------------------------------------第5次扩容:")
	fmt.Printf("s3地址:%p, s3[0]地址:%p ,s3的len:%d ,s3的cap:%d\n",&s3,&s3[0],len(s3),cap(s3))
	
	fmt.Println(&s3[0]==&s2[0])
	fmt.Println("s3:",s3)
	fmt.Println("s2:",s2)
	s3[1]=999
	fmt.Println("s3:",s3)
	fmt.Println("s2:",s2)

输出:
在这里插入图片描述
可以看出来在第四次扩容时,就直接新创建数组并且拷贝旧的数组值,在输出两者的头号元素也就是数组地址时,已经从true变为false。
以上便是对引用传递的实例分析,可以帮助我们更好的理解引用传递

2.引用类型和值类型

引用类型:该变量空间储存的引用值,例如:指针,slice切片,map ,chan ,interface
值类型:变量空间存的是真实值,例如:int系列,float系列,bool,string, 数组和结构体struct
因为go参数的传递用的是值拷贝,就如上面切片的例子一样,每次调用函数时都会把实参的值拷贝一份给形参,然后在函数里操作,正因此我们可以通过实参调用前后的值是否变化来判断他属于哪种类型,
引用类型空间保存的是地址值,那么当函数调用时会将该地址值拷贝给形参,那么形参和实参指向的是同一个空间,一个改变都会影响!
值类型空间存的就是真实值,传递时直接把值拷贝一份给形参,那么形参操作的就是自己空间中的数据,根本不会相互影响!

引用类型已经看了,这里在看看值类型:举一个结构体的例子:

type Person struct{
	name string
	id int
}

func demo(per Person){
	per.id = 1
	fmt.Println("修改了:",per)
}

	per_1 := Person{
		name:"大哈",
		id:26,
	}
	
	fmt.Println("调用前:",per_1)
	demo(per_1)
	fmt.Println("调用后:",per_1)
	/*
	调用前: {大哈 26}
	修改了: {大哈 1}
	调用后: {大哈 26}
	*/

可以看出来实参的值传给形参,形参改完对实参没有任何影响,这说明实参给新参传递的内容就直接是具体值而不是引用地址,所以值拷贝了后形参就是对自己空间的内容直接操作,根本不会影响到实参!
值得一提的是go中因为不能对地址进行加减操作,所以go的语法糖会直接把对引用的操作转化对引用所指向的内存地址进行操作。

	var per Person
	fmt.Println("per: ",per)//值类型的,定义了就有默认值所以即使不符值也不影响使用
	per.name = "大哈"
	per.id = 01
	fmt.Println("per: ",per)//per:  {大哈 1}

	per2:=per
	per2.name = "小哈"
	fmt.Println(per,per2)//{大哈 1} {小哈 1},说明per1和per2两空间里直接存的是具体值
	/*说明赋值了之后是值传递,只是把1空间的值复制给2,两个空间操作互不影响*/

	var per3 *Person = new (Person)//指针类型必须赋值,不然默认值为nil,底下用时就报空指针异常
	per3.name = "结构体指针类型per3"
	(*per3).id = 2
	fmt.Println("per3: ",per3)

	per4:=per3
	per4.name = "per4替代"
	fmt.Println(per3,per4)
	fmt.Println(per4==per3)
/*	输出:
	per:  { 0}
	per:  {大哈 1}
	{大哈 1} {小哈 1}
	per3:  &{结构体指针类型per3 2}
	&{per4替代 2} &{per4替代 2}
	true
	*/

我们看到pre3是指针类型的,理论上来讲操作结构体里的属性时应该用
(*per3).id = 2来操作,即 指向.属性
但是per3.name = “结构体指针类型per3”,不加也星号来指向也能操作对应的属性。
这里也可以看出来,开始时是直接per2:=per值传递,所以改了per2不会影响到per,那么怎么样才能使得互相影响呢,那就不要传递值,改为传递该结构体变量空间的地址值,这样就变为了引用传递,最后
per4:=per3
per4.name = “per4替代”
使得一个地方改都改

还有就是我们在进行“==”操作时比较的是空间内的具体值而不是该空间首地址;
值类型比较:

	 str := "A"
	 str_2:="A"
	 fmt.Println(str == str_2)
	 fmt.Println(&str,&str_2)

	 value := 1
	 value_2 :=1
	 fmt.Println(value_2 == value)
	 fmt.Println(&value_2,&value)
	/*
	true
	0xc04203e1d0 0xc04203e1e0
	true
	0xc04204a0f0 0xc04204a098
	*/

可以看出来空间地址不同,但是值相同则比较为true
引用类型:
这里指针可以通过==比较,但是切片和map就不可以会报错

 	 var ptr *int = &value
	 var ptr_2 *int = &value_2
	 fmt.Println(ptr_2 == ptr)
	 fmt.Println(&ptr_2,&ptr)
	 /*
	false
	0xc042004038 0xc042004030
	*/

可以通过==看出来俩空间存的指针不同,而且俩空间首地址也不同!
但是同样作为引用类型的切片和map就不能这么比

	s1 := make(map[int]int,10)
	s2 := make(map[int]int,10)
	fmt.Println(s1==s2)
	/*invalid operation: s1 == s2 (map can only be compared to nil)*/

问题

遇到的一些问题:
1.如何打印出切片空间具体保存的内容,我们知道切片本质就是一个结构体,他里面包含三部分:address + len + cap,但是打印的话直接就输出的是指向的数组值。也就是说要想知道两个切片空间内容一致只能通过打印指向的数组头号元素地址来比对,在通过len和cap得到全部信息!
2.append函数源码具体位置,src下找也找了,上网搜的也是一个汇编文档,没找到具体代码实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值