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

    var a uint = 1 
    var _ = map[uint]int {a : 123} // ok
    var _ = []int{a: 100} // error: 下标必须为常量 
    var _ = [5]int{a: 100} // error: 下标必须为常量

数组

数组:具有固定长度的数据结构,声明时需要指定数组的大小,且大小不能更改。 是值类型,直接存储元素的实际值。

arr1:=[...]int{1,2,3}
//2和3一样
arr2:=[3]string{"1","1","1"}
arr3:=[3]string{0:"1",1:"1",2:"1"}
  • 当数组作为参数传递给函数或赋值给另一个数组时,会复制整个数组的值长度是数组类型的一部分,不可更改。
     - 一个数组零值中的所有元素均为对应数组元素类型的零值。
     - 扩容:threshold为临界点,cap(slice)<threshold翻倍扩容,反之扩容3(oldcap+3threshold)/4
     - 大多数数组类型都是可比较类型,除了元素类型为不可比较类型的数组类型。当比较两个数组值时,它们的对应元素将按照逐一被比较(可以认为按照下标顺序比较)。这两个数组只有在它们的对应元素都相等的情况下才相等
     - 当一个数组被赋值给另一个数组,所有的元素都将被从源数组复制到目标数组。赋值完成之后,这两个数组不共享任何元素。

    a0 := [...]int{7, 8, 9} 
    a1 := a0 a1[0] = 2
    // [7 8 9] [2 8 9]
    fmt.Println(a0, a1) 
  • 一个数组中的元素个数总是恒定的,我们无法向其中添加元素,也无法从其中删除元素。但是可寻址的数组值中的元素是可以被修改的。
  • 数组不能使用内置make函数来创建。
  • 如果aContainer是一个数组,那么在遍历过程中对此数组元素的修改不会体现到循环变量中。 原因是此数组的副本(被真正遍历的容器)和此数组不共享任何元素。

    for key, element = range aContainer {...}
    ---------
    func main() {
      type Person struct {
          name string
          age  int
      }
      persons := [2]Person {{"Alice", 28}, {"Bob", 25}}
      for i, p := range persons {
          //0 {Alice 28}
          //1 {Bob 25}
          fmt.Println(i, p)
          // 此修改将不会体现在这个遍历过程中,
          // 因为被遍历的数组是persons的一个副本。
          persons[1].name = "Jack"
    
          // 此修改不会反映到persons数组中,因为p是persons数组的副本中的一个元素的副本。
          p.age = 31
      }
      
      //persons: &[{Alice 28} {Jack 25}]
      fmt.Println("persons:", &persons)
    }

    切片

type _slice struct {
    elements unsafe.Pointer // 引用着底层存储在间接部分上的元素
    len      int            // 长度
    cap      int            // 容量
}

nil切片与空切片:

func main() {

    var s []string
    //true
    s=[]string(nil)
    fmt.Println(s==nil)
    //false
    s=[]string{}
    fmt.Println(s==nil)
    //false
    s=make([]string, 0)
    fmt.Println(s==nil)
}

增改查

  • 当一个切片被用做一个append函数调用中的基础切片,如果添加的元素数量大于此(基础)切片的冗余元素槽位的数量,则一个新的底层内存片段将被开辟出来并用来存放结果切片的元素。 这时,基础切片和结果切片不共享任何底层元素。否则,不会有底层内存片段被开辟出来。这时,基础切片中的所有元素也同时属于结果切片。两个切片的元素都存放于同一个内存片段上。

    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
      //s0、s3共享一个底层数组
      //s1、s2共享一个底层数组
      //s4自己是一个底层数组
      fmt.Println(s2[0], s3[0], s4[0]) // 789 99 2
    }
  • 如果aContainer是一个切片(或者映射),那么在遍历过程中对此切片(或者映射)元素的修改将体现到循环变量中。 原因是此切片(或者映射)的副本和此切片(或者映射)共享元素(或条目)。

    for key, element = range aContainer {...}
    ---------
    persons := []Person {{"Alice", 28}, {"Bob", 25}}
      for i, p := range persons {
          //0 {Alice 28} 1 {Jack 25}
          fmt.Println(i, p)
          // 这次,此修改将反映在此次遍历过程中。
          persons[1].name = "Jack"
          // 这个修改仍然不会体现在persons切片容器中。
          p.age = 31
      }
      //persons: &[{Alice 28} {Jack 25}]
      fmt.Println("persons:", &persons)
  • 对一个如下for-range循环代码块(注意range前面是:=

    //在Go 1.22之前,所有被遍历的键值元素对将被赋值给同一对循环变量实例。
    //从Go 1.22版本开始,每组键值元素对将被赋值给一对与众不同的循环变量实例(既循环变量在每个循环步都会生成一份新的实例)。
    for key, element := range aContainer {...}
    
    ---------
    //1.19.4
    //2 2 
    //2 2 
    //2 2 
    //1.23.0
    //2 2 
    //1 1 
    //0 0
    func main() {
    
        for i, n := range []int{0, 1, 2} {
    
            defer func() {
    
                fmt.Println(i, n)
    
            }()
    
        }
    
    }
  • 一般来说,一个切片的长度和容量不能被单独修改。一个切片只有通过赋值的方式被整体修改。 但是,事实上,我们可以通过反射的途径来单独修改一个切片的长度或者容量。

    func main() {
      s := make([]int, 2, 6)
      fmt.Println(len(s), cap(s)) // 2 6
    
      reflect.ValueOf(&s).Elem().SetLen(3)
      fmt.Println(len(s), cap(s)) // 3 6
    
      reflect.ValueOf(&s).Elem().SetCap(5)
      fmt.Println(len(s), cap(s)) // 3 5
    }
  • 子切片操作有可能会造成暂时性的内存泄露

    func f() []int {
      //开辟的内存块中的前50个元素槽位在它的调用返回之后将不再可见。 这50个元素槽位所占内存浪费了,这属于暂时性的内存泄露。
      s := make([]int, 10, 100)
      return s[50:60]
    }

    删除数据

截取法(修改原切片)

func DeleteSlice1(s []int, elem int) []int {
    for i := 0; i < len(s); i++ {
        if s[i] == elem {
            s = append(s[:i], s[i+1:]...)
            i--
        }
    }
    return s
}

拷贝法(不改变原切片)

func DeleteSlice2(s []int, elem int) []int {
    r := make([]int, 0, len(s))
    for _, v := range s {
        if v != elem {
            r = append(r, v)
        }
    }
    return r
}

移位法(修改原切片)

func DeleteSlice3(s []int, elem int) []int {
    j := 0
    for _, v := range s {
        if v != elem {
            s[j] = v
            j++
        }
    }
    return s[:j]
}

扩容

//num是要添加的元素数量
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {

    oldLen := newLen - num
    
    //...竞争检测、内存和地址检测...
    
    if newLen < 0 {
        panic(errorString("growslice: len out of range"))
    }
    //如果元素大小为零,返回一个指向zerobase的新切片。
    if et.Size_ == 0 {
        return slice{unsafe.Pointer(&zerobase), newLen, newLen}
    }
    
    //计算新容量
    newcap := nextslicecap(newLen, oldCap)
    
    //...内存计算...
    
    //...溢出检查...
    
    //...内存分配...
    
    //...内存复制...

    return slice{p, newLen, newcap}
}

func nextslicecap(newLen, oldCap int) int {

    newcap := oldCap
    
    doublecap := newcap + newcap
    //newLen大于doublecap,直接返回newLen
    if newLen > doublecap {
    
        return newLen
    
    }
    //定义一个阈值 threshold为 256。
    const threshold = 256
    
    //如果oldCap小于该阈值,返回doublecap
    if oldCap < threshold {
    
        return doublecap
    
    }
    
    for {
    
        // 对于小切片增长 2 倍过渡
        // 对于大切片,增长 1.25 倍
        
        // 这个公式在两者之间提供平滑的过渡。
        newcap += (newcap + 3*threshold) >> 2
        
        // 我们需要检查 `newcap >= newLen` 以及 `newcap` 是否溢出。    
        if uint(newcap) >= uint(newLen) {
            break
        }
    }
    ...
}

映射

 在官方标准编译器和运行时中,映射是使用哈希表算法来实现的。所以一个映射中的所有元素也均存放在一块连续的内存中,但是映射中的元素并不一定紧挨着存放。
 

type hmap struct{
    count int //保存的元素个数
    ...
    B uint8
    ...
    buckets unsafe.Pointer //bucket数组指针,数组的大小为2^B
}

增删改查

  • 键的类型必须为可比较的类型( slice、map 和 function 不能作为 key)
  • 当一个映射赋值语句执行完毕之后,目标映射值和源映射值将共享底层的元素。 向其中一个映射中添加(或从中删除)元素将体现在另一个映射中

    m0 := map[int]int{0:7, 1:8, 2:9}
    m1 := m0
    m1[0] = 2
    // map[0:2 1:8 2:9] map[0:2 1:8 2:9]
    fmt.Println(m0, m1) 
  • 内置new函数可以用来为一个任何类型的值开辟内存并返回一个存储有此值的地址的指针。 用new函数开辟出来的值均为零值。因为这个原因,new函数对于创建映射和切片值来说没有任何价值。
  • 所有切片和映射类型的零值均用预声明的标识符nil来表示。一个nil切片或者映射值的元素的内存空间尚未被开辟出来。可以对nil map进行读取,不存在也不会报错
  • nil map 进行写操作会引发 panic
  • 使用 delete(m, key) 函数删除 map 中的元素,即使 key 不存在也不会报错即使m是一个nil零值映射。
  • 任何映射元素都是不可寻址的。

    //error
    _ = &map[int]bool{1: true}[1]
  • 如果aContainer是一个切片(或者映射),那么在遍历过程中对此切片(或者映射)元素的修改将体现到循环变量中。 原因是此切片(或者映射)的副本和此切片(或者映射)共享元素(或条目)。

    func main() {
      m := map[int]struct{ dynamic, strong bool } {
          0: {true, false},
          1: {false, true},
          2: {false, false},
      }
      
      for _, v := range m {
          // This following line has no effects on the map m.
          v.dynamic, v.strong = true, true
      }
      
      fmt.Println(m[0]) // {true false}
      fmt.Println(m[1]) // {false true}
      fmt.Println(m[2]) // {false false}
    }
  • for key, element := range aContainer {...}

    func main() {
      var m = map[*int]uint32{}
      for i, n := range []int{1, 2, 3} {
          m[&i]++
          m[&n]++
      }
      //1.19.4
      //map[0xc000018098:3 0xc0000180b0:3]
      //1.23.0
      //map[0xc000012120:1 0xc000012128:1 0xc000012130:1 0xc000012138:1 0xc000012140:1 0xc000012148:1]
      fmt.Println(m)
    }

    比较

  • 映射和切片类型都属于不可比较类型
  • 虽然go没有set类型,但是可以用映射来实现,用map[K]struct{} 这种形式来表示 set 不光是节省内存,还能明确表达出这是一个 set 的含义

    扩容

负载因子=kv数/桶数(不加溢出桶,即数组数)> 6.5时进行扩容

  • 增量(翻倍)扩容:创建新的 bucket 数组,容量是原来的 2 倍
  • 等量扩容(溢出桶数>数组数):创建新的 bucket 数组,与原数组大小相同,将原有元素重新分配到新的 bucket 中,使其更加紧凑
  • 渐进式扩容:扩容过程不是一次性完成的,而是采用"渐进式"的方式,每次操作 map 时(插入、删除、查找)都会尝试搬移 2 个 bucket 的数据,直到所有旧 bucket 的数据都搬移完毕
  • 扩容细节:在扩容过程中,新旧 bucket 数组都会保留,新的 key/value 会写入新的 bucket 数组,读取时会先查找新的 bucket 数组,如果没找到再查找旧的 bucket 数组

桃瑾
1 声望1 粉丝

常常播种,有时收获


« 上一篇
go-并发编程
下一篇 »
go-字符串