复合数据类型主要有数组,slice,map和结构体
数组
数组是具有固定长度且拥有零个或者多个相同数据类型元素的序列。由于数组的长度固定,所以在Go里面很少直接使用。slice的长度可以增长和缩短,在很多场合下使用更多,但是在了解slice之前先要理解数组的使用
数组定义
数组的声明
var balance [10] float32
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0} // 初始化
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0} // 忽略[]中的数字不设置数组大小,Go语言会根据元素的个数来设置数组的大小
balance[4] = 50.0 // 访问数组元素
var threedim [5][10][4]int // 多维数组定义
// 多维数组初始化
a = [3][4]int{
{0, 1, 2, 3} , /* 第一行索引为 0 */
{4, 5, 6, 7} , /* 第二行索引为 1 */
{8, 9, 10, 11}, /* 第三行索引为 2 */
}
// 以上代码中倒数第二行的}必须要有逗号,因为最后一行的}不能单独一行,也可以写成这样:
a = [3][4]int{
{0, 1, 2, 3} , /* 第一行索引为 0 */
{4, 5, 6, 7} , /* 第二行索引为 1 */
{8, 9, 10, 11}} /* 第三行索引为 2 */
数组的比较
数组的长度是数组类型的一部分所以[3]int 和[4]int是两种不同的类型。
q := [3]int{1, 2, 3}
q = [4]int{1, 2, 3, 4} // 编译错误: 不可以将[4]int赋值给[3]int
如果两个数组的元素类型是可比较的,那么这个数组也是可比较的,可以直接通过==来进行比较
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true, false, false"
d := [3]int{1, 2}
a == d 编译不通过,无法比较的两个类型
r := [...]int{99:-1} 定义一个拥有100个元素的数组r,除了最后一个元素值是-1,其他都是0
数组作为函数参数
当调用一个函数的时候,每个传入的参数都会创建一个副本,然后赋值给对应的函数变量,所以函数接受的是一个副本,而不是原始的参数。使用这中方式传递大的数组会变得很低效,并且在函数内部对数组的任何修改都仅影响副本,而不是原始数组。Go把数组和其他的类型都当成是值传递。而在其他的语言中,数组是隐式的使用引用传递。也可以显式的传递一个数组指针给函数,这样在函数内部对数组的任何修改都会直接反映到原始数组上面来。
参数指定数组类型的时候也需要指定数组的长度,如上所说数组的长度也是数组类型的一部分,但是这样设计的好处我没有理解透,倒反而带来了诸多的限制
// 函数作用是将原始数组的元素清零
func zero(ptr *[32]byte) {
*ptr = [32]byte{}
}
使用数组指针是高效的,同时允许被调函数修改调用方数组中的元素,到那时因为数组长度是固定的,所以数组本身是不可变的,不能给数组添加或删除元素。也因为数组长度不可变的特性,除了在特殊的情况之外,我们很少使用数组。
切片Slice
slic表示一个拥有相同类型元素的可变长度序列。slice通常写成[]T。其中元素的类型都是T,它看上去就像一个没有指定长度的数组类型,slice是一种轻量级的数据结构,可以用来访问数组的部分或者全部元素,而这个数组成为slice的底层数组。
slice有三个属性:指针、长度和容量,go的内置函数len和cap可以用来获取slice的长度和容量,len表示slice当前的长度,cap表示slice底层数组的实际容量
slice初始化
slice初始化主要有三种方式:
- 通过make函数
- 通过字面量方式
- 对源数组或者源slice使用[start:end]语法生成切片
而切片的cap取决于切片的初始化方式
- 通过make函数初始化一个切片时,capacity由我们自己定义
- 通过字面量初始化一个切片时,capacity默认等于该切片的长度
- 对数组或切片执行array[start:end]操作生成切片时,切片的capacity总等于源数组/源切片的capacity减去start的值,比如array原本的capacity为4,s0 := array[1:],则s0的capacity为3。
func main() {
//make切片
var s0 []int = make([]int,3,5) //切片类型、切片长度、切片容量
fmt.Println(len(s0),cap(s0),s0)
//--> 3 5 [0 0 0]
//字面量切片
var s1 []int = []int{1,2,3} //切片的元素
fmt.Println(len(s1),cap(s1),s1)
//--> 3 3 [1 2 3]
//从数组切片,这里先定义一个数组a
var a [10]int
s2 := a[:3] //从数组a的第0个到第3个元素生成切片
fmt.Println(len(s2),cap(s2),s2)
//--> 3 10 [0 0 0]
//从s2切片
s4 := s2[:2]
fmt.Println(len(s4),cap(s4),s4)
//-->2 10 [0 0]
}
通过make函数创建
slice1 := make([]type, len) 不指定容量的情况下,长度和容量相等
也可以指定容量
make([]T, len, cap) 和 make([]T, cap)[:len] 功能相同
通过对源数组的引用来创建
s :=[] int {1,2,3} // 原始数组
s := arr[:] // 初始化切片,表示是arr数组的引用
slice访问不能超过了设置的capacity的大小,当访问的是从源数组/源切片引用得到的slice时,只要访问的边界在容量范围内,会移动指针扩充slice。
months := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
Q2 := months[3:6]
summer := months[5:8]
endlessSummer := summer[:6]
fmt.Println(endlessSummer)
fmt.Println(summer)
[6 7 8 9 10 11]
[6 7 8]
slice本质上是对已有数组的引用,当我们在slice上对某个元素进行修改时,原数组中相应的元素也被修改了,同时其他指向这个元素的slice也被修改了。当我们用make函数或者字面量方式创建一个slice的时候,实际上是先创建一个数组,再创建一个基于该数组的切片引用
和数组不同的是,slice无法做比较,因此不能用== 来测试两个slice是否拥有相同的元素。标准库提供了bytes.Equal来比较两个字节slice,但是对其他类型的slice需要自己实现比较函数来进行比较。slice只可以和nil进行直接的比较,例如
if slice != nil {...}
slice类型的零值是nil,值为nil类型的slice没有相对应的底层数组。值为nil的slice长度和容量都是0。
var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
var s []int(nil) // len(s) == 0, s == nil
s= []int{} // len(s) == 0, s == nil
nil slice
sliceHeader{
Length: 0,
Capacity: 0,
ZerothElement: nil,
}
append函数
以上我们其实可以看到slice定义了capacity还是不能超过访问,否则就会出现runtime异常,而通过append函数可以扩充slice。append函数能将元素追加到slice后面。append的时候会检查追加的元素有没有超过当前的capacity,如果没超过就会直接设置len+1的位置的元素为append的元素,否则就会重新创建一个数组,并将原始数组利用内置的copy方法进行拷贝,此时返回给调用者的slice已经是指向新的数组地址,大致逻辑如下
func appendInt(x []int, y int) {
var z []int
zlen := len(x) + 1
if zlen <= cap(x)
// slice仍然有增长的空间,扩展slice的内容
z = x[:len]
} else {
// slice已经没有空间了
zcap := zlen
if zcap < 2 * len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x)
}
z[len(x)] = y
return z
}
append方法会修改原始数组的元素
months := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
Q2 := months[3:6]
summer := months[5:8]
endlessSummer := summer[:6]
fakeSummer := append(summer, 100)
fmt.Println(endlessSummer)
fmt.Println(summer)
fmt.Println(fakeSummer)
fmt.Println(months)
[6 7 8 100 10 11]
[6 7 8]
[6 7 8 100]
[1 2 3 4 5 6 7 8 100 10 11 12]
注意到以上 append(summer, 100)
操作导致所有的slice和原始数组都发生了改变。
并且由于append的调用会修改slice header的值,因此一定要保存append后返回的新的slice变量,golang编译器也会强制这一点
slice作为函数参数使用
slice虽然包含了一个指向底层数组的指针,但是他本身是一个值。他是一个结构体,包含了指针,长度和容量,而不是一个指向结构体的指针。因此当我们进行函数调用的时候,进行的是值传递,在go语言中的引用传递都需要显示的通过指针参数来进行,所以函数调用传递slice参数的时候会有一次值的copy
示例1
func main() {
buffer := [100]byte{}
slice := buffer[10:20]
for i := 0; i < len(slice); i++ {
slice[i] = byte(i)
}
fmt.Printf("before %v slice point: %p, array point: %p \n", slice, &slice, slice)
AddOneToEachElement(slice)
fmt.Printf("after %v slice point: %p, array point: %p\n", slice, &slice, slice)
}
func AddOneToEachElement(slice []byte) {
fmt.Printf("in function %v, slice point: %p, array point: %p\n", slice, &slice, slice)
for i := range slice {
slice[i]++
}
}
before [0 1 2 3 4 5 6 7 8 9] slice point: 0xc00000c060, array point: 0xc00005807a
in function [0 1 2 3 4 5 6 7 8 9], slice point: 0xc00000c0c0, array point: 0xc00005807a
after [1 2 3 4 5 6 7 8 9 10] slice point: 0xc00000c060, array point: 0xc00005807a
AddOneToEachElement函数是对参数slice遍历+1, 我们可以看到调用函数之后,main函数中原始的slice中的值也都+1了,也就是slice作为函数参数调用被修改影响了原始slice的内容。
尽管slice是按照值传递,但是函数参数和原始slice内部的指针是指向了同一个数组,因此在函数中的修改会映射到原始的slice变量中。但是由于是值传递,所以函数中使用的slice变量的地址和原始的slice是不一样的,上面代码中打印了两个地址 &slice
得到的是slice变量的真实地址,slice
得到的是底层数组的指针。
可以看到上面输出中原始slice和函数中的slice point是不相同的,但是array point是相同的。
示例2
func SubtractOneFromLength(slice []byte) []byte {
fmt.Printf("%p, In funciton: len(slice) = %v \n", &slice, len(slice))
slice = slice[0 : len(slice)-1]
return slice
}
func main() {
buffer := [100]byte{}
slice := buffer[10:20]
fmt.Printf("%p, Before: len(slice) = %v \n", &slice, len(slice))
newSlice := SubtractOneFromLength(slice)
fmt.Printf("%p, After: len(slice) = %v \n", &slice, len(slice))
fmt.Printf("%p, After: len(newSlice) = %v \n", &slice, len(newSlice))
}
0xc00000c060, Before: len(slice) = 10
0xc00000c080, In funciton: len(slice) = 10
0xc00000c060, After: len(slice) = 10
0xc00000c060, After: len(newSlice) = 9
这里SubtractOneFromLength函数是对原始的slice截断最后一位,并返回。结果显示这样的函数中的截断操作不会影响原始main函数中定义的slice的长度,只有返回的newslice的len缩小了。也就是slice作为函数参数调用修改不影响原始的slice。 和上面的结论是相反的,那这是为什么呢?
这里同样是因为值传递的,函数中的slice是copy原始的slice,两者指向了同一个数组,但是truncate操作修改的其实是len的长度,那么slice的修改其实是互不影响的。
值传递的时候,其实是新创建了以下的数据结构,示例一,因为修改的是array的内容,所以函数内外都受影响了,示例2修改的是新建的slice对象的len的变量,因此函数内外互不影响。
slice的结构(runtime/slice.go)
type slice struct {
array unsafe.Pointer
len int
cap int
}
因此这里我们可以看到slice的内容是可以被一个函数所修改的,但是slice变量的指向(header包括len,cap这些)是不能修改的,如果我们想要写一个函数来修改header的内容,我们就需要将产生的新的slice作为结果返回。
除了作为返回值返回,还可以将结构体作为指针参数
func PtrSubtractOneFromLength(slicePtr *[]byte) {
*slicePtr = (*slicePtr)[0 : len(*slicePtr)-1]
}
func main() {
buffer := [100]byte{}
slice := buffer[10:20]
for i := 0; i < len(slice); i++ {
slice[i] = byte(i)
}
fmt.Println("Before: len(slice) =", len(slice))
PtrSubtractOneFromLength(&slice)
fmt.Println("After: len(slice) =", len(slice))
}
再考虑以下的例子
func grow(s []int) {
fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
s = append(s, 4, 5, 6)
fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
}
func main() {
s := []int{1, 2, 3}
fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
grow(s)
fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
}
输出
value: [1 2 3], length: 3, capacity: 3, addr: 0xc00000a460
value: [1 2 3], length: 3, capacity: 3, addr: 0xc00000a460
value: [1 2 3 4 5 6], length: 6, capacity: 6, addr: 0xc00000c330
value: [1 2 3], length: 3, capacity: 3, addr: 0xc00000a460
注意这里打印出1和2的地址是一样的,这里打印的地址其实是底层数组的地址,上面已经有过相关的解释
为什么调用 grow()
之后得到的 s
是 [1 2 3]
,而不是 [1 2 3 4 5 6]
呢?
因为在 append
的时候,s
没有足够的容量。因此,会创建一个新的 slice。可以从上面的打印看到,在 append
调用前后,s
的容量和地址都发生了改变。因此,append
并不会对 main
函数中的 s
作出任何修改。
那么,如果我们给 s
设置了一个足够大的 capacity 呢?
func grow(s []int) {
fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
s = append(s, 4, 5, 6)
fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
}
func main() {
s := make([]int, 0, 10)
s = append(s, 1, 2, 3)
fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
grow(s)
fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
}
value: [1 2 3], length: 3, capacity: 10, addr: 0xc00007e0a0
value: [1 2 3], length: 3, capacity: 10, addr: 0xc00007e0a0
value: [1 2 3 4 5 6], length: 6, capacity: 10, addr: 0xc00007e0a0
value: [1 2 3], length: 3, capacity: 10, addr: 0xc00007e0a0
结果是,调用 grow()
之后得到的 s
还是 [1 2 3]
!这个和上面的示例2 有异曲同工之处,都是因为slice没有替换,函数中和函数外是两个slice对象导致的。
map
map的类型是map[K]V,map中的所有键都拥有相同的类型,同时所有的值也都拥有相同的数据类型。键的类型K必须是可以通过 ==
来进行比较的数据类型,比如slice就不能作为map的key。
创建map
ages := make(map[string]int)
ages := map[string]int{
"alice": 34,
"yihu": 18
}
type Values map[string][]string 这个定义的是一个map类型,key是string,值是字符串slice
字典删除和访问
delete(ages, "alice")
age, ok := ages["bob"] // 判断一个值是否在map中
if age, ok := ages["bob"]; !ok {...}
map元素不是一个变量,不能获取它的地址,无法获取map元素地址的一个原因是map的增长可能会导致已有元素被重新散列到新的存储位置,这样就可能示获取的地址变为无效
_ = &ages["yh"] // 这个是不允许的操作
因为map的键不能是slice类型,如果要将slice作为键 需要借助一个辅助函数,将slice转化成string
func k(list []string) string {
return fmt.Sprintf("%q", list)
}
参考
深入浅出go slice
https://blog.golang.org/slices 这篇文章分析slice实现非常清晰
https://www.zhihu.com/question/47390706