Go 容器类型

admin
admin 2020年03月10日
  • 在其它设备中阅读本文章

每个 容器 用来表示和存储一个元素(element)序列或集合。一个容器中的所有元素的 类型是相同的 ,此相同的类型称为此容器的 元素类型 。Go 中有三种容器类型:数组、切片和映射。
存储在一个容器中的每个元素值都关联着着一个 键值 (key)。每个元素可以通过它的键值而被访问到。一个映射类型的键值类型必须为一个 可比较类型 。数组和切片类型的键值类型均为内置类型 int。一个数组或切片的一个元素对应的键值总是一个非负整数下标,此非负整数表示该元素在该数组或切片所有元素中的顺序位置。此非负整数下标亦常称为一个 元素索引 (index)。
每个容器值有一个长度属性,用来表明此容器中当前存储了多少个元素。一个数组或切片中的每个元素所关联的非负整数索引键值的合法取值范围为左闭右开区间 [0, 此数组或切片的长度)。一个映射值类型的容器值中的元素关联的键值可以是其键值类型的任何值。
一个数组或者切片的所有元素按顺序存放在一块连续的内存中。在官方标准编译器和运行时中,映射是使用哈希表算法来实现的。所以一个映射中的所有元素也均存放在一块连续的内存中,但是映射中的元素并不一定按顺序存放。一般来说,映射元素访问消耗的时长要数倍于数组和切片元素访问消耗的时长。但是映射相对于数组和切片有两个优点:

  • 映射的键值类型可以是任何可比较类型。
  • 相对于使用含有大量稀疏索引的数组和切片,使用映射可以节省大量的内存。

一、容器类型的字面表示

容器类型的字面表示形式如下:

  • 数组类型:[N]T
  • 切片类型:[]T
  • 映射类型:map[K]T
  • 其中:

    • T 可为任意类型。它表示一个容器类型的元素类型。某个特定类型的容器中只能存储此容器的元素类型的值。
    • N 必须为一个非负整数常量。它指定了一个数组类型的长度,或者说它指定了此数组类型的任何一个值中存储了多少个元素。一个数组类型的长度是此数组类型的一部分。比如 [5]int 和 [8]int 是两个不同的类型。
    • K 必须为一个可比较类型。它指定了一个映射类型的键值类型。

    下面列出了一些容器类型的字面表示:

    const Size = 32
    type Person struct {
      name string
      age  int
    }
    // 数组类型
    [5]string
    [Size]int
    [16][]byte  // 元素类型为一个切片类型:[]byte
    [100]Person // 元素类型为一个结构体类型:Person
    // 切片类型
    []bool
    []int64
    []map[int]bool // 元素类型为一个映射类型:map[int]bool
    []*int         // 元素类型为一个指针类型:*int
    // 映射类型
    map[string]int
    map[int]bool
    map[int16][6]string     // 元素类型为一个数组类型:[6]string
    map[bool][]string       // 元素类型为一个切片类型:[]string
    map[struct{x int}]*int8 // 元素类型为一个指针类型:*int8;
                          // 键值类型为一个结构体类型。

二、容器值的字面表示

和结构体值类似,容器值的文字表示也可以用组合字面值(composite literal)来表示。比如对于一个容器类型 T,它的值可以用形式 T{…}来表示(除了切片和映射的零值外)。下面是一些结构体值的字面表示形式:

// 一个含有4个布尔元素的数组值。
[4]bool{false, true, true, false}
// 一个含有三个字符串值的切片值。
[]string{"break", "continue", "fallthrough"}
// 一个映射值。
map[string]int{"C": 1972, "Python": 1991, "Go": 2009}

映射组合字面值中大括号中的每一项称为一个键值对(key-value pair),或者称为一个条目(entry)。 数组和切片组合字面值有一些微小的变种:

// 下面这些切片的字面表示形式都是等价的。
[]string{"break", "continue", "fallthrough"}
[]string{0: "break", 1: "continue", 2: "fallthrough"}
[]string{2: "fallthrough", 1: "continue", 0: "break"}
[]string{2: "fallthrough", 0: "break", "continue"}
// 下面这些数组的字面表示形式都是等价的。
[4]bool{false, true, true, false}
[4]bool{0: false, 1: true, 2: true, 3: false}
[4]bool{1: true, true}
[4]bool{2: true, 1: true}
[...]bool{false, true, true, false}
[...]bool{3: false, 1: true, true}

上例中最后两行中的…表示让编译器推断出相应数组值的类型的长度。 从上面的例子中,我们可以看出数组和切片组合字面值中的索引下标(即键值)是可选的。在一个数组或者切片组合字面值中:

  • 如果一个索引下标出现,它的类型不必是数组和切片类型的键值类型 int,但它必须是一个可以表示为 int 值的非负常量;如果它是一个类型确定值,则它的类型必须为一个基本整数类型。
  • 在一个数组和切片组合字面值中,如果一个元素的索引下标缺失,则编译器认为它的索引下标为出现在它之前的元素的索引下标加一。
  • 如果出现的第一个元素的索引下标缺失,则它的索引下标被认为是 0。 映射组合字面值中元素对应的键值不可缺失,并且它们可以为非常量。

注意:一个容器组合字面值中的常量键值(包括索引下标)不可重复。

零值的字面表示:
和结构体类似,一个数组类型 A 的零值可以表示为 A{}。比如,数组类型 [100]int 的零值可以表示为[100]int{}。一个数组零值中的所有元素均为对应数组元素类型的零值。
和指针一样,所有切片和映射类型的零值均用预声明的标识符 nil 来表示。在运行时刻,即使一个数组变量在声明的时候未指定初始值,它的元素所占的内存空间也已经被开辟出来。但是一个 nil 切片或者映射值的元素的内存空间尚未被开辟出来。

注意:[]T{}表示类型 []T 的一个空切片值,它和[]T(nil) 是不等价的。同样,map[K]T{}和 map[K]T(nil)也是不等价的。

容器字面值是不可寻址的但可以被取地址:
例子:

package main
import "fmt"
func main() {
    pm := &map[string]int{"C": 1972, "Go": 2009}
    ps := &[]string{"break", "continue"}
    pa := &[...]bool{false, true, true, false}
    fmt.Printf("%T\n", pm) // *map[string]int
    fmt.Printf("%T\n", ps) // *[]string
    fmt.Printf("%T\n", pa) // *[4]bool
}

内嵌组合字面值可以被简化:
在某些情形下,内嵌在其它组合字面值中的组合字面值可以简化为 {…}(即类型部分被省略掉了)。内嵌组合字面值前的取地址操作符 & 有时也可以被省略。 比如,下面的组合字面值

// heads为一个切片值。它的类型的元素类型为*[4]byte。
// 此元素类型为一个基类型为[4]byte的指针类型。
// 此指针基类型为一个元素类型为byte的数组类型。
var heads = []*[4]byte{
    &[4]byte{'P', 'N', 'G', ' '},
    &[4]byte{'J', 'P', 'E', 'G'},
}

可以被简化为

var heads = []*[4]byte{
    {'P', 'N', 'G', ' '},
    {'J', 'P', 'E', 'G'},
}

下面这个数组组合字面值

type language struct {
    name string
    year int
}
var _ = [...]language{
    language{"C", 1972},
    language{"Python", 1991},
    language{"Go", 2009},
}

可以被简化为

var _ = [...]language{
    {"C", 1972},
    {"Python", 1991},
    {"Go", 2009},
}

下面这个映射组合字面值

type LangCategory struct {
    dynamic bool
    strong  bool
}
// 此映射值的类型的键值类型为一个结构体类型,
// 元素类型为另一个映射类型:map[string]int。
var _ = map[LangCategory]map[string]int{
    LangCategory{true, true}: map[string]int{
        "Python": 1991,
        "Erlang": 1986,
    },
    LangCategory{true, false}: map[string]int{
        "JavaScript": 1995,
    },
    LangCategory{false, true}: map[string]int{
        "Go":   2009,
        "Rust": 2010,
    },
    LangCategory{false, false}: map[string]int{
        "C": 1972,
    },
}

可以被简化为

var _ = map[LangCategory]map[string]int{
    {true, true}: {
        "Python": 1991,
        "Erlang": 1986,
    },
    {true, false}: {
        "JavaScript": 1995,
    },
    {false, true}: {
        "Go":   2009,
        "Rust": 2010,
    },
    {false, false}: {
        "C": 1972,
    },
}

三、容器比较

映射和切片类型都属于不可比较类型,即任意两个映射值(或切片值)是不能相互比较的,而大多数数组类型都是可比较类型,除了元素类型为不可比较类型的数组类型。当比较两个数组值时,它么的对应元素将逐一被比较。这两个数组只有在它们的对应元素都相等的情况下才相等。
尽管两个映射值和切片值是不能比较的,但是一个映射值或者切片值可以和预声明的 nil 标识符进行比较以检查此映射值或者切片值是否为一个零值。
一个例子:

package main
import "fmt"
func main() {
    var a [16]byte
    var s []int
    var m map[string]int
    fmt.Println(a == a)   // true
    fmt.Println(m == nil) // true
    fmt.Println(s == nil) // true
    fmt.Println(nil == map[string]int{}) // false
    fmt.Println(nil == []int{})          // false
    // 下面这些行编译不通过。
    /*
    _ = m == m
    _ = s == s
    _ = m == map[string]int(nil)
    _ = s == []int(nil)
    var x [16][]int
    _ = x == x
    var y [16]map[int]bool
    _ = y == y
    */
}

四、容器值的长度和容量

除了上面已提到的容器长度属性(此容器中含有有多少个元素),每个容器值还有一个容量属性。一个数组值的容量总是和它的长度相等;一个非零映射值的容量可以被认为是无限大的。一个切片值的容量总是不小于此切片值的长度。在编程中,只有切片值的容量有实际意义。
我们可以调用内置函数 len 来获取一个容器值的长度,或者调用内置函数 cap 来获取一个容器值的容量。这两个函数都返回一个 int 类型确定结果值。因为非零映射值的容量是无限大,所以 cap 并不适用于映射值。
一个数组值的长度和容量永不改变。同一个数组类型的所有值的长度和容量都总是和此数组类型的长度相等。切片值的长度和容量可在运行时刻改变。因为此原因,切片可以被认为是动态数组。切片在使用上相比数组更为灵活,所以切片(相对数组)在编程用得更为广泛。

package main
import "fmt"
func main() {
    var a [5]int
    fmt.Println(len(a), cap(a)) // 5 5
    var s []int
    fmt.Println(len(s), cap(s)) // 0 0
    s, s2 := []int{2, 3, 5}, []bool{}
    fmt.Println(len(s), cap(s), len(s2), cap(s2)) // 3 3 0 0
    var m map[int]bool
    fmt.Println(len(m)) // 0
    m, m2 := map[int]bool{1: true, 0: false}, map[int]int{}
    fmt.Println(len(m), len(m2)) // 2 0
}

五、读取和修改容器的元素

一个容器 v 中存储的对应着键值 k 的元素用语法形式 v[k]来表示。今后我们称 v[k]为一个元素索引表达式。 假设 v 是一个数组或者切片,在 v[k]中,

  • 如果 k 是一个常量,则它必须满足上面列出的对出现在组合字面值中的索引的要求。另外,如果 v 是一个数组,则 k 必须小于此数组的长度。
  • 如果 k 不是一个常量,则它必须为一个整数。另外它必须为一个非负数并且小于 len(v),否则,在运行时刻将产生一个恐慌。
  • 如果 v 是一个零值切片,则在运行时刻将产生一个恐慌。假设 v 是一个映射值,在 v[k]中,k 的类型必须为(或者可以隐式转换为)v 的类型的元素类型。另外,
  • 如果 k 是一个动态类型为不可比较类型的接口值,则 v[k]在运行时刻将造成一个恐慌;
  • 如果 v[k]被用做一个赋值语句中的目标值并且 v 是一个零值 nil 映射,则 v[k]在运行时刻将造成一个恐慌;
  • 如果 v[k]用来表示读取映射值 v 中键值 k 对应的元素,则它无论如何都不会产生一个恐慌,即使 v 是一个零值 nil 映射(假设 k 的估值没有造成恐慌);
  • 如果 v[k]用来表示读取映射值 v 中键值 k 对应的元素,并且映射值 v 中并不含有对应着键值 k 的条目,则 v[k]返回一个此映射值的类型的元素类型的零值。一般情况下,v[k]被认为是一个单值表达式。但是在一个 v[k]被用为唯一源值的赋值语句中,v[k]可以返回一个可选的第二个返回值。此第二个返回值是一个类型不确定布尔值,用来表示是否有对应着键值 k 的条目存储在映射值 v 中。

    package main
    import "fmt"
    func main() {
      a := [3]int{-1, 0, 1}
      s := []bool{true, false}
      m := map[string]int{"abc": 123, "xyz": 789}
      fmt.Println (a[2], s[1], m["abc"])    // 读取
      a[2], s[1], m["abc"] = 999, true, 567 // 修改
      fmt.Println (a[2], s[1], m["abc"])    // 读取
      n, present := m["hello"]
      fmt.Println(n, present, m["hello"]) // 0 false 0
      n, present = m["abc"]
      fmt.Println(n, present, m["abc"]) // 567 true 567
      m = nil
      fmt.Println(m["abc"]) // 0
      // 下面这两行编译不同过。
      /*
      _ = a[3]  // 下标越界
      _ = s[-1] // 下标越界
      */
      // 下面这几行每行都会造成一个恐慌。
      _ = a[n]         // panic: 下标越界
      _ = s[n]         // panic: 下标越界
      m["hello"] = 555 // panic: m为一个零值映射
    }

    六、容器赋值

    当一个映射赋值语句执行完毕之后,目标映射值和源映射值将共享底层的元素。向其中一个映射中添加(或从一个映射中删除)元素将体现在另一个映射中。
    和映射一样,当一个切片赋值给另一个切片后,它们将共享底层的元素。它们的长度和容量也相等。但是如果以后其中一个切片改变了长度或者容量,此变化不会影响到另一个切片。
    当一个数组被赋值给另一个数组,所有的元素都将被从源数组复制到目标数组。赋值完成之后,这两个数组不共享任何元素。

    package main
    import "fmt"
    func main() {

      m0 := map[int]int{0:7, 1:8, 2:9}
      m1 := m0
      m1[0] = 2
      fmt.Println(m0, m1) // map[0:2 1:8 2:9] map[0:2 1:8 2:9]
      s0 := []int{7, 8, 9}
      s1 := s0
      s1[0] = 2
      fmt.Println(s0, s1) // [2 8 9] [2 8 9]
      a0 := [...]int{7, 8, 9}
      a1 := a0
      a1[0] = 2
      fmt.Println(a0, a1) // [7 8 9] [2 8 9]

    }

七、添加和删除容器元素

向一个映射中添加一个条目的语法和修改一个映射元素的语法是一样的。比如,对于一个非零映射值 m,如果当前 m 中尚未存储条目(k, e),则下面的语法形式将把此条目存入 m;否则,下面的语法形式将把键值 k 对应的元素值更新为 e。

m[k] = e

内置函数 delete 用来从一个映射中删除一个条目。比如,下面的 delete 调用将把键值 k 对应的条目从映射 m 中删除。如果映射 m 中未存储键值为 k 的条目,则此调用为一个空操作,它不会产生一个恐慌,即使 m 是一个 nil 零值映射。

delete(m, k)

下面的例子展示了如何向一个映射添加和从一个映射删除条目。

package main
import "fmt"
func main() {
    m := map[string]int{"Go": 2007}
    m["C"] = 1972     // 添加
    m["Java"] = 1995  // 添加
    fmt.Println(m)    // map[C:1972 Go:2007 Java:1995]
    m["Go"] = 2009    // 修改
    delete(m, "Java") // 删除
    fmt.Println(m)    // map[C:1972 Go:2009]
}

一个数组中的元素个数总是恒定的,我们无法向其中添加元素,也无法从其中删除元素。但是可寻址的数组值中的元素是可以被修改的。 我们可以通过调用内置 append 函数,以一个切片为基础,来添加不定数量的元素并返回一个新的切片。此新的结果切片包含着基础切片中所有的元素和所有被添加的元素。注意,基础切片并未被此 append 函数调用所修改。当然,如果我们愿意(事实上在实践中常常如此),我们可以将结果切片赋值给基础切片以修改基础切片。 Go 中并未提供一个内置方式来从一个切片中删除一个元素。我们必须使用 append 函数和后面将要介绍的子切片语法一起来实现元素删除操作。

package main
import "fmt"
func main() {
    s0 := []int{2, 3, 5}
    fmt.Println(s0, cap(s0)) // [2 3 5] 3
    s1 := append(s0, 7)      // 添加一个元素
    fmt.Println(s1, cap(s1)) // [2 3 5 7] 6
    s2 := append(s1, 11, 13) // 添加两个元素
    fmt.Println(s2, cap(s2)) // [2 3 5 7 11 13] 6
    s3 := append(s0)         // <=> s3 := s0
    fmt.Println(s3, cap(s3)) // [2 3 5] 3
    s4 := append(s0, s0…)  // 以s0为基础添加s0中所有的元素
    fmt.Println(s4, cap(s4)) // [2 3 5 2 3 5] 6
    s0[0], s1[0] = 99, 789
    fmt.Println(s2[0], s3[0], s4[0]) // 789 99 2
}

八、容器创建

1、使用内置 make 函数来创建切片和映射

除了使用组合字面值来创建映射和切片,我们还可以使用内置 make 函数来创建映射和切片,数组不能使用内置 make 函数来创建。假设 M 是一个映射类型并且 n 是一个非负整数,我们可以用下面的两种函数调用来各自生成一个类型为 M 的映射值。

make(M, n)
make(M)

第一个函数调用形式创建了一个可以容纳至少 n 个条目而无需再次开辟内存的空映射值。第二个函数调用形式创建了一个可以容纳一个小数目的条目而无需再次开辟内存的空映射值。此小数目的值取决于具体编译器实现。 假设 S 是一个切片类型,length 和 capacity 是两个非负整数,并且 length 小于等于 capacity,我们可以用下面的两种函数调用来各自生成一个类型为 S 的切片值。

make(S, length, capacity)
make(S, length) // <=> make(S, length, length)

第一个函数调用创建了一个长度为 length 并且容量为 capacity 的切片。第一个函数调用创建了一个长度为 length 并且容量也为 length 的切片。 使用 make 函数创建的切片中的所有元素值均被初始化为(结果切片的元素类型的)零值。

package main
import "fmt"
func main() {
    // 创建映射。
    fmt.Println(make(map[string]int)) // map[]
    m := make(map[string]int, 3)
    fmt.Println(m, len(m)) // map[] 0
    m["C"] = 1972
    m["Go"] = 2009
    fmt.Println(m, len(m)) // map[C:1972 Go:2009] 2
    // 创建切片。
    s := make([]int, 3, 5)
    fmt.Println(s, len(s), cap(s)) // [0 0 0] 3 5
    s = make([]int, 2)
    fmt.Println(s, len(s), cap(s)) // [0 0] 2 2
}

2、使用内置 new 函数来创建容器

内置 new 函数可以用来为一个任何类型的值开辟内存并返回一个存储有此值的地址的指针。用 new 函数开辟出来的值均为零值。因为这个原因,new 函数对于创建映射和切片值来说没有任何价值。 使用 new 函数来用来创建数组值并非是完全没有意义的,但是在实践中很少这么做,因为使用组合字面值来创建数组值更为方便。 一个使用 new 函数创建容器值的例子:

package main
import "fmt"
func main() {
    m := new(map[string]int)   // <=> var m map[string]int
    fmt.Println(m == nil)       // true
    s := new([]int)            // <=> var s []int
    fmt.Println(s == nil)       // true
    a := new([5]bool)          // <=> var a [5]bool
    fmt.Println(a == [5]bool{}) // true
}