• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Golang的map类型

武飞扬头像
@航空母舰
帮助1

删除Map的key 内存是否会自动释放

  • 如果删除的元素是值类型,如int,float,bool,string以及数组和struct,map的内存不会自动释放
  • 如果删除的元素是引用类型,如指针,slice,map,chan等,map的内存会自动释放,但释放的内存是子元素应用类型的内存占用
  • 将map设置为nil后,内存被回收

值类型Map

  1.  
    package main
  2.  
     
  3.  
    import (
  4.  
    "log"
  5.  
    "runtime"
  6.  
    )
  7.  
  8.  
    var lastTotalFreed uint64
  9.  
    var intMap map[int]int
  10.  
    var cnt = 8192
  11.  
     
  12.  
    func main() {
  13.  
    printMemStats()
  14.  
     
  15.  
    initMap()
  16.  
    runtime.GC()
  17.  
    printMemStats()
  18.  
     
  19.  
    log.Println(len(intMap))
  20.  
    for i := 0; i < cnt; i {
  21.  
    delete(intMap, i)
  22.  
    }
  23.  
    log.Println(len(intMap))
  24.  
     
  25.  
    runtime.GC()
  26.  
    printMemStats()
  27.  
     
  28.  
    intMap = nil
  29.  
    runtime.GC()
  30.  
    printMemStats()
  31.  
    }
  32.  
     
  33.  
    func initMap() {
  34.  
    intMap = make(map[int]int, cnt)
  35.  
     
  36.  
    for i := 0; i < cnt; i {
  37.  
    intMap[i] = i
  38.  
    }
  39.  
    }
  40.  
     
  41.  
    func printMemStats() {
  42.  
    var m runtime.MemStats
  43.  
    runtime.ReadMemStats(&m)
  44.  
    log.Printf("Alloc = %v TotalAlloc = %v Just Freed = %v Sys = %v NumGC = %v\n",
  45.  
    m.Alloc/1024, m.TotalAlloc/1024, ((m.TotalAlloc-m.Alloc)-lastTotalFreed)/1024, m.Sys/1024, m.NumGC)
  46.  
     
  47.  
    lastTotalFreed = m.TotalAlloc - m.Alloc
  48.  
    }
学新通

看结果前,解释下几个字段:

  • Alloc:当前堆上对象占用的内存大小。
  • TotalAlloc:堆上总共分配出的内存大小。
  • Sys:程序从操作系统总共申请的内存大小。
  • NumGC:垃圾回收运行的次数。

结果如下: 

  1.  
    2019/12/19 11:48:03 Alloc = 89 TotalAlloc = 89 Just Freed = 0 Sys = 1700 NumGC = 0
  2.  
    2019/12/19 11:48:03 Alloc = 403 TotalAlloc = 437 Just Freed = 33 Sys = 3234 NumGC = 1
  3.  
    2019/12/19 11:48:03 8192
  4.  
    2019/12/19 11:48:03 0
  5.  
    2019/12/19 11:48:03 Alloc = 404 TotalAlloc = 438 Just Freed = 1 Sys = 3234 NumGC = 2
  6.  
    2019/12/19 11:48:03 Alloc = 91 TotalAlloc = 439 Just Freed = 313 Sys = 3234 NumGC = 3

Alloc代表了map占用的内存大小,这个结果表明,执行完delete后,map占用的内存并没有变小,Alloc依然是403,代表map的key和value占用的空间仍在map里.执行完map设置为nil,Alloc变为91,与刚创建的map大小基本是约等于 

引用类型Map

  1.  
    package main
  2.  
     
  3.  
    import (
  4.  
    "log"
  5.  
    "runtime"
  6.  
    )
  7.  
     
  8.  
    var intMapMap map[int]map[int]int
  9.  
     
  10.  
    var cnt = 1024
  11.  
    var lastTotalFreed uint64 // size of last memory has been freed
  12.  
     
  13.  
    func main() {
  14.  
    // 1
  15.  
    printMemStats()
  16.  
     
  17.  
    // 2
  18.  
    initMapMap()
  19.  
    runtime.GC()
  20.  
    printMemStats()
  21.  
     
  22.  
    // 3
  23.  
    fillMapMap()
  24.  
    runtime.GC()
  25.  
    printMemStats()
  26.  
     
  27.  
    // 4
  28.  
    log.Println(len(intMapMap))
  29.  
    for i := 0; i < cnt; i {
  30.  
    delete(intMapMap, i)
  31.  
    }
  32.  
    log.Println(len(intMapMap))
  33.  
    runtime.GC()
  34.  
    printMemStats()
  35.  
     
  36.  
    // 5
  37.  
    intMapMap = nil
  38.  
    runtime.GC()
  39.  
    printMemStats()
  40.  
    }
  41.  
     
  42.  
    func initMapMap() {
  43.  
    intMapMap = make(map[int]map[int]int, cnt)
  44.  
    for i := 0; i < cnt; i {
  45.  
    intMapMap[i] = make(map[int]int, cnt)
  46.  
    }
  47.  
    }
  48.  
     
  49.  
    func fillMapMap() {
  50.  
    for i := 0; i < cnt; i {
  51.  
    for j := 0; j < cnt; j {
  52.  
    intMapMap[i][j] = j
  53.  
    }
  54.  
    }
  55.  
    }
  56.  
     
  57.  
    func printMemStats() {
  58.  
    var m runtime.MemStats
  59.  
    runtime.ReadMemStats(&m)
  60.  
    log.Printf("Alloc = %v TotalAlloc = %v Just Freed = %v Sys = %v NumGC = %v\n",
  61.  
    m.Alloc/1024, m.TotalAlloc/1024, ((m.TotalAlloc-m.Alloc)-lastTotalFreed)/1024, m.Sys/1024, m.NumGC)
  62.  
     
  63.  
    lastTotalFreed = m.TotalAlloc - m.Alloc
  64.  
    }
学新通

结果如下:

  1.  
    2019/12/19 11:49:59 Alloc = 89 TotalAlloc = 89 Just Freed = 0 Sys = 1700 NumGC = 0
  2.  
    2019/12/19 11:50:00 Alloc = 41171 TotalAlloc = 41204 Just Freed = 32 Sys = 46026 NumGC = 5
  3.  
    2019/12/19 11:50:00 Alloc = 41259 TotalAlloc = 41342 Just Freed = 49 Sys = 46026 NumGC = 6
  4.  
    2019/12/19 11:50:00 1024
  5.  
    2019/12/19 11:50:00 0
  6.  
    2019/12/19 11:50:00 Alloc = 132 TotalAlloc = 41343 Just Freed = 41129 Sys = 46026 NumGC = 7
  7.  
    2019/12/19 11:50:00 Alloc = 91 TotalAlloc = 41344 Just Freed = 41 Sys = 46026 NumGC = 8

这个结果表明,在执行完delete后,顶层map占用的内存从41259降到了132,子层map占用的空间肯定是被GC回收了,不然占用内存不会下降这么显著。但依然比初始化的顶层map占用的内存89多出不少,那是因为delete操作,顶层map的key占用的空间依然在map里,当把顶层map设置为nil时,大小变为91吗,顶层map占用那些空间被释放了.

不能作为Map key 的类型

  • slices
  • maps
  • functions

Map实现两种 get 操作

Go 语言中读取 map 有两种语法:带 comma 和 不带 comma。当要查询的 key 不在 map 里,带 comma 的用法会返回一个 bool 型变量提示 key 是否在 map 中;而不带 comma 的语句则会返回一个 key 对应 value 类型的零值。如果 value 是 int 型就会返回 0,如果 value 是 string 类型,就会返回空字符串

  1.  
    func main() {
  2.  
    ageMap := make(map[string]int)
  3.  
    ageMap["qcrao"] = 18
  4.  
     
  5.  
    // 不带 comma 用法
  6.  
    age1 := ageMap["stefno"]
  7.  
    fmt.Println(age1)
  8.  
     
  9.  
    // 带 comma 用法
  10.  
    age2, ok := ageMap["stefno"]
  11.  
    fmt.Println(age2, ok)
  12.  
    }

Map为什么无序

  • 底层数据 哈希表,本就是无序的,
    • 正常写入(非哈希冲突写入):是hash到某一个bucket上,而不是按buckets顺序写入
    • 哈希冲突写入:如果存在hash冲突,会写到同一个bucket上。
  • range遍历的时候随机一个位置开始

Map扩容机制

  • 成倍扩容: (元素数量/bucket数量) > 6.5时触发成倍扩容,元素顺序变化
  • 等量扩容:溢出桶的数量大于等于 2*B时 触发等量扩容,不会改变元素顺序

Map顺序输出

Golang中map的遍历输出的时候是无序的,不同的遍历会有不同的输出结果,如果想要顺序输出的话,需要额外保存顺序,例如使用slice,将slice中排序,再通过slice的顺序去读取。

  1.  
    package main
  2.  
     
  3.  
    import (
  4.  
    "fmt"
  5.  
    "sort"
  6.  
    )
  7.  
     
  8.  
    func sortMap(testMap map[string]string) {
  9.  
    var testSlice []string
  10.  
    for key, value := range testMap {
  11.  
    testSlice = append(testSlice, key)
  12.  
    fmt.Println(key, ":", value)
  13.  
    }
  14.  
     
  15.  
    /* 对slice数组进行排序,然后就可以根据key值顺序读取map */
  16.  
    sort.Strings(testSlice)
  17.  
    fmt.Println("排序输出:")
  18.  
    for _, Key := range testSlice {
  19.  
    /* 按顺序从MAP中取值输出 */
  20.  
    fmt.Println(Key, ":", testMap[Key])
  21.  
    }
  22.  
    }
  23.  
     
  24.  
    func main() {
  25.  
    /* 声明索引类型为字符串的map */
  26.  
    var testMap = make(map[string]string)
  27.  
    testMap["Bda"] = "B"
  28.  
    testMap["Ada"] = "A"
  29.  
    testMap["Dda"] = "D"
  30.  
    testMap["Cda"] = "C"
  31.  
    testMap["Eda"] = "E"
  32.  
    sortMap(testMap)
  33.  
    }
学新通

for range陷阱

关键字range可用于循环,类似迭代器操作,它可以遍历slice,array,string,mapchannel,然后返回索引或值。

  • 1. 只有一个返回值时,则第一个参数是index;
  • 2. 遍历 map 为随机序输出,slice 为索引序输出;
  • 3. range v 是值拷贝,且只会声明初始化一次
    1.  
      func main() {
    2.  
      mySlice := []string{"I", "am", "peachesTao"}
    3.  
      fmt.Printf("遍历前首元素内存地址:%p\n", &mySlice[0])
    4.  
      for _, ele := range mySlice {
    5.  
      ele = ele "-new"
    6.  
      fmt.Printf("遍历中元素内存地址:%p\n", &ele)
    7.  
      }
    8.  
      fmt.Println(mySlice)
    9.  
      }
    10.  
      //遍历前首元素内存地址:0xc000070480
    11.  
      //遍历中元素内存地址:0xc00003a230
    12.  
      //遍历中元素内存地址:0xc00003a230
    13.  
      //遍历中元素内存地址:0xc00003a230
    14.  
      //[I am peachesTao]
  1.  
    func main() {
  2.  
    slice := []int{0, 1, 2, 3}
  3.  
    m := make(map[int]*int)
  4.  
    for key, val := range slice {
  5.  
    m[key] = &val
  6.  
    }
  7.  
    for k, v := range m {
  8.  
    fmt.Println(k, "->", *v)
  9.  
    }
  10.  
    }

答案:

  1.  
    0 -> 3
  2.  
    1 -> 3
  3.  
    2 -> 3
  4.  
    3 -> 3

参考解析:这是新手常会犯的错误写法,for range 循环的时候会创建每个元素的副本,而不是元素的引用,所以 m[key] = &val 取的都是变量 val 的地址,所以最后 map 中的所有元素的值都是变量 val 的地址,因为最后 val 被赋值为3,所有输出都是3.

  1.  
    func main() {
  2.  
    var m = [...]int{1, 2, 3}
  3.  
     
  4.  
    for i, v := range m {
  5.  
    go func() {
  6.  
    fmt.Println(i, v)
  7.  
    }()
  8.  
    }
  9.  
     
  10.  
    time.Sleep(time.Second * 1)
  11.  
    }

答案及解析:

  1.  
    2 3
  2.  
    2 3
  3.  
    2 3

for range 使用短变量声明(:=)的形式迭代变量,需要注意的是,变量 i、v 在每次循环体中都会被重用,而不是重新声明。各个 goroutine 中输出的 i、v 值都是 for range 循环结束后的 i、v 最终值,而不是各个goroutine启动时的i, v值。可以理解为闭包引用,使用的是上下文环境的值。

闭包换成函数传递

  1.  
    for i, v := range m {
  2.  
    go func(i,v int) {
  3.  
    fmt.Println(i, v)
  4.  
    }(i,v)
  5.  
    }

下面代码输出什么

  1.  
    func main() {
  2.  
    var a = []int{1, 2, 3, 4, 5}
  3.  
    var r = make([]int, 0)
  4.  
     
  5.  
    for i, v := range a {
  6.  
    if i == 0 {
  7.  
    a = append(a, 6, 7)
  8.  
    fmt.Println(a)
  9.  
    }
  10.  
     
  11.  
    r = append(r, v)
  12.  
    }
  13.  
     
  14.  
    fmt.Println(r)
  15.  
    }
学新通

参考答案及解析:[1 2 3 4 5]。a 在 for range 过程中增加了两个元素,len 由 5 增加到 7,但 for range 时会使用 a 的副本 a’ 参与循环,副本的 len 依旧是 5,因此 for range 只会循环 5 次,也就只获取 a 对应的底层数组的前 5 个元素。

并发缺陷

Go语言原生的Map非并发安全的, 在多并发的情况下,如果有写的操作,会出现Panic,提示concurrent map writes的错误

  1.  
    func main() {
  2.  
    mm := map[int]int{}
  3.  
    for i := 0; i < 21; i {
  4.  
    go func() { mm[1] = 1 }()
  5.  
    }
  6.  
    }

另外如果多线程同时 read 和 write ,或者删除 key,还会出现 fatal error: concurrent map read and map write,这都是 map 存在的并发问题。

sync.RWMutex包实现

  1.  
    type Demo struct {
  2.  
    Data map[string]string
  3.  
    Lock sync.RWMutex
  4.  
    }
  5.  
     
  6.  
    func (d Demo) Get(k string) string{
  7.  
    d.Lock.RLock()
  8.  
    defer d.Lock.RUnlock()
  9.  
    return d.Data[k]
  10.  
    }
  11.  
     
  12.  
    func (d Demo) Set(k,v string) {
  13.  
    d.Lock.Lock()
  14.  
    defer d.Lock.Unlock()
  15.  
    d.Data[k]=v
  16.  
    }
  17.  
     
  18.  
    func main() {
  19.  
    mapInfo := make(map[int]string)
  20.  
    mutex := sync.RWMutex{}
  21.  
     
  22.  
    // 使用for循环模拟多个请求对map进行写操作。
  23.  
    for i := 0; i < 10000; i {
  24.  
    mutex.Lock()
  25.  
    go func(index int, mapInfo map[int]string) {
  26.  
    mapInfo[index] = "demo"
  27.  
    mutex.Unlock()
  28.  
    }(i, mapInfo)
  29.  
    }
  30.  
     
  31.  
    fmt.Println(len(mapInfo))
  32.  
     
  33.  
    // 正常写法
  34.  
    //mapInfo := make(map[int]string)
  35.  
    //mutex := sync.RWMutex{}
  36.  
    //mutex.Lock()
  37.  
    //mapInfo[0] = "demo"
  38.  
    //mutex.Unlock()
  39.  
    }
学新通

sync.map包实现

官方在新版本中推荐使用sync.Map来实现并发写入操作。go1.9之后诞生了sync.Map。sync.Map思路来自java的ConcurrentHashMap。sync.map就是1.9版本带的线程安全map,主要有如下几种方法

  1.  
    Load(key interface{}) (value interface{}, ok bool)
  2.  
    //通过提供一个键key,查找对应的值value,如果不存在,则返回nil。ok的结果表示是否在map中找到值
  3.  
     
  4.  
    Store(key, value interface{})
  5.  
    //这个相当于是写map(更新或新增),第一个参数是key,第二个参数是value
  6.  
     
  7.  
    LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
  8.  
    //通过提供一个键key,查找对应的值value,如果存在返回键的现有值,否则存储并返回给定的值,如果是读取则返回true,如果是存储返回false
  9.  
     
  10.  
    Delete(key interface{})
  11.  
    //通过提供一个键key,删除键对应的值
  12.  
     
  13.  
    Range(f func(key, value interface{}) bool)
  14.  
    //循环读取map中的值。
  15.  
    //因为for ... range map是内置的语言特性,所以没有办法使用for range遍历sync.Map, 但是可以使用它的Range方法,通过回调的方式遍
学新通
  1.  
    var sy sync.Map
  2.  
     
  3.  
    func main() {
  4.  
    sy.Store("name", "tom")
  5.  
     
  6.  
    sy.Range(func(key, value interface{}) bool {
  7.  
    fmt.Println(key, value)
  8.  
    return false
  9.  
    })
  10.  
    }

sync.Map核心思想是减少锁,使用空间换取时间。该包实现如下几个优化点:

  1. 空间换时间。通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。

  2. 使用只读数据(read),避免读写冲突。

  3. 动态调整,miss次数多了之后,将dirty数据提升为read。

  4. double-checking。

  5. 延迟删除。删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。

  6. 优先从read读取、更新、删除,因为对read的读取不需要锁。

无法对 map 的 key 或 value 进行取址

  1.  
    package main
  2.  
     
  3.  
    import "fmt"
  4.  
     
  5.  
    func main() {
  6.  
    m := make(map[string]int)
  7.  
     
  8.  
    fmt.Println(&m["qcrao"])
  9.  
    }

cannot assign to struct field list["student"].Name in map

  1.  
    package main
  2.  
     
  3.  
    import "fmt"
  4.  
     
  5.  
    type Student struct {
  6.  
    Name string
  7.  
    }
  8.  
     
  9.  
    var list map[string]Student
  10.  
     
  11.  
    func main() {
  12.  
     
  13.  
    list = make(map[string]Student)
  14.  
     
  15.  
    student := Student{"Aceld"}
  16.  
     
  17.  
    list["student"] = student
  18.  
    list["student"].Name = "LDB"
  19.  
     
  20.  
    fmt.Println(list["student"])
  21.  
    }
学新通

分析

map[string]Student 的 value 是一个 Student 结构值,所以当list["student"] = student,是一个值拷贝过程。而list["student"]则是一个值引用。那么值引用的特点是只读。所以对list["student"].Name = "LDB"的修改是不允许的。

  1.  
    package main
  2.  
     
  3.  
    import "fmt"
  4.  
     
  5.  
    type Student struct {
  6.  
    Name string
  7.  
    }
  8.  
     
  9.  
    var list map[string]*Student
  10.  
     
  11.  
    func main() {
  12.  
     
  13.  
    list = make(map[string]*Student)
  14.  
     
  15.  
    student := Student{"Aceld"}
  16.  
     
  17.  
    list["student"] = &student
  18.  
    list["student"].Name = "LDB"
  19.  
     
  20.  
    fmt.Println(list["student"])
  21.  
    }
学新通

指向的 Student 是可以随便修改的

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgcbjhf
系列文章
更多 icon
同类精品
更多 icon
继续加载