Golang中的map类型

本文探讨了Go语言中Map的内存管理,包括删除键值后内存是否释放,以及不同类型的值在Map中删除后的表现。此外,还讨论了Map的并发安全问题,指出在并发环境下直接操作Map可能导致的错误,并介绍了如何使用`sync.RWMutex`和`sync.Map`解决并发问题。最后,文章提到了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 是可以随便修改的

Golang 中,`map` 是一种非常常用的数据结构,用于存储键值对。关于 `map` 的键(key)类型Golang 有明确的限制和规范。 ### Key 类型要求 Golang 中的 `map` 允许的 key 类型必须是可以进行比较的类型,因为 `map` 需要能够判断两个 key 是否相等。具体来说,以下类型的值可以作为 `map` 的 key: 1. **基本类型**:如 `int`、`string`、`bool`、`float32`、`float64` 等。 2. **指针类型**:指向任何类型的指针都可以作为 key。 3. **接口类型**:只要接口的动态类型是可比较的,就可以作为 key。 4. **数组类型**:数组的元素类型必须是可比较的,且数组长度固定。 5. **结构体类型**:结构体的所有字段都必须是可比较的类型。 6. **复合类型**:如 `slice` 和 `map` 本身不能作为 key,因为它们不可比较。 ### 不允许的 Key 类型 以下类型不能作为 `map` 的 key,因为它们无法进行比较: - `slice` - `map` - `func` ### 示例代码 ```go package main import "fmt" func main() { // 合法的 key 类型示例 m1 := map[string]int{"a": 1, "b": 2} fmt.Println("m1:", m1) m2 := map[int]bool{1: true, 2: false} fmt.Println("m2:", m2) m3 := map[[2]int]string{{1, 2}: "array key"} fmt.Println("m3:", m3) // 不合法的 key 类型示例(会导致编译错误) // m4 := map[[]int]string{{1, 2}: "slice key"} // 编译错误 // m5 := map[map[int]int]string{{1: 2}: "nested map key"} // 编译错误 } ``` ### 总结 Golang 对 `map` 的 key 类型进行了严格的限制,确保 key 是可比较的,从而保证 `map` 操作的正确性和高效性。开发者在使用 `map` 时需要注意这些规则,以避免编译错误或运行时异常。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值