复合数据类型主要有数组,slice,map和结构体

数组

数组是具有固定长度且拥有零个或者多个相同数据类型元素的序列。由于数组的长度固定,所以在Go里面很少直接使用。slice的长度可以增长和缩短,在很多场合下使用更多,但是在了解slice之前先要理解数组的使用

数组定义

数组的声明

  1. var balance [10] float32
  2. var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0} // 初始化
  3. var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0} // 忽略[]中的数字不设置数组大小,Go语言会根据元素的个数来设置数组的大小
  4. balance[4] = 50.0 // 访问数组元素
  5. var threedim [5][10][4]int // 多维数组定义
  6. // 多维数组初始化
  7. a = [3][4]int{
  8. {0, 1, 2, 3} , /* 第一行索引为 0 */
  9. {4, 5, 6, 7} , /* 第二行索引为 1 */
  10. {8, 9, 10, 11}, /* 第三行索引为 2 */
  11. }
  12. // 以上代码中倒数第二行的}必须要有逗号,因为最后一行的}不能单独一行,也可以写成这样:
  13. a = [3][4]int{
  14. {0, 1, 2, 3} , /* 第一行索引为 0 */
  15. {4, 5, 6, 7} , /* 第二行索引为 1 */
  16. {8, 9, 10, 11}} /* 第三行索引为 2 */

数组的比较

数组的长度是数组类型的一部分所以[3]int 和[4]int是两种不同的类型。

  1. q := [3]int{1, 2, 3}
  2. q = [4]int{1, 2, 3, 4} // 编译错误: 不可以将[4]int赋值给[3]int

如果两个数组的元素类型是可比较的,那么这个数组也是可比较的,可以直接通过==来进行比较

  1. a := [2]int{1, 2}
  2. b := [...]int{1, 2}
  3. c := [2]int{1, 3}
  4. fmt.Println(a == b, a == c, b == c) // "true, false, false"
  5. d := [3]int{1, 2}
  6. a == d 编译不通过,无法比较的两个类型
  1. r := [...]int{99:-1} 定义一个拥有100个元素的数组r,除了最后一个元素值是-1,其他都是0

数组作为函数参数

当调用一个函数的时候,每个传入的参数都会创建一个副本,然后赋值给对应的函数变量,所以函数接受的是一个副本,而不是原始的参数。使用这中方式传递大的数组会变得很低效,并且在函数内部对数组的任何修改都仅影响副本,而不是原始数组。Go把数组和其他的类型都当成是值传递。而在其他的语言中,数组是隐式的使用引用传递。也可以显式的传递一个数组指针给函数,这样在函数内部对数组的任何修改都会直接反映到原始数组上面来。

参数指定数组类型的时候也需要指定数组的长度,如上所说数组的长度也是数组类型的一部分,但是这样设计的好处我没有理解透,倒反而带来了诸多的限制

  1. // 函数作用是将原始数组的元素清零
  2. func zero(ptr *[32]byte) {
  3. *ptr = [32]byte{}
  4. }

使用数组指针是高效的,同时允许被调函数修改调用方数组中的元素,到那时因为数组长度是固定的,所以数组本身是不可变的,不能给数组添加或删除元素。也因为数组长度不可变的特性,除了在特殊的情况之外,我们很少使用数组。

切片Slice

slic表示一个拥有相同类型元素的可变长度序列。slice通常写成[]T。其中元素的类型都是T,它看上去就像一个没有指定长度的数组类型,slice是一种轻量级的数据结构,可以用来访问数组的部分或者全部元素,而这个数组成为slice的底层数组。

slice有三个属性:指针、长度和容量,go的内置函数len和cap可以用来获取slice的长度和容量,len表示slice当前的长度,cap表示slice底层数组的实际容量

slice初始化

slice初始化主要有三种方式:

  • 通过make函数
  • 通过字面量方式
  • 对源数组或者源slice使用[start:end]语法生成切片

而切片的cap取决于切片的初始化方式

  1. 通过make函数初始化一个切片时,capacity由我们自己定义
  2. 通过字面量初始化一个切片时,capacity默认等于该切片的长度
  3. 对数组或切片执行array[start:end]操作生成切片时,切片的capacity总等于源数组/源切片的capacity减去start的值,比如array原本的capacity为4,s0 := array[1:],则s0的capacity为3。
  1. func main() {
  2. //make切片
  3. var s0 []int = make([]int,3,5) //切片类型、切片长度、切片容量
  4. fmt.Println(len(s0),cap(s0),s0)
  5. //--> 3 5 [0 0 0]
  6. //字面量切片
  7. var s1 []int = []int{1,2,3} //切片的元素
  8. fmt.Println(len(s1),cap(s1),s1)
  9. //--> 3 3 [1 2 3]
  10. //从数组切片,这里先定义一个数组a
  11. var a [10]int
  12. s2 := a[:3] //从数组a的第0个到第3个元素生成切片
  13. fmt.Println(len(s2),cap(s2),s2)
  14. //--> 3 10 [0 0 0]
  15. //从s2切片
  16. s4 := s2[:2]
  17. fmt.Println(len(s4),cap(s4),s4)
  18. //-->2 10 [0 0]
  19. }

通过make函数创建

  1. slice1 := make([]type, len) 不指定容量的情况下,长度和容量相等
  2. 也可以指定容量
  3. make([]T, len, cap) make([]T, cap)[:len] 功能相同

通过对源数组的引用来创建

  1. s :=[] int {1,2,3} // 原始数组
  2. s := arr[:] // 初始化切片,表示是arr数组的引用

slice访问不能超过了设置的capacity的大小,当访问的是从源数组/源切片引用得到的slice时,只要访问的边界在容量范围内,会移动指针扩充slice。

  1. months := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
  2. Q2 := months[3:6]
  3. summer := months[5:8]
  4. endlessSummer := summer[:6]
  5. fmt.Println(endlessSummer)
  6. fmt.Println(summer)
  7. [6 7 8 9 10 11]
  8. [6 7 8]

slice本质上是对已有数组的引用,当我们在slice上对某个元素进行修改时,原数组中相应的元素也被修改了,同时其他指向这个元素的slice也被修改了。当我们用make函数或者字面量方式创建一个slice的时候,实际上是先创建一个数组,再创建一个基于该数组的切片引用
和数组不同的是,slice无法做比较,因此不能用== 来测试两个slice是否拥有相同的元素。标准库提供了bytes.Equal来比较两个字节slice,但是对其他类型的slice需要自己实现比较函数来进行比较。slice只可以和nil进行直接的比较,例如

  1. if slice != nil {...}

slice类型的零值是nil,值为nil类型的slice没有相对应的底层数组。值为nil的slice长度和容量都是0。

  1. var s []int // len(s) == 0, s == nil
  2. s = nil // len(s) == 0, s == nil
  3. var s []int(nil) // len(s) == 0, s == nil
  4. s= []int{} // len(s) == 0, s == nil

nil slice

  1. sliceHeader{
  2. Length: 0,
  3. Capacity: 0,
  4. ZerothElement: nil,
  5. }

append函数

以上我们其实可以看到slice定义了capacity还是不能超过访问,否则就会出现runtime异常,而通过append函数可以扩充slice。append函数能将元素追加到slice后面。append的时候会检查追加的元素有没有超过当前的capacity,如果没超过就会直接设置len+1的位置的元素为append的元素,否则就会重新创建一个数组,并将原始数组利用内置的copy方法进行拷贝,此时返回给调用者的slice已经是指向新的数组地址,大致逻辑如下

  1. func appendInt(x []int, y int) {
  2. var z []int
  3. zlen := len(x) + 1
  4. if zlen <= cap(x)
  5. // slice仍然有增长的空间,扩展slice的内容
  6. z = x[:len]
  7. } else {
  8. // slice已经没有空间了
  9. zcap := zlen
  10. if zcap < 2 * len(x) {
  11. zcap = 2 * len(x)
  12. }
  13. z = make([]int, zlen, zcap)
  14. copy(z, x)
  15. }
  16. z[len(x)] = y
  17. return z
  18. }

append方法会修改原始数组的元素

  1. months := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
  2. Q2 := months[3:6]
  3. summer := months[5:8]
  4. endlessSummer := summer[:6]
  5. fakeSummer := append(summer, 100)
  6. fmt.Println(endlessSummer)
  7. fmt.Println(summer)
  8. fmt.Println(fakeSummer)
  9. fmt.Println(months)
  10. [6 7 8 100 10 11]
  11. [6 7 8]
  12. [6 7 8 100]
  13. [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

  1. func main() {
  2. buffer := [100]byte{}
  3. slice := buffer[10:20]
  4. for i := 0; i < len(slice); i++ {
  5. slice[i] = byte(i)
  6. }
  7. fmt.Printf("before %v slice point: %p, array point: %p \n", slice, &slice, slice)
  8. AddOneToEachElement(slice)
  9. fmt.Printf("after %v slice point: %p, array point: %p\n", slice, &slice, slice)
  10. }
  11. func AddOneToEachElement(slice []byte) {
  12. fmt.Printf("in function %v, slice point: %p, array point: %p\n", slice, &slice, slice)
  13. for i := range slice {
  14. slice[i]++
  15. }
  16. }
  17. before [0 1 2 3 4 5 6 7 8 9] slice point: 0xc00000c060, array point: 0xc00005807a
  18. in function [0 1 2 3 4 5 6 7 8 9], slice point: 0xc00000c0c0, array point: 0xc00005807a
  19. 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

  1. func SubtractOneFromLength(slice []byte) []byte {
  2. fmt.Printf("%p, In funciton: len(slice) = %v \n", &slice, len(slice))
  3. slice = slice[0 : len(slice)-1]
  4. return slice
  5. }
  6. func main() {
  7. buffer := [100]byte{}
  8. slice := buffer[10:20]
  9. fmt.Printf("%p, Before: len(slice) = %v \n", &slice, len(slice))
  10. newSlice := SubtractOneFromLength(slice)
  11. fmt.Printf("%p, After: len(slice) = %v \n", &slice, len(slice))
  12. fmt.Printf("%p, After: len(newSlice) = %v \n", &slice, len(newSlice))
  13. }
  14. 0xc00000c060, Before: len(slice) = 10
  15. 0xc00000c080, In funciton: len(slice) = 10
  16. 0xc00000c060, After: len(slice) = 10
  17. 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)

  1. type slice struct {
  2. array unsafe.Pointer
  3. len int
  4. cap int
  5. }

因此这里我们可以看到slice的内容是可以被一个函数所修改的,但是slice变量的指向(header包括len,cap这些)是不能修改的,如果我们想要写一个函数来修改header的内容,我们就需要将产生的新的slice作为结果返回。

除了作为返回值返回,还可以将结构体作为指针参数

  1. func PtrSubtractOneFromLength(slicePtr *[]byte) {
  2. *slicePtr = (*slicePtr)[0 : len(*slicePtr)-1]
  3. }
  4. func main() {
  5. buffer := [100]byte{}
  6. slice := buffer[10:20]
  7. for i := 0; i < len(slice); i++ {
  8. slice[i] = byte(i)
  9. }
  10. fmt.Println("Before: len(slice) =", len(slice))
  11. PtrSubtractOneFromLength(&slice)
  12. fmt.Println("After: len(slice) =", len(slice))
  13. }

再考虑以下的例子

  1. func grow(s []int) {
  2. fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
  3. s = append(s, 4, 5, 6)
  4. fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
  5. }
  6. func main() {
  7. s := []int{1, 2, 3}
  8. fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
  9. grow(s)
  10. fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
  11. }

输出

  1. value: [1 2 3], length: 3, capacity: 3, addr: 0xc00000a460
  2. value: [1 2 3], length: 3, capacity: 3, addr: 0xc00000a460
  3. value: [1 2 3 4 5 6], length: 6, capacity: 6, addr: 0xc00000c330
  4. 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 呢?

  1. func grow(s []int) {
  2. fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
  3. s = append(s, 4, 5, 6)
  4. fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
  5. }
  6. func main() {
  7. s := make([]int, 0, 10)
  8. s = append(s, 1, 2, 3)
  9. fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
  10. grow(s)
  11. fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
  12. }
  1. value: [1 2 3], length: 3, capacity: 10, addr: 0xc00007e0a0
  2. value: [1 2 3], length: 3, capacity: 10, addr: 0xc00007e0a0
  3. value: [1 2 3 4 5 6], length: 6, capacity: 10, addr: 0xc00007e0a0
  4. 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

  1. ages := make(map[string]int)
  2. ages := map[string]int{
  3. "alice": 34,
  4. "yihu": 18
  5. }
  6. type Values map[string][]string 这个定义的是一个map类型,keystring,值是字符串slice

字典删除和访问

  1. delete(ages, "alice")
  2. age, ok := ages["bob"] // 判断一个值是否在map中
  3. if age, ok := ages["bob"]; !ok {...}

map元素不是一个变量,不能获取它的地址,无法获取map元素地址的一个原因是map的增长可能会导致已有元素被重新散列到新的存储位置,这样就可能示获取的地址变为无效

  1. _ = &ages["yh"] // 这个是不允许的操作

因为map的键不能是slice类型,如果要将slice作为键 需要借助一个辅助函数,将slice转化成string

  1. func k(list []string) string {
  2. return fmt.Sprintf("%q", list)
  3. }

参考

深入浅出go slice
https://blog.golang.org/slices 这篇文章分析slice实现非常清晰
https://www.zhihu.com/question/47390706