Golang Slice 数组的区别,以及指针,值 传递,append, for range 常见题一文搞懂
常见题目
比如面试中常问的问题
func main(){
//文字描述:
//定义了一个长度为3,容量为10的切片
//然后作为参数传递到了一个函数里,(这个函数的函数签名就是有参(这个切片类型的参)无返回)
//在这个函数里面做一些操作,先append了三个元素,然后修改切片下标为1的元素的值
//main里打印这个切片,请问打印结果是?
}
目录结构
myproject
---------mytest
-------my_test.go
翻译成代码就是:
package mytest
import (
"fmt"
"testing"
)
func TestMy(t *testing.T) {
x := make([]int,3,10)
fmt.Println(cap(x))
//fmt.Printf("11111-----%p\n", x) //%p用来看变量指向的地址,即对于切片来说就是看它指向的底层数组的地址。
//x = []int{1,2,4,4,3} //这么赋值相当于另一个切片赋给x了。 而不是给make初始化的切片赋值了。 可以看到x的地址变了。也就是底层数组变了。
//fmt.Printf("22222-----%p\n", x)
//fmt.Println(cap(x))
//x := make([]int, 0)
fmt.Printf("%p\n",x)
x = append(x, 1, 2, 3, 4) //追加四个元素
fmt.Printf("%p\n", x) //append的话,如果容量够,底层不需要扩容,还是原来的底层数组,因为一开始指定容量,就从内存弄出了容量这么大的底层数组了。
HandleSlice(x)
fmt.Println(x)
//m := make(map[int]int)
//m[4] = 8
//m[5] = 10
//HandleMap(m)
//fmt.Println(m)
}
func HandleSlice(v []int) { //v的地址和传进来的原切片是一个地址 引用传递
fmt.Printf("3333-----%p\n",v)
v = append(v, 5) //append原理,如果超容了,那这里v的底层就变了,下面再改,就影响不到原有切片了。
fmt.Printf("4444-----%p\n",v)
//v = []int{6,7,5}
v[1] = 5555
}
func HandleMap(m map[int]int) { //引用传递了
m[4] = 7888
}
func HandleInterface(v interface{}) {
}
func HandleChannel(c chan int) {
}
考察知识点
第一个,函数调用,传参,传进来的是引用传递还是参数传递,对外面变量是否有影响?
即golang对于函数传参是引用传递还是参数传递的问题。
第二个,切片的原理是啥?它传参进来属于引用还是值传递?
第三个,append的原理,是啥?
第四个,按下标索引切片的元素,底层逻辑是啥?(答:还是寻址访问)
就发现离不开指针这块,另外,在代码验证教材书逻辑时,会用到fmt的格式化打印的一些知识点,比如打印指针类型的值,下面也做相关补充说明。(%p是打印引用(指针)类型地址的)
内容
1、golang函数调用传参—值传递和引用传递
- 如果参数是个值类型,那就是值传递。比如:结构体(struct)、int, string 等。
- 如果参数是个引用类型,那就是引用传递。比如:切片(slice)、字典(map)、接口(interface)、通道(channel)这些go中自带的引用类型,以及值类型前加取地址符号&就是引用类型了。
- 值传递:传递参数的副本,函数接收参数副本之后,在使用过程可能对副本值进行更改但不会影响原来的变量。
- 引用传递:函数可以直接修改参数的值,那就传参数的地址。
2、切片、数组,以及append、索引元素
切片
slice是数组的引用。当它被初始化后,它指向一个底层数组。自然,它是引用类型。引用不需要使用额外的内存。
不同的切片的底层可以指向同一个底层数组。(即共享存储)。而数组不同数组,都是不同的存储。
与数组的相同点:可索引,具有长度。
不同点:
- 数组定长,声明时需要说明长度; 切片变长,可扩容(扩容时新容量为原来容量*2)
- 数组是值类型;切片是引用类型
- 切片有容量概念
切片的声明和初始化写法
切片声明方式:
var identifier []type (不需要说明长度)
切片初始化方式,三种:
-
make() ,即:
var slice1 []type = make([]type, len, cap)
也可以省去cap,简写。即,
slice1 []type = make([]type, len),省略cap,cap取len的值。
eg: v := make([]int, 10, 50) 底层是:分配一个有50个int值的数组,一个切片指向了这个数组的前10个元素。这个切片是被创建为长度为10,容量为50这样属性的切片的,其名为v。
fmt.Println(v) //output: [0 0 0 0 0 0 0 0 0 0] 是10个零
fmt.Println(append(v, 1)) //output: [0 0 0 0 0 0 0 0 0 0 1] 是10个零后面追加了一个1
(注意append是在底层数组len后追加)
2.
var slice1 []type = arr1[start:end]
//从数组arr1中取下标start到end-1之间的元素的数组,被slice1指向
3.
var slice1 = []int{2,3,5,11}
//类似数组的方式初始化
索引元素
切片取值时索引值大于长度会导致异常发生,即使容量远远大于长度也没有用。oops!
索引就还是寻址访问。先找到那个数组在哪里,再去找那个下标的元素在哪里。
append追加
func append(s S, x ...T) //T是S元素类型
append()函数将0个或多个具有相同类型S的元素追加到切片s后面并且返回新的切片
如果append后,元素数量<=原有切片容量,即没有扩容,则返回的切片和追加前的切片地址相同。
如果append后,元素数量>原有切片容量,即扩容了,则返回的切片是一个新的切片,和追加前的切片地址不同。也就是新的切片和原来的切片没有任何关系,它们底层是俩不同的数组,即使修改了数据也
延申问题:append后底层数组变为新的之后,那么旧的底层数组,没有变量去引用它时,什么时候会被GC?
连续append好多次,期间发生了两次扩容,但最后只赋值一次,那么中间那次扩容产生的临时底层数组,什么时候被GC?,它是不是在第二次扩容后,就属于没有人去引用它的状态了?
—-即问题,什么时候会被GC,具体的场景举例。
往切片append一堆元素时,它是先计算是否要扩容吧
for range 之k,v
for k, v := range xx {
fmt.Printf("%p,%p\n",&k,&v)
}
//output
0xc00009e1f8,0xc00009e200
0xc00009e1f8,0xc00009e200
0xc00009e1f8,0xc00009e200
k,v声明一次,地址固定。
总结
–