数组 长度是固定的,而 切片( slice )是一个数组元素的弹性视图,长度可以是动态的。实际上,切片比数组更为常用。
类型 []T
就是一个由类型 T
元素组成的切片。
切片通过两个索引下标定义,一个表示 下边界( low bound ),一个表示 上边界( high bound ),以冒号( :
)分隔:
这表示一个 半开半闭 区间,包括第一个元素,但不包括最后一个。
以下表达式创建一个包含元素 1 到元素 3 的切片(不包括元素 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
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
的切片,其余参数均为追加至 s 的 T
元素。
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 的切片,切片元素为长度为 dx 的 8 位无符号整数切片。 这个切片,其实就是一个二维阵列,可以用来表示一张图片的像素。
当你运行这个程序时,它将展示一张图片,每个数值被解释为图片像素的 灰度值( 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语言】系列文章首发于公众号【小菜学编程】,敬请关注: