Golang Slice 数组的区别,以及指针,值 传递,append, for range 常见题一文搞懂

  • Post author:
  • Post category:golang




常见题目

比如面试中常问的问题

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 (不需要说明长度)

切片初始化方式,三种:

  1. 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声明一次,地址固定。



总结


在这里插入图片描述



版权声明:本文为weixin_43973689原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。