go 容器类型:数组、切片、映射

63 阅读3分钟

容器用法

go 有三种一等公民类型

  • 数组
  • 切片
  • 映射

映射的键必须是可比较类型,数组和切片的键值类型均为内置类型int

数组仅由一个直接部分组成,切片或映射由一个直接部分和一个引用的间接部分组成

一个赋值结束后,源值和目标值可能会共享底层间接部分。

容器类型的字面量

  • 数组: [N]T
  • 切片:[]T
  • 映射: map[K]T
[4]bool{false, true, true, false}
[]string{"break", "continue", "fallthrough"}
map[string]int{"C": 1972, "Python": 1991, "Go": 2009}

容器类型的零值表示形式

  • 数组:[100]int{}
  • 切片: nil
  • 映射: nil

函数、通道和接口类型的零值也用nil来表示。 数组在声明时内存空间就被开辟出来,但 nil 的切片或映射空间尚未开辟。 []T{}[]T(nil)不等价,同样,map[K]T{}map[K]T(nil)也不等价。

容器值的比较

映射和切片类型都不可比较。 可以和 nil 比较。 大多数数组都是可比较的,但元素类型为不可比较类型的数组不可比较。

容器赋值

  • 映射和切片:会共享底层的元素
  • 数组:不会共享,每次赋值将所有元素从源数组复制到目标数组。
m0 := map[int]int{0:7, 1:8, 2:9}
m1 := m0
m1[0] = 2

使用make函数来创建切片和映射

// map
make(M, n)
make(M)

//slice
make(S, length, capacity)
make(S, length)

容器遍历

遍历一个nil映射或nil切片是允许的,相当于一个空操作

添加或删除容器的元素

s = append(s, elements...) //  插入到结尾
s = append(elements, s...) // 插入到开头
s, e = s[1:], s[0]  // pop front, 弹出第一个元素
s, e = s[:len(s)-1], s[len(s)-1]  // pop back
s = append([]T{e}, s...)  // push front
s = append(s, e)  // push back

删除满足某条件的切片元素

result := s[:0]  //  result 与 s 共享底层数组
 for _, v := range s {
     if keep(v) {
     result = append(result, v)
     }
 }
 
 if clear {
     temp := s[len(result):]
     for i := range temp {
         temp[i] = t0   // t0 是 T 的零值
     }
 }
 
 return result

Go 1.21 引入了 clear内置函数,可以清空 map 或 slice

s := []int{1, 2, 3}
clear(s)

容器元素的可寻址性

  • 一个切片值的任何元素都是可寻址的
  • 任何映射的元素都是不可寻址的
type T struct{age int}
mt := map[string]T{}
mt["John"] = T{age: 29} // 整体修改是允许的
mt["John"].age = 30  // error,这有点反直觉,这个限制以后可能会被移除

t : = mt["John"] 
t.age = 30  // 这是允许的

并发安全

  • 并发读是安全的
  • 并发读写未进行同步,是不安全的

内存泄露

如果删除了切片某些元素,应将删除元素的槽位赋类型的零值,以避免内存泄露

temp := s[len(s): len(s)

容器的实现

切片的实现

type _slice struct {
elements unsafe.Pointer
len int
cap int
}

切片可能位于一个比较大的内存块上,但切片只感知到 0~cap的内存子片段。

如果使用 append 向切片添加元素,新生成的切片可能与基础切片相同,也可能不同。

  • 如果添加元素数量没有超过 cap,仍然在原来的基础切片的内存片段。
  • 如果超过了 cap,新的底层内存会被开辟。