之前讲到了Go的基础数据类型,除此之外,Go还支持很多复合类型的数据结构。
数组(array)
数组就是指一系列同一类型数据 的集合。
Go语言中,类型 [n]T 表示拥有 n 个 T 类型的值的数组。如:
var a [3]int
表示变量 a 声明为拥有有 3个整数的数组。声明语法上与java的区别是[]是写在类型前面的。
当然,也可以让编译器统计数组字面值中元素的数目:
a := [...]int{1, 2,3}
这两种写法, a 都是对应长度为3的int数组。
数组的长度是其类型的一部分,因此数组不能改变大小。 可以用内置函数len()取得数组长度。
package main
import "fmt"
func main() {
var a [2]string //定义一个长度为2的字符串数组
a[0] = "Hello" //下标1赋值为Hello
a[1] = "World"
fmt.Println(a[0], a[1]) //按下标取值
fmt.Println(a) //打印数组
primes := [...]int{2, 3, 5, 7, 11, 13} //定义一个长度为6的int数组,并初始化
for i := 0; i < len(primes); i++ {
fmt.Println(primes[i])
}
}
从上面可以看出,数组访问和赋值可以用下标的方式,下标从0开始,这点和其他大部分编程语言一致。
Go的数组也支持多维数组。定义方式如下:
var arrayName [ x ][ y ] variable_type
package main
import "fmt"
func main() {
a := [3][4]int{{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}}
fmt.Println(a)
for i := 0; i < 3; i++ {
for j := 0; j < 4; j++ {
fmt.Printf("a[%d][%d] = %d\n", i, j, a[i][j])
}
}
}
上面展示了二维数组的定义初始化和取值。
特别需要说明的一点是,初始化时的最后两个引号不能分行写,否则编译会不过,Go编译器不知为何做这种限制。如下写法是错误的。
a := [3][4]int{{0, 1, 2, 3}, {4, 5, 6, 7},
{8, 9, 10, 11}
}
切片(slice)
前面说过,数组的长度是不可变的,这在操作上带来了很大不便,但是Go给出了很好的解决方案,就是切片(slice)。
Go的切片是对数组的抽象。Go数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
定义
可以通过声明一个未指定大小的数组来定义切片,类型 []T 表示一个元素类型为 T 的切片。从这个角度来说,切片可以视为动态大小的数组。
但是,切片并不存储任何数据, 它只是描述了底层数组中的一段。更改切片的元素会修改其底层数组中对应的元素。与它共享底层数组的切片都会观测到这些修改。
var s []type
除此之外,可以使用make()函数来创建切片:
var slice1 []type = make([]type, length ,capacity)
其中type是切片的类型,length是切片的初始化长度,capacity是可选参数,指切片容量。
make 函数会分配一个元素为零值的数组并返回一个引用了它的切片。
a := make([]int, 5) // len(a)=5, cap(a)=5
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
len()函数可以返回切片的长度,cap()函数返回切片的容量。
初始化
切片初始化是很灵活的,方法也有很多种。
1、直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
s :=[] int {1,2,3 }
2、初始化切片s,是数组arr的引用
s := arr[:]
3、将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片,arr可以是数组也可以是一个切片,这是定义的切片就是切片的切片。
s := arr[startIndex:endIndex]
4、缺省endIndex时将表示一直到arr的最后一个元素
s := arr[startIndex:]
5、缺省startIndex时将表示从arr的第一个元素开始
s := arr[:endIndex]
6、通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片
s :=make([]int,len,cap)
package main
import "fmt"
func main() {
//1、直接初始化切片
var s1 = []int{1, 2, 3, 4, 5}
s11 := []int{1, 2, 3, 4, 5}
//2、初始化切片s,是数组arr的引用
var arr = []int{1, 2, 3, 4, 5}
s2 := arr[:]
//3、从下标startIndex到endIndex-1 下的元素创建为一个新的切片
s3 := arr[1:3]
s31 := s1[1:3]
//4、缺省endIndex时将表示一直到arr的最后一个元素
s4 := arr[3:]
s41 := s1[3:]
//5、缺省startIndex时将表示从arr的第一个元素开始
s5 := arr[:4]
s51 := s1[:4]
//6、通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片
s6 := make([]string, 4, 50)
s6[0] = "a"
s6[1] = "b"
s6[2] = "c"
s6[3] = "d"
s61 := make([]string, 4)
fmt.Println("s1:", s1)
fmt.Println("s11:", s11)
fmt.Println("s2:", s2)
fmt.Println("s3:", s3)
fmt.Println("s31:", s31)
fmt.Println("s4:", s4)
fmt.Println("s41:", s41)
fmt.Println("s5:", s5)
fmt.Println("s51:", s51)
fmt.Println("s6:", s6)
fmt.Println("len(s6):", len(s6))
fmt.Println("cap(s6)", cap(s6))
fmt.Println("s61:", s61)
fmt.Println("len(s61):", len(s61))
fmt.Println("cap(s61)", cap(s61))
}
输出为:
空(nil)切片
上面是对于切面的初始化,在一个切片在未初始化之前默认为 nil,长度为 0且没有底层数组。。nil是Go的一个关键字。
package main
import "fmt"
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
可以看出,切片s长度和容量都是0,值是nil。即切片是空的。
切片的内幕
一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。
切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素会影响到原始切片的对应元素。
package main
import "fmt"
func main() {
s1 := [...]int{1, 2, 3, 4, 5}
s2 := s1[2:]
fmt.Println("修改前s1:", s1)
fmt.Println("修改前s2:", s2)
s2[2] = 10
fmt.Println("修改后s2:", s2)
fmt.Println("修改后s1:", s1)
}
切片的增长
前面说过,切片可以看成是动态数组,所以他的长度是可变的。只要切片不超出底层数组的限制,它的长度就是可变的,只需将它赋予其自身的切片即可。
package main
import "fmt"
func main() {
s := make([]int, 5, 10)
fmt.Println("修改后s:", len(s))
s = s[:cap(s)]
fmt.Println("修改后s:", len(s))
}
上面就是把切片s的长度修改成他的最大长度。如果超过他的最大长度,则会报错–“panic: runtime error: slice bounds out of range”。
s = s[:12]
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。整个技术是一些支持动态数组语言的常见实现。
package main
import "fmt"
func main() {
s := make([]int, 5, 10)
t := make([]int, len(s), cap(s)*2) // 扩大s的容量
for i := range s {
s[i] = i
t[i] = s[i]
}
fmt.Println("修改前s:", s)
fmt.Println("修改前len(s):", len(s))
fmt.Println("修改前cap(s):", cap(s))
s = t
fmt.Println("修改后s:", s)
fmt.Println("修改后len(s):", len(s))
fmt.Println("修改后cap(s):", cap(s))
}
上面把一个切片的容量扩大了2倍。
对于循环中复制的操作Go提供了可copy内置函数。copy函数可以将源切片的元素复制到目的切片。copy函数支持不同长度的切片之间的复制(它只复制较短切片的长度个元素)。此外, copy 函数可以正确处理源和目的切片有重叠的情况。
使用copy函数可以直接替换上面的for循环。
copy(t, s)
除此之外,Go还提供了一个为切片追加新的元素操作的方法– append()。
append 的第一个参数 s 是一个元素类型为 T 的切片, 其余类型为 T 的值将会追加到该切片的末尾。append 的结果是一个包含原切片所有元素加上新添加元素的切片。
package main
import "fmt"
func main() {
var s []int
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
s = append(s, 0)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
s = append(s, 1)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
s = append(s, 2, 3, 4)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
s2 := []int{5, 6, 7}
s = append(s, s2...)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
上面的程序,先创建了一个nil切片,然后不断往里面添加新的数据。s = append(s, s2…)这个写法是把后面的s2切片打散传给append,相当于是s = append(s, 5, 6, 7),这也是Go支持的语法。
可以看出切片的长度和容量是不断增加的。通过我的观察,append增加容量是按照如果容量不够把之前切片的容量乘以2,如果乘以2还不够就之前容量+1乘以2来递增的。不过这个以后还得看看源码确认下,今天一直没找到在哪。
通过到目前的了解,切片应该在Go中使用的比数组要多。
有疑问加站长微信联系(非本文作者)