Golang数据类型之切片

1、切片介绍

Gosliceslicesliceappendslicesliceslice
// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 数组指针
    len   int // 长度 
    cap   int // 容量
}
slice
sliceslice
35int
s := make([]int, 3, 4)
fmt.Println(a, len(s), cap(s)) // [0 0 0] 3 5

2、声明和初始化

Go

是否提前知道切片所需的容量通常会决定如何创建切片

2.1 make创建

// 创建一个整型切片, 其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)

// 我们也可以省略容量, 默认长度==容量
// 创建一个整型切片 其长度和容量都是 5 个元素
slice := make([]int, 5)

// 但是长度不能小于容量, 否则编译器过不了
// a := make([]int, 5, 3)

2.2 字面量创建

// 这种方法和创建数组类似,只是不需要指定[]运算符里的值。初始的长度和容量会基于初始化时提供的元素的个数确定
slice := []int{1,2,3}

// 和数组一样也可以通过指定索引初始化, 比如index 4 值为100
slice := []int{3: 100}

2.3 创建数组和切片的区别

[]
a := [3]int{1,2,3}
b := []int{1,2,3}

虽然他们声明时只要这一点点区别,但是他们的数据结构区差别却很大,一个是引用类型一个是值类型

2.4 创建切片的本质

makemakeslice
// 这里一波操作过后返回的是slice的pointer
func makeslice(et *_type, len, cap int) unsafe.Pointer {}

3、切片访问

[]
s := []int{1,2,3}
s[0]

// 但是不能越界访问, 比如
s[3] // panic: runtime error: index out of range [3] with length 3
len
cap

4、nil和空切片

nil
var s []int
var s1 []int
fmt.Printf("%pn", s1)  // 0x0
make
s := make([]int,0)
// unsafe.Pointer ——> *slice
s2 := make([]int, 0)
fmt.Printf("%pn", s2)  // 0x126c9
nil
var s []int
s[0] = 1 // panic: runtime error: index out of range [0] with length 0

5、切片中添加元素

append
s := make([]int, 0, 4)
s = append(s, 10, 20, 30, 40)

现在底层数组已经满了,再往里面追加元素会如何?

s = append(s,50)
append()102410241.2525%

因此扩容对于切片来说是一个比较消耗成本的事情,会开辟新的内存空间

gc
s1 := make([]int, 0, 4)
s1 = append(s1, 10, 20, 30, 40) // 10, 20, 30, 40
fmt.Println(s1, len(s1), cap(s1))  // [10 20 30 40] 4 4
s1 = append(s1, 50)
fmt.Println(s1, len(s1), cap(s1))  // [10 20 30 40 50] 5 8

6、通过切片创建切片

切片之所以被称为切片,是因为创建一个新的切片,也就是把底层数组切出一部分。通过切片创建新切片的语法如下, 详情请参考: 切片的语法

slice[low : high]
slice[low : high : max]
slichigh-lowmax-low

比如

s1 := []int{1, 2, 3, 4}
s2 := s1[2:4:4]  // [index2, index4) 左闭右开区间, 容量 4-2
fmt.Println(s2, len(s2), cap(s2)) // [3 4] 2 2
high == maxmax
s3 := s1[2:4]

再次基础上还要几种省略写法:

 index 0lenlen
slice[i:]  // 从 i 切到最尾部
slice[:j]  // 从最开头切到 j(不包含 j)
slice[:]   // 从头切到尾,等价于复制整个 slice

注意: 通过切片创建出来的切片是共享底层数据结构的(数组)

共享底层数组会导致相互影响, 比如修改原切片会影响多所有复制出来的切片

s1 := []int{10, 20, 30, 40}
s2 := s1[1:3]
fmt.Println(s2, len(s2), cap(s2))

fmt.Println(s1[1], s2[0])
s1[1] = 200
fmt.Println(s1[1], s2[0])

有扩容的原理也可以知道,当扩容后,就不共享底层数组了,比如:

s1 := []int{10, 20, 30, 40}
s2 := s1[1:3:3]
fmt.Println(s2, len(s2), cap(s2))

fmt.Println(s1[1], s2[0])
s2 = append(s2, 30)   // s2 扩容
s1[1] = 200           // 修改s1
fmt.Println(s1[1], s2[0]) // s1修改并不会影响s2

因此,一般不要修改切片,如果要修改请使用后面的深拷贝复制一个全新的切片

7、切片遍历

Gorangefor
func TestSliceAppend1(t *testing.T) {
	s := make([]int, 0, 4)
	s = append(s, 10, 20, 30, 40)
	for i, v := range s {
		fmt.Println(i, v)
	}
	/*
	0 10
	1 20
	2 30
	3 40
	 */
}

这种方式底层的实现,也是拷贝一份切片提供给循环使用,因此同样会带来开销

rangerange

8、切片拷贝

不能像数组一样直接使用赋值语句来拷贝一个切片,因为数组是值,而切片是指针, 真正的数据维护在底层数组里面

a1 := [2]{1,2}
a2 := a1    // 值拷贝, a1, a2 互不影响

s1 := []{1, 2}
s2 := s1   // 指针拷贝 s1, s2 指向同一*slice结构体, 就是一个东西,等于没拷贝
Gocopy()
func copy(dst, src []Type) int
srcdstsrcdstsrcdstsrc
s1 := []int{10, 20, 30, 40}
s2 := make([]int, 5)
num := copy(s2, s1)   // 这时候s1 和 s2 就是2个切片,包含底层数据, 互不影响
fmt.Println(num)  // 4
fmt.Println(s1, s2)  // [10 20 30 40] [10 20 30 40 0]
s1[0] = 100
fmt.Println(s1[0], s2[0])  // 100 10

9、切片作为函数参数

函数在调用传参时,都是值拷贝

切片的本质是指针,如果是切片作为函数的参数调用,则拷贝的是指针的地址

因此切片作为函数的参数时,最大的好处是传递效率高

因此切片的用法远多于数组,数组用来定义底层的数据结构

func TestSliceMain2(t *testing.T) {
	s1 := make([]int, 0, 4)
	s1 = append(s1, 10, 20, 30, 40) // 10, 20, 30, 40
	fmt.Println(Sum1(s1))  // 100
}

func Sum1(args []int) int {
	sum := 0
	for _, v := range args {
		sum += v
	}
	return sum
}