Golang中的map类型

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

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

值类型Map

package main

import (
	"log"
	"runtime"
)
​
var lastTotalFreed uint64
var intMap map[int]int
var cnt = 8192

func main() {
	printMemStats()

	initMap()
	runtime.GC()
	printMemStats()

	log.Println(len(intMap))
	for i := 0; i < cnt; i++ {
		delete(intMap, i)
	}
	log.Println(len(intMap))

	runtime.GC()
	printMemStats()

	intMap = nil
	runtime.GC()
	printMemStats()
}

func initMap() {
	intMap = make(map[int]int, cnt)

	for i := 0; i < cnt; i++ {
		intMap[i] = i
	}
}

func printMemStats() {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	log.Printf("Alloc = %v TotalAlloc = %v  Just Freed = %v Sys = %v NumGC = %v\n",
		m.Alloc/1024, m.TotalAlloc/1024, ((m.TotalAlloc-m.Alloc)-lastTotalFreed)/1024, m.Sys/1024, m.NumGC)

	lastTotalFreed = m.TotalAlloc - m.Alloc
}

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

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

结果如下: 

2019/12/19 11:48:03 Alloc = 89 TotalAlloc = 89  Just Freed = 0 Sys = 1700 NumGC = 0
2019/12/19 11:48:03 Alloc = 403 TotalAlloc = 437  Just Freed = 33 Sys = 3234 NumGC = 1
2019/12/19 11:48:03 8192
2019/12/19 11:48:03 0
2019/12/19 11:48:03 Alloc = 404 TotalAlloc = 438  Just Freed = 1 Sys = 3234 NumGC = 2
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

package main

import (
	"log"
	"runtime"
)

var intMapMap map[int]map[int]int

var cnt = 1024
var lastTotalFreed uint64 // size of last memory has been freed

func main() {
	// 1
	printMemStats()

	// 2
	initMapMap()
	runtime.GC()
	printMemStats()

	// 3
	fillMapMap()
	runtime.GC()
	printMemStats()

	// 4
	log.Println(len(intMapMap))
	for i := 0; i < cnt; i++ {
		delete(intMapMap, i)
	}
	log.Println(len(intMapMap))
	runtime.GC()
	printMemStats()

	// 5
	intMapMap = nil
	runtime.GC()
	printMemStats()
}

func initMapMap() {
	intMapMap = make(map[int]map[int]int, cnt)
	for i := 0; i < cnt; i++ {
		intMapMap[i] = make(map[int]int, cnt)
	}
}

func fillMapMap() {
	for i := 0; i < cnt; i++ {
		for j := 0; j < cnt; j++ {
			intMapMap[i][j] = j
		}
	}
}

func printMemStats() {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	log.Printf("Alloc = %v TotalAlloc = %v  Just Freed = %v Sys = %v NumGC = %v\n",
		m.Alloc/1024, m.TotalAlloc/1024, ((m.TotalAlloc-m.Alloc)-lastTotalFreed)/1024, m.Sys/1024, m.NumGC)

	lastTotalFreed = m.TotalAlloc - m.Alloc
}

结果如下:

2019/12/19 11:49:59 Alloc = 89 TotalAlloc = 89  Just Freed = 0 Sys = 1700 NumGC = 0
2019/12/19 11:50:00 Alloc = 41171 TotalAlloc = 41204  Just Freed = 32 Sys = 46026 NumGC = 5
2019/12/19 11:50:00 Alloc = 41259 TotalAlloc = 41342  Just Freed = 49 Sys = 46026 NumGC = 6
2019/12/19 11:50:00 1024
2019/12/19 11:50:00 0
2019/12/19 11:50:00 Alloc = 132 TotalAlloc = 41343  Just Freed = 41129 Sys = 46026 NumGC = 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 类型,就会返回空字符串

func main() {
	ageMap := make(map[string]int)
	ageMap["qcrao"] = 18

	// 不带 comma 用法
	age1 := ageMap["stefno"]
	fmt.Println(age1)

	// 带 comma 用法
	age2, ok := ageMap["stefno"]
	fmt.Println(age2, ok)
}

Map为什么无序

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

Map扩容机制

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

Map顺序输出

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

package main

import (
	"fmt"
	"sort"
)

func sortMap(testMap map[string]string) {
	var testSlice []string
	for key, value := range testMap {
		testSlice = append(testSlice, key)
		fmt.Println(key, ":", value)
	}

	/* 对slice数组进行排序,然后就可以根据key值顺序读取map */
	sort.Strings(testSlice)
	fmt.Println("排序输出:")
	for _, Key := range testSlice {
		/* 按顺序从MAP中取值输出 */
		fmt.Println(Key, ":", testMap[Key])
	}
}

func main() {
	/* 声明索引类型为字符串的map */
	var testMap = make(map[string]string)
	testMap["Bda"] = "B"
	testMap["Ada"] = "A"
	testMap["Dda"] = "D"
	testMap["Cda"] = "C"
	testMap["Eda"] = "E"
	sortMap(testMap)
}

for range陷阱

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

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

答案:

0 -> 3
1 -> 3
2 -> 3
3 -> 3

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

func main() {
	var m = [...]int{1, 2, 3}

	for i, v := range m {
		go func() {
			fmt.Println(i, v)
		}()
	}

	time.Sleep(time.Second * 1)
}

答案及解析:

2 3
2 3
2 3

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

闭包换成函数传递

for i, v := range m {
	go func(i,v int) {
		fmt.Println(i, v)
	}(i,v)
}

下面代码输出什么

func main() {
	var a = []int{1, 2, 3, 4, 5}
	var r = make([]int, 0)

	for i, v := range a {
		if i == 0 {
			a = append(a, 6, 7)
			fmt.Println(a)
		}

		r = append(r, v)
	}

	fmt.Println(r)
}

参考答案及解析:[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的错误

func main() {
	mm := map[int]int{}
	for i := 0; i < 21; i++ {
		go func() { mm[1] = 1 }()
	}
}

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

sync.RWMutex包实现

type Demo struct {
    Data map[string]string
    Lock sync.RWMutex
}
 
func (d Demo) Get(k string) string{
    d.Lock.RLock()
    defer d.Lock.RUnlock()
    return d.Data[k]
}
 
func (d Demo) Set(k,v string) {
    d.Lock.Lock()
    defer d.Lock.Unlock()
    d.Data[k]=v
}

func main() {
	mapInfo := make(map[int]string)
	mutex := sync.RWMutex{}

	// 使用for循环模拟多个请求对map进行写操作。
	for i := 0; i < 10000; i++ {
		mutex.Lock()
		go func(index int, mapInfo map[int]string) {
			mapInfo[index] = "demo"
			mutex.Unlock()
		}(i, mapInfo)
	}

	fmt.Println(len(mapInfo))

	// 正常写法
	//mapInfo := make(map[int]string)
	//mutex := sync.RWMutex{}
	//mutex.Lock()
	//mapInfo[0] = "demo"
	//mutex.Unlock()
}

sync.map包实现

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

Load(key interface{}) (value interface{}, ok bool)
//通过提供一个键key,查找对应的值value,如果不存在,则返回nil。ok的结果表示是否在map中找到值

Store(key, value interface{})
//这个相当于是写map(更新或新增),第一个参数是key,第二个参数是value

LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
//通过提供一个键key,查找对应的值value,如果存在返回键的现有值,否则存储并返回给定的值,如果是读取则返回true,如果是存储返回false

Delete(key interface{})
//通过提供一个键key,删除键对应的值

Range(f func(key, value interface{}) bool)
//循环读取map中的值。
//因为for ... range map是内置的语言特性,所以没有办法使用for range遍历sync.Map, 但是可以使用它的Range方法,通过回调的方式遍
var sy sync.Map

func main() {
	sy.Store("name", "tom")

	sy.Range(func(key, value interface{}) bool {
		fmt.Println(key, value)
		return false
	})
}

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

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

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

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

  4. double-checking。

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

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

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

package main

import "fmt"

func main() {
	m := make(map[string]int)

	fmt.Println(&m["qcrao"])
}

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

package main

import "fmt"

type Student struct {
	Name string
}

var list map[string]Student

func main() {

	list = make(map[string]Student)

	student := Student{"Aceld"}

	list["student"] = student
	list["student"].Name = "LDB"

	fmt.Println(list["student"])
}

分析

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

package main

import "fmt"

type Student struct {
	Name string
}

var list map[string]*Student

func main() {

	list = make(map[string]*Student)

	student := Student{"Aceld"}

	list["student"] = &student
	list["student"].Name = "LDB"

	fmt.Println(list["student"])
}

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值