先看如下代码, 看看运行结果如何:
func main() { var c = [3]int{1, 2, 3} //定义一个长度为3的int类型的数组 d := c //将数组c赋值给d d[1] = 100 //修改数组d中索引为1的值为100 fmt.Printf("c的值是%v,c的内存地址是%p ", c, &c) //c的值是[1 2 3],c的内存地址是0xc42000a180 fmt.Printf("d的值是%v,d的内存地址是%p ", d, &d) //d的值是[1 100 3],d的内存地址是0xc42000a1a0 var a = []int{1, 2, 3, 4, 5} //creates and array and returns a slice reference b := a //此时a,b都指向了内存中的[1 2 3 4 5]的地址 b[1] = 10 //相当于修改同一个内存地址,所以a的值也会改变 fmt.Printf("a的值是%v,a的内存地址是%p ", a, &a) //a的值是[1 10 3 4 5],a的内存地址是0xc42000a180 fmt.Printf("b的值是%v,b的内存地址是%p ", b, &b) //b的值是[1 10 3 4 5],b的内存地址是0xc42000a1a0 }
运行结果:
D:GoProjectsrcmain>go run main.go c的值是[1 2 3],c的内存地址是0xc000190020 d的值是[1 100 3],d的内存地址是0xc000190040 a的值是[1 10 3 4 5],a的内存地址是0xc0001840a0 b的值是[1 10 3 4 5],b的内存地址是0xc0001840c0
至于原因 注释已经解释了, c是数组 值类型,a是切片引用类型。 来看看他们忘得的一些介绍吧:
数组是内置(build-in)类型
是一组同类型数据的集合,它是值类型,通过从0开始的下标索引访问元素值。在初始化后长度是固定的,无法修改其长度。当作为方法的参数传入时将复制一份数组而不是引用同一指针。数组的长度也是其类型的一部分,通过内置函数len(array)获取其长度。
注意:和C中的数组相比,又是有一些不同的
1. Go中的数组是值类型,换句话说,如果你将一个数组赋值给另外一个数组,那么,实际上就是将整个数组拷贝一份
2.如果Go中的数组作为函数的参数,那么实际传递的参数是一份数组的拷贝,而不是数组的指针。这个和C要区分开。因此,在Go中如果将数组作为函数的参数传递的话,那效率就肯定没有传递指针高了。
3. array的长度也是Type的一部分,这样就说明[10]int和[20]int是不一样的。
数组初始化
[5] int {1,2,3,4,5}长度为5的数组,其元素值依次为:1,2,3,4,5 [5] int {1,2}长度为5的数组,其元素值依次为:1,2,0,0,0 //在初始化时没有指定初值的元素将会赋值为其元素类型int的默认值0,string的默认值是"" [...] int {1,2,3,4,5}长度为5的数组,其长度是根据初始化时指定的元素个数决定的 [5] int { 2:1,3:2,4:3}长度为5的数组,key:value,其元素值依次为:0,0,1,2,3。在初始化时指定了2,3,4索引中对应的值:1,2,3 [...] int {2:1,4:3}长度为5的数组,起元素值依次为:0,0,1,0,3。由于指定了最大索引4对应的值3,根据初始化的元素个数确定其长度为5赋值与使用
内置类型Slices切片(“动态数组")
与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。切片中有两个概念:一是len长度,二是cap容量,长度是指已经被赋过值的最大下标+1,可通过内置函数len()获得。容量是指切片目前可容纳的最多元素个数,可通过内置函数cap()获得。切片是引用类型,因此在当传递切片时将引用同一指针,修改值将会影响其他的对象。
切片可以通过数组来初始化,也可以通过内置函数make()初始化.初始化时len=cap,在追加元素时如果容量cap不足时将按len的2倍扩容 查看示例代码,在线运行示例代码
切片初始化
s :=[] int {1,2,3 } 直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3 s := arr[:] 初始化切片s,是数组arr的引用 s := arr[startIndex:endIndex] 将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片 s := arr[startIndex:] 缺省endIndex时将表示一直到arr的最后一个元素 s := arr[:endIndex] 缺省startIndex时将表示从arr的第一个元素开始 s1 := s[startIndex:endIndex] 通过切片s初始化切片s1 s :=make([]int,len,cap) 通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片
slice可以从一个数组或一个已经存在的slice中再次声明。slice通过array[i:j]来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i
我们再来看看 值类型 和引用类型的一些例子吧
值类型
值类型包括基本数据类型,int,float,bool,string,以及数组和结构体(struct)。值类型变量声明后,不管是否已经赋值,编译器为其分配内存,此时该值存储于栈上。值类型的默认值:
var a int //int类型默认值为 0 var b string //string类型默认值为 nil空 var c bool //bool类型默认值为false var d [2]int //数组默认值为[0 0] fmt.Println(&a) //默认已经分配内存地址,可以使用&来取内存地址
当使用等号=将一个变量的值赋给另一个变量时,如 j = i ,实际上是在内存中将 i 的值进行了拷贝,可以通过 &i 获取变量 i 的内存地址。此时如果修改某个变量的值,不会影响另一个。
//变量的赋值 var a =10 //定义变量a b := a //将a的值赋值给b b = 101 //修改b的值,此时不会影响a fmt.Printf("a的值是%v,a的内存地址是%p ",a,&a) //a的值是10,a的内存地址是0xc42000e228 fmt.Printf("b的值是%v,b的内存地址是%p ",b,&b) //b的值是101,b的内存地址是0xc42000e250 //数组的赋值 var c =[3]int{1,2,3} //定义一个长度为3的int类型的数组 d := c //将数组c赋值给d d[1] = 100 //修改数组d中索引为1的值为100 fmt.Printf("c的值是%v,c的内存地址是%p ",c,&c) //c的值是[1 2 3],c的内存地址是0xc42000a180 fmt.Printf("d的值是%v,d的内存地址是%p ",d,&d) //d的值是[1 100 3],d的内存地址是0xc42000a1a0
画图示例:
引用类型
引用类型包括指针,slice切片,map ,chan,interface。变量直接存放的就是一个内存地址值,这个地址值指向的空间存的才是值。所以修改其中一个,另外一个也会修改(同一个内存地址)。引用类型必须申请内存才可以使用,make()是给引用类型申请内存空间。
var a = []int{1,2,3,4,5} b := a //此时a,b都指向了内存中的[1 2 3 4 5]的地址 b[1] = 10 //相当于修改同一个内存地址,所以a的值也会改变 c := make([]int,5,5) //切片的初始化 copy(c,a) //将切片acopy到c c[1] = 20 //copy是值类型,所以a不会改变 fmt.Printf("a的值是%v,a的内存地址是%p ",a,&a) //a的值是[1 10 3 4 5],a的内存地址是0xc42000a180 fmt.Printf("b的值是%v,b的内存地址是%p ",b,&b) //b的值是[1 10 3 4 5],b的内存地址是0xc42000a1a0 fmt.Printf("c的值是%v,c的内存地址是%p ",c,&c) //c的值是[1 20 3 4 5],c的内存地址是0xc42000a1c0 d := &a //将a的内存地址赋值给d,取值用*d a[1] = 11 fmt.Printf("d的值是%v,d的内存地址是%p ",*d,d) //d的值是[1 11 3 4 5],d的内存地址是0xc420084060 fmt.Printf("a的值是%v,a的内存地址是%p ",a,&a) //a的值是[1 11 3 4 5],a的内存地址是0xc420084060
a,b,c底层数组是一样的,但是上层切片不同,所以内存地址不一样。
下面是来之网上的 《关于 Go 中 Map 类型和 Slice 类型的传递》内容
Map 类型
先看例子 m1:
func main() { m := make(map[int]int) mdMap(m) fmt.Println(m) } func mdMap(m map[int]int) { m[1] = 100 m[2] = 200 }
结果是 map[2:200 1:100]
我们再修改如下 m2:
func main() { var m map[int]int mdMap(m) fmt.Println(m) } func mdMap(m map[int]int) { m = make(map[int]int) m[1] = 100 m[2] = 200 }
发现结果变成了 map[]
要理解这个问题,需要明确在 Go 中不存在引用传递,所有的参数传递都是值传递。
现在再来分析下,如图:
可能有些人会有疑问,为什么途中的 m 像是一个指针呢。查看官方的 Blog 中有写:
Map types are reference types, like pointers or slices, ...
这边说 Map 类型是引用类型,像是指针或是 Slice(切片)。所以我们基本上可以把它当作是指针来看待(注意,只是近似,或者说其中含有指针,其内部仍然含有其他信息,这里只是为了便于理解),只不过这个指针有些特殊罢了。
m1 中,当调用 mdMap
方法时重新开辟了内存,将 m 的内容,也就是 map 的地址拷贝入了 m',所以此时当操作 map 时,m 和 m' 所指向的内存为同一块,就导致 m 的 map 发生了改变。
而在 m2 中,在调用 mdMap
之前,m 并未分配内存,也就是说并未指向任何的 map 内存区域。从未导致 m' 的 map 修改不能反馈到 m 上。
Slice 类型
现在看一下 Slice。
s1:
func main() { s := make([]int, 2) mdSlice(s) fmt.Println(s) } func mdSlice(s []int) { s[0] = 1 s[1] = 2 }
s2:
func main() { var s []int mdSlice(s) fmt.Println(s) } func mdSlice(s []int) { s = make([]int, 2) s[0] = 1 s[1] = 2 }
不出所料:
s1 结果为 [1 2]
s2 为 []
因为正如官方所说,Slice 类型与 Map 类型一样,类似于指针,Slice 中仍然含有长度等信息。
修改一下 s1,变成 s3:
func main() { s := make([]int, 2) mdSlice(s) fmt.Println(s) } func mdSlice(s []int) { s = append(s, 1) s = append(s, 2) }
不再修改 slice 原先的两个元素,而加上另外两个,结果为:[0 0]
发现修改并没有反馈到原先的 slice 上。
这里我们需要把 slice 想象为特殊的指针,其已经保存了所指向内存区域长度,所以 append
之后的内存并不会反映到 main()
中,当新元素通过调用 append
函数追加到切片末尾时,如果超出了容量,append
内部会创建一个新的数组。并将原有数组的元素被拷贝给这个新的数组,最后返回建立在这个新数组上的切片。这个新切片的容量是旧切片的二倍(译者注:当超出切片的容量时,append
将会在其内部创建新的数组,该数组的大小是原切片容量的 2 倍。最后 append
返回这个数组的全切片,即从 0 到 length - 1 的切片):
那如何才能反映到 main()
中呢?没错,使用指向 Slice 的指针。
func mdSlice(s *[]int) { *s = append(*s, 1) *s = append(*s, 2) }
内存如图所示:
注意本文中内存区域分配是否连续完全随机,不影响程序,只是为了图解清晰。
Chan 类型
Go 中 make
函数能创建的数据类型就 3 类:Slice, Map, Chan。不比多说,相比读者已经能想象 Chan 类型的内存模型了。的确如此,读者可以自己尝试,这边就不过多赘述了。(可以通通过 == nil 的比较来进行测试)。
参考:
https://www.cnblogs.com/itbsl/p/10599948.html
https://www.cnblogs.com/liuzhongchao/p/9159896.html