Go语言切片入门

数组 长度是固定的,而 切片slice )是一个数组元素的弹性视图,长度可以是动态的。实际上,切片比数组更为常用。

类型 []T 就是一个由类型 T 元素组成的切片。

切片通过两个索引下标定义,一个表示 下边界low bound ),一个表示 上边界high bound ),以冒号( : )分隔:

1
a[low : high]

这表示一个 半开半闭 区间,包括第一个元素,但不包括最后一个。

以下表达式创建一个包含元素 1 到元素 3 的切片(不包括元素 4 ):

1
a[1:4]

完整例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
    primes := [6]int{2, 3, 5, 7, 11, 13}

    var s []int = primes[1:4]
    fmt.Println(s)
}

数组引用

切片实际上并 不存储任何数据 ,只是用来描述关联数组的一部分。因此,修改切片元素等价于修改对应的数组元素,其他共用该数组的切片也对此可见。换句话讲,切片就像是数组的引用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
    names := [4]string{
        "John",
        "Paul",
        "George",
        "Ringo",
    }
    fmt.Println(names)

    a := names[0:2]
    b := names[1:3]
    fmt.Println(a, b)

    b[0] = "XXX"
    fmt.Println(a, b)
    fmt.Println(names)
}

下标默认值

定义切片,可以忽略上边界或者下边界,而使用 默认值 。对于下边界,默认值为 0 ;下边界,默认值则是 切片长度

因此,对于数组:

1
var a [10]int

以下切片表达式均是等价的:

1
2
3
4
a[0:10]
a[:10]
a[0:]
a[:]

最后一起来看一个完整的例子,猜猜它输出什么?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}

    s = s[1:4]
    fmt.Println(s)

    s = s[:2]
    fmt.Println(s)

    s = s[1:]
    fmt.Println(s)
}

长度和容量

切片有两个重要属性, 长度length )和 容量capacity )。

切片的长度等于切片包含的元素个数。

切片的容量等于底下数组的元素个数,从切片第一个元素算起。

对于切片 s ,长度和容量分别可以通过表达式 len(s) 以及 cap(s) 获得。

通过 重切re-slicing ),你可以扩张一个切片的长度,只要容量足够。你可以试试修改下面这个例子,将切片扩张到超出容量,看看会发生什么事情:

 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
package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    // Slice the slice to give it zero length.
    s = s[:0]
    printSlice(s)

    // Extend its length.
    s = s[:4]
    printSlice(s)

    // Drop its first two values.
    s = s[2:]
    printSlice(s)
}


func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

空切片

切片的 零值nil ,即 空切片 。 空切片长度和容量均为 0 ,当然也不需要底层数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func main() {
    var s []int
    fmt.Println(s, len(s), cap(s))
    if s == nil {
        fmt.Println("nil!")
    }
}

make

切片可以由内置函数 make 来创建,相当于你可以创建动态长度的数组。

make 函数分配一个由 零值 填充的数组,并返回一个引用该数组的切片:

1
a := make([]int, 5) // len(a)=5

要指定容量,可以通过 make 函数第三个参数指定:

1
2
3
4
b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)]  // len(b)=5, cap(b)=5
b = b[1:]       // len(b)=4, cap(b)=4

完整例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
    a := make([]int, 5)
    printSlice("a", a)

    b := make([]int, 0, 5)
    printSlice("b", b)

    c := b[:2]
    printSlice("c", c)

    d := c[2:5]
    printSlice("d", d)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x)
}

切片的切片

切片可以包含任何类型,当然包括其他切片。

 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
package main

import (
    "fmt"
    "strings"
)

func main() {
    // Create a tic-tac-toe board.
    board := [][]string{
        []string{"-", "_", "_"},
        []string{"-", "_", "_"},
        []string{"-", "_", "_"},
    }

    // The players take turns.
    board[0][0] = "X"
    board[2][2] = "O"
    board[1][2] = "X"
    board[1][0] = "O"
    board[0][2] = "X"

    for i := 0; i < len(board); i++ {
        fmt.Printf("%s\n", strings.Join(board[i], " "))
    }
}

元素追加

向切片追加元素是一个很常用的操作,为此 Go 提供了一个 内置函数 append

1
func append(s []T, vs ...T) []T

append 函数第一个参数 s 是一个类型为 T 的切片,其余参数均为追加至 sT 元素。

append 函数返回一个新切片,包含原切片以及所有追加元素。

如果底层数组太小, append 函数会分配一个更大的数组,新切片则指向新数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

func main() {
    var s []int
    printSlice(s)

    // append works on nil slices.
    s = append(s, 0)
    printSlice(s)

    // The slice grows as needed.
    s = append(s, 1)
    printSlice(s)

    // We can add more than one elements at a time.
    s = append(s, 2, 3, 4)
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

遍历

配合 range 关键字, for 循环可对切片进行 遍历 。 下节,我们将看到,这种做法也适用于 映射表map )。

每次迭代都返回两个值,第一个是 下标index ),第二个是与该下标对应的 元素拷贝

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
    for i, v := range pow {
        fmt.Printf("2**%d=%d\n", i, v)
    }
}

如果无须下标,可以直接赋值给下划线 _ 。 如果只要下标,则不写后半部分即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

func main() {
    pow := make([]int, 10)

    for i := range pow {
        pow[i] = 1 << uint(i)   // == 2**i
    }

    for _, value := range pow {
        fmt.Printf("%d\n", value)
    }
}

练习

1. 图片像素矩阵

尝试实现一个函数 Pic :返回一个长度为 dy 的切片,切片元素为长度为 dx8 位无符号整数切片。 这个切片,其实就是一个二维阵列,可以用来表示一张图片的像素。

当你运行这个程序时,它将展示一张图片,每个数值被解释为图片像素的 灰度值grayscale )。

代码框架如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "golang.org/x/tour/pic"

func Pic(dx, dy int) [][]uint8 {
}

func main() {
    pic.Show(Pic)
}

图片长啥样取决于你返回的切片。试试以下以下公式来生成切片,结果将非常有趣:

  • $\frac{x+y}{2}$

  • $x \times y$

  • $x^y$

这三个公式生成的图片分别是:

答案

相信你已经写出自己的程序,看到各种有趣的图片了。

如果练习过程中遇到什么问题,可以参考下面这个例子并尝试解决:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "golang.org/x/tour/pic"

func Pic(dx, dy int) [][]uint8 {
    s := make([][]uint8, dy)
    for y := 0; y < dy; y++ {
        sx := make([]uint8, dx)
        for x := 0; x < dx; x++ {
            sx[x] = uint8((x+y)/2)
            //sx[x] = uint8(x*y)
            //sx[x] = uint8(x^y)
        }
        s[y] = sx
    }
    return s
}

func main() {
    pic.Show(Pic)
}

【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注: