删除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
,map
和channel
,然后返回索引或值。
- 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核心思想是减少锁,使用空间换取时间
。该包实现如下几个优化点:
-
空间换时间。通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
-
使用只读数据(read),避免读写冲突。
-
动态调整,miss次数多了之后,将dirty数据提升为read。
-
double-checking。
-
延迟删除。删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。
-
优先从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 是可以随便修改的