前言

通过上一篇博文,我们学习了数组的使用,但是数组有一个致命的缺陷,那就是大小固定,这个特性并不能满足我们平时的开发需求,所以Go的切片由此诞生。

切片的内存分布是连续的,所以你可以把切片当做一个大小不固定的数组。

切片有三个字段的数据结构,这些数据结构包含Go 语言需要操作底层数组的元数据,这 3 个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)。后面会进一步讲解长度和容量的区别。

picture alt

创建和初始化切片

切片的创建有多种方式,下面我们一一来讲解。

使用make创建切片

1
2
3
4
//创建了一个长度和容量都为5的切片
slice1 := make[[]string,5]
//创建了一个长度5,容量为10的切片
slice2 := make[[]string,5,10]

需要说明的是,切片对应的底层数组的大小为指定的容量,这个一定要谨记,比如对于上面的例子,我们指定了 slice2 的容量为10,那么 slice2 对应的底层数组的大小就是10。 上面虽然创建的切片对应底层数组的大小为10,但是你不能访问索引值5以后的元素,比如,如果你运行以下代码,你就会发现:

1
2
3
4
5
6
func main() {
    slice := make([]int, 4, 10)
    fmt.Println(slice[2])
    fmt.Println(slice[6])
}

运行结果如下:

1
2
3
4
5
6
7
8
0
panic: runtime error: index out of range

goroutine 1 [running]:
main.main()
    E:/go-source/go-arr/main.go:19 +0x8d

Process finished with exit code 2

通过切片字面量来创建切片

1
2
3
func main() {
    slice := []int{1, 2, 4, 4}
}

创建数组和创建切片非常相似,如果你在[]指定了值,那么创建的是一个数组,反之就是一个切片

切面字面量也可以指定切片的大小和容量,如下所示:

1
2
3
func main() {
    slice := []int{99: 100}
}

上面创建的切片的大小和容量都为100,并且初始化第100个元素的值为100,只是在这种情况下,容量和长度是相等的。

创建空切片

1
2
3
4
func main() {
    slice1 := []int{}
    slice2 := make([]int, 0)
}

空切片在底层数组包含0 个元素,也没有分配任何存储空间。想表示空集合时空切片很有用。

picture alt

切片的使用

切片的使用和数组是一模一样的:

1
2
3
4
5
func main() {
    slice1 := []int{1,2,3,4}
    fmt.Println(slice1[1])
}

切片创建切片

切片之所以称为切片,是因为它只是对应底层数组的一部分,看如下所示代码:

1
2
3
4
func main() {
    slice := []int{10, 20, 30, 40, 50}
    newSlice := slice[1:3]
}

为了说明上面的代码,我们看下面的这张图:

picture alt

第一个切片slice 能够看到底层数组全部5 个元素的容量,不过之后的newSlice 就看不到。对于newSlice,底层数组的容量只有4 个元素。newSlice 无法访问到它所指向的底层数组的第一个元素之前的部分。所以,对newSlice 来说,之前的那些元素就是不存在的。

需要记住的是,现在两个切片共享同一个底层数组。如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到,运行下面的代码:

1
2
3
4
5
6
7
8
func main() {
    slice := []int{10, 20, 30, 40, 50}
    newSlice := slice[1:3]
    fmt.Println(newSlice)

    slice[1] = 200
    fmt.Println(newSlice[0], newSlice[1])
}

运行结果如下:

1
2
[20 30]
200 30

切片只能访问到其长度内的元素。试图访问超出其长度的元素将会导致语言运行时异常,比如对上面的 newSlice ,他只能访问索引为1和2的元素(不包括3),比如:

1
2
3
4
5
6
func main() {
    slice := []int{10, 20, 30, 40, 50}
    newSlice := slice[1:3]

    fmt.Println(newSlice[3])
}

运行代码,控制台会报错:

1
2
3
4
5
panic: runtime error: index out of range

goroutine 1 [running]:
main.main()
    E:/go-source/go-arr/main.go:20 +0x11

子切片的容量

我们知道切片可以再生出切片,那么子切片的容量为多大呢?我们来测试一下:

1
2
3
4
5
6
7
func main() {
    slice := make([]int, 2, 10)
    slice1 := slice[1:2]
    fmt.Println(cap(slice))
    fmt.Println(cap(slice1))
}

控制台打印结果为:

1
2
10
9

从结果我们可以推测,子切片的容量为底层数组的长度减去切片在底层数组的开始偏移量,比如在上面的例子中,slice1的偏移值为1,底层数组的大小为10,所以两者相减,得到结果9。

向切片中追加元素

go提供了 append 方法用于向切片中追加元素,如下所示:

1
2
3
4
5
6
7
8
func main() {
    slice := make([]int, 2, 10)
    slice1 := slice[1:2]
    slice2 := append(slice1, 1)
    slice2[0] = 10001
    fmt.Println(slice)
    fmt.Println(cap(slice2))
}

输出结果如下:

1
2
[0 10001]
9

此时slice,slice1,slice2共享底层数组,所以只要一个切片改变了某一个索引的值,会影响到所有的切片,还有一点值得注意,就是slice2的容量为9,记住这个值。

为了说明问题,我把例子改为如下所示代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func main() {
    slice := make([]int, 2, 10)
    slice1 := slice[1:2]
    slice2 := append(slice1, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2[0] = 10001
    fmt.Println(slice)
    fmt.Println(slice1)
    fmt.Println(cap(slice2))
}

此时我们再次打印结果,神奇的事情出现了:

1
2
3
[0 0]
[0]
18

虽然我们改变0位置的值,但是并没有影响到原来的slice和slice1,这是为啥呢?我们知道原始的slice2对应的底层数组的容量为9,经过我们一系列的append操作,原始的底层数组已经无法容纳更多的元素了,此时Go会分配另外一块内存,把原始切片从位置1开始的内存复制到新的内存地址中,也就是说现在的slice2切片对应的底层数组和slice切片对应的底层数组完全不是在同一个内存地址,所以当你此时更改slice2中的元素时,对slice已经来说,一点儿关系都没有。

另外根据上面的打印结果,你也应该猜到了,当切片容量不足的时候,Go会以原始切片容量的2倍建立新的切片,在我们的例子中2*9=18,就是这么粗暴。

如何创建子切片时指定容量

在前面的例子中,我们创建子切片的时候,没有指定子切片的容量,所以子切片的容量和我们上面讨论的计算子切片的容量方法相等,那么我们如何手动指定子切片的容量呢?

在这里我们借用《Go实战》中的一个例子:

1
2
3
4
5
func main() {
    source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
    slice := source[2:3:4]
    fmt.Println(cap(slice))
}

source[2:3:4]

如果你仔细看的话,上面的子切片的生成方式和普通的切片有所不同,[]里面有三个部分组成,第一个值表示新切片开始元素的索引位置,这个例子中是2。第二个值表示开始的索引位置(2)加上希望包括的元素的个数(1),2+1 的结果是3,所以第二个值就是3。为了设置容量,从索引位置2 开始,加上希望容量中包含的元素的个数(2),就得到了第三个值4。所以这个新的切片slice的长度为1,容量为2。还有一点大家一定要记住,你指定的容量不能比原先的容量大:

1
2
3
4
5
func main() {
    source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
    slice := source[2:3:10]
    fmt.Println(cap(slice))
}

运行结果如下,报错了,哈哈:

1
2
3
4
5
panic: runtime error: slice bounds out of range [::10] with capacity 5

goroutine 1 [running]:
main.main()
    E:/learn-go/slice/main.go:7 +0x1d

迭代切片

关于如何迭代切片,我们可以使用range配置来使用,如下:

1
2
3
4
5
6
7
func main() {
    slice:=[]int{1,2,4,6}
    for _, value:=range slice{
        fmt.Println(value)
    }
}

关于迭代切片,大家有一点需要注意,就以上面的例子为例,value只是slice中元素的副本,为啥呢?我们来验证这一点:

1
2
3
4
5
6
func main() {
    slice:=[]int{1,2,4,6}
    for index, value:=range slice{
        fmt.Printf("value[%d],indexAddr:[%X],valueAddr:[%X],sliceAddr:[%X]\n",value,&index,&value,&slice[index])
    }
}

控制台打印结果如下:

1
2
3
4
value[1],indexAddr:[C00000A0B8],valueAddr:[C00000A0D0],sliceAddr:[C000010380]
value[2],indexAddr:[C00000A0B8],valueAddr:[C00000A0D0],sliceAddr:[C000010388]
value[4],indexAddr:[C00000A0B8],valueAddr:[C00000A0D0],sliceAddr:[C000010390]
value[6],indexAddr:[C00000A0B8],valueAddr:[C00000A0D0],sliceAddr:[C000010398]

从上面的结果可以看到index和value的地址始终是不变的,所以它们始终是同一个变量,只是变量引用地址的内容发生了变化,从而验证迭代的时候,只能是切片元素的副本,最后看看sliceAddr代表的地址相隔8个字节,因为在64位系统上,每一个int类型的大小为8个字节。

函数间传递切片

函数间传递切片,也是以值的方式传递的,但是你还记得这篇博文开头给出的切片的布局么?

picture alt

切片由三个部分组成,包括指向底层数组的指针,当前切片的长度,当前切片的容量,所以切片本身并不大,我们来测试一个切片的大小:

1
2
3
4
func main() {
    slice:=[]int{1,2,4,6}
    fmt.Println(unsafe.Sizeof(slice))
}

测试结果为:

1
24

也就是这个slice切片的大小为24字节,所以当切片作为参数传递的时候,几乎没有性能开销,还有很重要的一点,参数生成的副本的地址指针和原始切片的地址指针是一样的,因此,如果你在函数里面修改了切片,那么会影响到原始的切片,我们来验证这点:

1
2
3
4
5
6
func main() {
    slice:=[]int{1,2,4,6}
    handleSlice(slice)
    fmt.Println(slice)
}

打印结果:

1
[100 2 4 6]

函数间传递切片:如何避免引用传值

采用深拷贝方案: 使用 copy 方法得到新的切片后再传值。

copy 复制会比等号复制慢。但是 copy 复制为值复制,改变原切片的值不会影响新切片。而等号复制为指针复制,改变原切片或新切片都会对另一个产生影响。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 切片深拷贝示例
func test() {
    a := make([]int, 10)
    for i := 0; i < 10; i++ {
        a[i] = i
    }
    b := a[1:4]
    b[1] = 22 // 浅拷贝:值变化会影响到切片a

    var c = make([]int, 3)
    copy(c, b)
    c[1] = 222 // 深拷贝: 值变化不会影响到切片a、b

    fmt.Println(a)
    fmt.Println(b)
    fmt.Println(c)
}

移除切片元素

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
func main()  {
    userFieldNames := []string{"`id`", "`name`", "`v`", "`create_time`", "`update_time`"}
    out := Remove(userFieldNames, "`id`", "`create_time`", "`update_time`")
    fmt.Printf("%+v\n", out)
    fmt.Printf("%+v\n", userFieldNames)

    userFieldNames2 := []string{"`id`", "`name`", "`v`", "`create_time`", "`update_time`"}
    out2 := Remove2(userFieldNames2, "`id`", "`create_time`", "`update_time`")
    fmt.Printf("%+v\n", out2)
    fmt.Printf("%+v\n", userFieldNames2)
}

// 方案一
func Remove(strings []string, strs ...string) []string {
    out := make([]string, len(strings))
    copy(out, strings)

    for _, str := range strs {
        var n int
        for _, v := range out {
            if v != str {
                out[n] = v
                n++
            }
        }
        out = out[:n]
    }

    return out
}

// 方案二
func Remove2(strings []string, strs ...string) []string {
    out := make([]string, 0)

    var flag bool
    for _, str := range strings {
        flag = true
        for _, v := range strs {
            if v == str {
                flag = false
                break
            }
        }
        if flag {
            out = append(out, str)
        }
    }

    return out
}

指定切片容量

在尽可能的情况下,在使用 make() 初始化切片时提供容量信息,特别是在追加切片时,优点是可以减少内存分配。

1
make([]T, length, capacity)

与 map 不同,slice capacity 不是一个提示:编译器将为提供给 make() 的 slice 的容量分配足够的内存,这意味着后续的 append() 操作将导致零分配(直到 slice 的长度与容量匹配,在此之后,任何 append 都可能调整大小以容纳其他元素)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const size = 1000000

// Bad
for n := 0; n < b.N; n++ {
 data := make([]int, 0)
   for k := 0; k < size; k++ {
     data = append(data, k)
  }
}

BenchmarkBad-4    219    5202179 ns/op

// Good
for n := 0; n < b.N; n++ {
 data := make([]int, 0, size)
   for k := 0; k < size; k++ {
     data = append(data, k)
  }
}

BenchmarkGood-4   706    1528934 ns/op

转载声明

作者:Dennis_Ritchie

链接:https://learnku.com/articles/38316#reply150710

来源:LearnKu

著作权归作者所有,任何形式的转载都请联系作者