go语言的数组与切片

go语言的数组与切片

如果有编程基础的话,提到数组我们肯定不会陌生,所谓数组,就是有数的元素序列,我们可以认为数组是有限的相同类型数据的集合。

数组长度是固定的,所以这会带来很多局限性。

比如说只接受相同类型的元素,长度固定等等。

那么切片的出现,则为golang解决了以上数组所带来的不便。

切片(slice)是一个引用类型,它是拥有相同类型的元素并且长度可变的序列。

基于数组类型做的一层封装,支持扩容。

切片的内部结构包含地址、长度、容量。它主要用于服务一块数据的集合。

下面我们来看一看数组的声明:

var arr1 [5]int 									// 声明一个长度为5的数组
var arr2 = [5]int{1, 2, 3, 4, 5} 	//声明一个数组,并初始化

然后再看一看切片的声明:

var slice1 []string                 //声明一个字符串切片
var intSlice []int                  //定义一个整型切片
var boolSlice = []bool{true, false} //声明一个布尔切片并初始化

数组的遍历

第一种,for循环

var arr1 = [5]int{1,2,3,4,5}
for i := 0; i < len(arr1); i++ {
	fmt.Println(arr1[i])
}

第二种:for-range

var arr1 = [5]int{1,2,3,4,5}	
for i, v := range arr1 {
	fmt.Printf("第%d个元素,值为:%d\n", i, v)
}

数组类型

数组的类型实际上是值类型,所以可以通过new() 来创建数组:

var arr1 = new([5]int)

通过new创建的数组和var arr2 [5]int的区别是什么呢?

arr1的类型是*[5]int, 而arr2的类型则是[5]int

	var arr1 = new([5]int)
	var arr2 = [5]int{1, 2, 3, 4, 5}
	fmt.Printf("arr1 type : %T, arr2 type : %T\n", arr1, arr2)

输出如下:

arr1 type : *[5]int, arr2 type : [5]int

这样的结果是什么呢?就是当把一个数组赋值给另一个数组后,需要再做一次数组内存的拷贝操作。

例如:

arr2 := *arr1
arr2[2] = 100

切片

切片的底层就是数组,所以我们可以基于数组来定义切片。

举个例子:

func main() {
	var ar1 = [5]int{1, 2, 3, 4, 5}
	ar2 := ar1[:3]
	fmt.Printf("ar1 type : %T, ar2 type : %T", ar1, ar2)
}

输出如下:

ar1 type : [5]int, ar2 type : []int

基于切片再得到切片

func main() {
	var ar1 = [5]int{1, 2, 3, 4, 5}
	ar2 := ar1[:3]
	fmt.Println(ar2)
	ar3 := ar2[0:4]
	fmt.Println(ar3)
}

输出:

[1 2 3]
[1 2 3 4]

使用make构造切片

make函数是内置的,格式如下:

make([]T,size, cap)

T就是创建切片的类型,size是切片中元素的数量,cap是切片的容量。

举个例子:

func main() {
	a := make([]int, 2, 10)
	a[1] = 10
	fmt.Println(a) //[0,10]
	fmt.Println(len(a)) //元素数量2
	fmt.Println(cap(a)) //容量是10
}

但注意!虽然切片a的容量是10,但是这并不意味着我们可以随意的给切片a赋值。

比如说:我定义了一个切片 a := make([]int, 2, 10)a[1]= 10 但是如果这时我让a[2] = 11则会报错。

panic: runtime error: index out of range [2] with length 2

使用append为切片追加数据

所以这里涉及到如何为切片添加元素,我们可以用系统自带的append函数,来为切片添加元素。

每个切片都会指向一个底层数组,这个数组会容纳一定数量的元素。

当底层数组不能容纳新增的元素时,就会发生扩容,那么这时候切片指向的底层数组就会更换。

下面我们来看一个例子:

func main() {
	a := make([]int, 2, 10)
	a[0] = 1						//第一个元素为1
	a[1] = 10						//第二个元素为10
	a = append(a, 11) 	//此时我们追加一个新的元素,11
	fmt.Println(a) 			//[1,10,11]
}

使用appen()函数即可让切片添加新的元素。

那么问题来了,如果我们不停地追加新的元素,切片指向的数组什么时候会改变呢?下面再看一段代码:

func main() {
	a := make([]int, 2, 5)
	a[0] = 1
	a[1] = 10
	fmt.Printf("切片a循环前的内存地址:%p\n", a)
	for i := 0; i < 10; i++ {
		a = append(a, 100+i)
		fmt.Printf("追加完毕,循环次数:%d, 切片a此时的内存地址:%p, 切片a的容量:%d\n", i, a, cap(a))
	}
	fmt.Println(a)
}

输出结果:

切片a循环前的内存地址:0xc0000a8030
追加完毕,循环次数:0, 切片a此时的内存地址:0xc0000a8030, 切片a的容量:5
追加完毕,循环次数:1, 切片a此时的内存地址:0xc0000a8030, 切片a的容量:5
追加完毕,循环次数:2, 切片a此时的内存地址:0xc0000a8030, 切片a的容量:5
追加完毕,循环次数:3, 切片a此时的内存地址:0xc0000ac000, 切片a的容量:10
追加完毕,循环次数:4, 切片a此时的内存地址:0xc0000ac000, 切片a的容量:10
追加完毕,循环次数:5, 切片a此时的内存地址:0xc0000ac000, 切片a的容量:10
追加完毕,循环次数:6, 切片a此时的内存地址:0xc0000ac000, 切片a的容量:10
追加完毕,循环次数:7, 切片a此时的内存地址:0xc0000ac000, 切片a的容量:10
追加完毕,循环次数:8, 切片a此时的内存地址:0xc0000ae000, 切片a的容量:20
追加完毕,循环次数:9, 切片a此时的内存地址:0xc0000ae000, 切片a的容量:20
[1 10 100 101 102 103 104 105 106 107 108 109]
切片a此时的内存地址:0xc0000ae000

我们在切片初始化的时候指定容量为5,由上面运行结果可以知道,在i循环到2后,切片本身的元素数量已经达到了5,也就是说,下一次再添加元素的时候就会发生扩容,然后底层指向的数组会改变。

所以,在循环到3点时候,a的容量由5变为10,此时内存地址也发生改变。每次扩容都是上一次的2倍大。

追加多个元素

append()函数会将元素添加到切片最后,并返回该切片,同时也支持追加多个元素。

下面代码示例:

func main() {
	a := make([]int, 2, 5)
	a = append(a, 1, 2, 3, 4, 5) //追加多个元素
	b := []int{6, 6, 6}          //我们再定义一个切片
	a = append(a, b...)          //追加切片
	fmt.Print(a)
}

输出如下:

[0 0 1 2 3 4 5 6 6 6]

由此我们可以看到,前两个元素0,0是切片a本身定义的元素,由于初始化没有赋值,所以默认是0,接着是1,2,3,4,5,最后三个6则是切片b追加到最后。

切片的扩容策略

可以通过查看$GOROOT/src/runtime/slice.go源码,其中有个函数叫做growslice

那么在这个函数中,源码上面就写了很多的注释,如下所示:

// growslice handles slice growth during append.

// It is passed the slice element type, the old slice, and the desired new minimum capacity,

// and it returns a new slice with at least that capacity, with the old data

// copied into it.

// The new slice's length is set to the old slice's length,

// NOT to the new requested capacity.

// This is for codegen convenience. The old slice's length is used immediately

// to calculate where to write new values during an append.

// TODO: When the old backend is gone, reconsider this decision.

// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.

那它究竟是怎么扩容的呢?我们往下看:

newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

由代码我们可以得知,每次扩容时的条件是什么,并不是每次扩容都会扩大到2倍,如果旧切片长度小于1024,那最终容量就是old cap的两倍,否则就会增加原来的四分之一,直到newcap >= cap.

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的处理方式就不一样。

使用copy()函数

切片是引用类型,所以如果我们定义了一个切片a,然后切片 b = a,那此时,a和b都是指向了同一块内存地址,修改a的时候也会修改b。

下面我们来看一段代码:

func main() {
	a := []int{1, 2, 3, 4}
	b := a
	fmt.Printf("a : %d, b: %d\n", a, b)
	a[2] = 999
	fmt.Printf("a : %d, b: %d\n", a, b)
}

a : [1 2 3 4], b: [1 2 3 4] 修改之后 a : [1 2 999 4], b: [1 2 999 4]

那如何避免这种情况发生呢?这时候我们就要用到go内置的copy()函数了。

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:

copy(destSlice, srcSlice []T)
func main() {
	a := []int{1, 2, 3, 4}
	b := make([]int, 4, 4)
	fmt.Printf("修改之前 a : %d, b: %d\n", a, b)
	copy(b, a)
	a[2] = 999
	fmt.Printf("修改之后 a : %d, b: %d\n", a, b)
}

输出如下:

修改之前 a : [1 2 3 4], b: [0 0 0 0]

修改之后 a : [1 2 999 4], b: [1 2 3 4]

切片删除元素

go语言并没有删除切片元素的专有方法,但是可以通过索引来删除切片中的元素。

func main() {
	// 从切片中删除元素
	a := []int{30, 31, 32, 33, 34, 35, 36, 37}
	// 要删除索引为1,2,3的元素
	a = append(a[:1], a[4:]...)
	fmt.Println(a) 
}

[30 34 35 36 37]

去掉最后一个元素:

slice1 = slice1[:len(slice1)-1]

posted on 2020-03-05 21:59  长方形  阅读(229)  评论(0编辑  收藏  举报

导航