教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 golang中map的一些注意事项

golang中map的一些注意事项

发布时间:2022-02-12   编辑:jiaochengji.com
教程集为您提供golang中map的一些注意事项等资源,欢迎您收藏本站,我们将为您提供最新的golang中map的一些注意事项资源
<h3 id="map">map</h3> <ul><li>类似其它语言中的哈希表或字典,以key-value形式存储数据</li><li><span style="color:#f33b45;">key必须是支持==或!=比较运算的类型,不可以是函数、map或slice</span></li><li>Map通过key查找value比线性搜索快很多</li><li>

Map使用make()创建,支持:=这种简写方式

</li><li>make([keyType]valueType,cap),cap表示容量,可省略</li><li>超出容量时会自动扩容,但尽量提供一个合理的初始值</li><li>使用len()获取元素个数</li><li>键值对不存在时自动添加,使用delete()删除某键值对</li><li>

使用for range对map和slice进行迭代

</li></ul><h3>map的声明与默认值</h3> <pre class="has"><code class="language-Go">// 声明 var m map[string]string // bool 的零值是false var m map[int]bool a, ok := m[1] fmt.Println(a, ok) // false false // int 的零值是0 var m map[int]int a, ok := m[1] fmt.Println(a, ok) // 0 false </code></pre>

map的声明的时候默认值是nil ,此时进行取值,返回的是对应类型的零值(不存在也是返回零值)

<pre class="has"><code class="language-Go">// 先声明map var m1 map[string]string // 再使用make函数创建一个非nil的map,nil map不能赋值 m1 = make(map[string]string) // 最后给已声明的map赋值 m1["a"] = "aa" m1["b"] = "bb" // 直接创建 m2 := make(map[string]string) // 然后赋值 m2["a"] = "aa" m2["b"] = "bb" // 初始化 赋值一体化 m3 := map[string]string{ "a": "aa", "b": "bb", } // ========================================== // 查找键值是否存在 if v, ok := m1["a"]; ok { fmt.Println(v) } else { fmt.Println("Key Not Found") } // 遍历map for k, v := range m1 { fmt.Println(k, v) } m := make(map[interface{} ]interface{}) m[1] = 56 m["str"] = "dfsdf" fmt.Println(m)</code></pre> <ol><li>

map数据类型初始化:

两种方式:map[string]string{}或make(map[string]string)

</li><li>

未初始化的map是nil:

未初始化的map是nil,它与一个空map基本等价,只是nil的map不允许往里面添加值。(A nil map is equivalent to an empty map except that no elements may be added) 
因此,map是nil时,取值是不会报错的(取不到而已),但增加值会报错。 
其实,还有一个区别,delete一个nil map会panic,但是delete 空map是一个空操作(并不会panic)(这个区别在最新的Go tips中已经没有了,即:delete一个nil map也不会panic)

</li><li>

通过fmt打印map时,空map和nil map结果是一样的:

通过fmt打印map时,空map和nil map结果是一样的,都为map[]。所以,这个时候别断定map是空还是nil,而应该通过map == nil来判断。 
Request中的Form字段就是如此,在没有直接或间接调用ParseForm()时,Form其实是nil,但是,你如果println出来,却是map[],可能有些困惑。通过跟踪源码可以发现,Form根本没有初始化。而在FormValue()方法中会判断Form是否为nil,然后决定是否调用ParseForm()方法,当然,你也可以手动调用ParseForm()方法。

</li></ol><pre class="has"><code class="language-Go">var m1 map[string]int m1 = make(map[string]int) m2 := make(map[string]int) fmt.Println(m1,m2) m1["chen"] = 88888 m2["chen"] = 88888 fmt.Println(m1,m2) fmt.Println(reflect.DeepEqual(m1, m2))</code></pre>

若只有var m1 map[string]int 无m1 = make(map[string]int),无法向map中添加数据

<pre class="has"><code class="language-Go">var m1 map[string]int m1["chen"] = 88888 fmt.Println(m1) </code></pre>

出现panic     <span style="color:#f33b45;">panic: assignment to entry in nil map</span>

<h3>map的初始化</h3> <pre class="has"><code class="language-Go">// 声明之后必须初始化,才能使用 m = make(map[string]int) m = map[string]int{} // 声明并初始化 <= 注意这里是 := 不是 = m := make(map[string]int) m := map[string]int{1:1} </code></pre>

向未初始化的map赋值引起 panic: assign to entry in nil map.

注意:golang中的map,的<span style="color:#f33b45;"> key 可以是很多种类型,比如 bool, 数字,string, 指针, channel</span> , 还有 只包含前面几个类型的 interface types, structs, arrays,显然,<span style="color:#f33b45;">slice, map 还有 function 是不可以了,因为这几个没法用 == 来判断。</span>

如果是非法的key类型,会报错:invalid map key type xxx

几种类型的比较:

<pre class="has"><code class="language-Go">arr1 := []int{1,2,3,4} arr2 := []int{1,2,3,4}</code></pre>

切片不可以arr1 == arr2,会报错<span style="color:#f33b45;">invalid operation: arr1 == arr2 (slice can only be compared to nil)</span>

<span style="color:#f33b45;">切片只可以与nil比较,判断是否为nil,不可以直接用“==”比较,但可以借助于reflect.DeepEqual(arr1, arr2)比较,返回true或false,此外map也可以通过reflect.DeepEqual(m1, m2)比较</span>

结构体比较

不同结构的结构体不可以比较,但同一类型的实例值是可以比较的。

<span style="color:#f33b45;">两个 struct完全相等, 意味着里面的所有变量的值都完全相等</span>

<pre class="has"><code class="language-Go">type Person struct { Name, Country string } hits := make(map[Person]int)</code></pre>

判断key是否在map中

<pre class="has"><code class="language-Go">if _, ok := map[key]; ok { //存在进行相应操作 } </code></pre> <h3>map的新增 & 删除 & 更新 & 查询</h3> <pre class="has"><code class="language-Go">// 新增 m["name"] = "wade" // 删除,key不存在则啥也不干 delete(m, "name") // 更新 m["name"] = "Tigerwolf" // 查询,key不存在返回value类型的零值 i := m["name"] // 三种查询方式, i, ok := m["name"] _, ok := m["name"] </code></pre> <h3>map的遍历</h3>

需要强调的是map本身是无序的在遍历的时候并不会按照你传入的顺序,进行传出。

正常遍历:

<pre class="has"><code class="language-Go">for k, v := range m { fmt.Println(k, v) }</code></pre>

 有序遍历(借助数组排序):

<pre class="has"><code class="language-Go">import "sort" var tempArr []string // 把key单独抽取出来,放在数组中 for k, _ := range m { tempArr = append(tempArr, k) } // 进行数组的排序 sort.Strings(tempArr) // 遍历数组就是有序的了 for _, k := range tempArr { fmt.Println(k, m[k]) }</code></pre> <h3>map作为函数传参</h3>

Golang中是没有引用传递的,均为值传递。这意味着传递的是数据的拷贝。
那么map本身是引用类型,作为形参或返回参数的时候,传递的是地址的拷贝扩容时也不会改变这个地址。

源码地址 go/src/runtime/map.go

<h3>map的底层数据结构</h3> <pre class="has"><code class="language-Go">type hmap struct { count int //元素个数 flags uint8 B uint8 //扩容常量 noverflow uint16 //溢出 bucket 个数 hash0 uint32 //hash 种子 buckets unsafe.Pointer //bucket 数组指针 oldbuckets unsafe.Pointer //扩容时旧的buckets 数组指针 nevacuate uintptr //扩容搬迁进度 extra *mapextra //记录溢出相关 } type bmap struct { tophash [bucketCnt]uint8 // Followed by bucketCnt keys //and then bucketan Cnt values // Followed by overflow pointer. } </code></pre>

说明:<span style="color:#f33b45;">每个map的底层结构是hmap,是有若干个机构为bmap的bucket组成的数组,每个bucket可以存放若干个元素(通常是8个),那么每个key会根据hash算法归到同一个bucket中,当一个bucket中的元素超过8个的时候,hmap会使用extra中的overflow来扩展存储key。</span>

来一个图,方便记忆:

<h3>实现map的关键要素</h3>

哈希函数

让哈希函数的结果能够尽可能的均匀分布,然后通过工程上的手段解决哈希碰撞的问题。
Golang中每个类型的哈希函数在程序启动后是确定的,在类型的信息中。

哈希冲突

<ol><li>线性探测法: <ul><li>数组实现</li><li>当前哈希表写入新的数据时发生了冲突,就会将键值对写入到下一个不为空的位置;</li><li>查找对应key会继续查找后面的元素,直到内存为空或者找到目标元素;</li></ul></li><li>拉链法: <ul><li>数组 链表/红黑树;</li><li>自动扩容;</li></ul></li></ol>

map的hash值计算

那么具体key是分配到哪个bucket呢?也就是bmap中的tophash是如何计算?

golang为每个类型定义了类型描述器_type,并实现了hashable类型的_type.alg.hash和_type.alg.equal

<pre class="has"><code class="language-Go">type typeAlg struct { // function for hashing objects of this type // (ptr to object, seed) -> hash hash func(unsafe.Pointer, uintptr) uintptr // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? equal func(unsafe.Pointer, unsafe.Pointer) bool</code></pre>

具体实现文件:go/1.10.3/libexec/src/runtime/hashmap.go:

<pre class="has"><code class="language-Go">// tophash calculates the tophash value for hash. func tophash(hash uintptr) uint8 { top := uint8(hash >> (sys.PtrSize*8 - 8)) if top < minTopHash { top = minTopHash } return top }</code></pre>

map的查找

具体实现文件:go/1.10.3/libexec/src/runtime/hashmap.go:

<pre class="has"><code class="language-Go">// mapaccess1 returns a pointer to h[key]. Never returns nil, instead // it will return a reference to the zero object for the value type if // the key is not in the map. // NOTE: The returned pointer may keep the whole map live, so don't // hold onto it for very long. func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { ... // 并发访问检查 if h.flags&hashWriting != 0 { throw("concurrent map read and map write") } // 计算key的hash值 alg := t.key.alg hash := alg.hash(key, uintptr(h.hash0)) // alg.hash // hash值对m取余数得到对应的bucket m := uintptr(1)<<h.B - 1 b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize))) // 如果老的bucket还没有迁移,则在老的bucket里面找 if c := h.oldbuckets; c != nil { if !h.sameSizeGrow() { m >>= 1 } oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize))) if !evacuated(oldb) { b = oldb } } // 计算tophash,取高8位 top := uint8(hash >> (sys.PtrSize*8 - 8)) for { for i := uintptr(0); i < bucketCnt; i { // 检查top值,如高8位不一样就找下一个 if b.tophash[i] != top { continue } // 取key的地址 k := add(unsafe.Pointer(b), dataOffset i*uintptr(t.keysize)) if alg.equal(key, k) { // alg.equal // 取value得地址 v := add(unsafe.Pointer(b), dataOffset bucketCnt*uintptr(t.keysize) i*uintptr(t.valuesize)) } } // 如果当前bucket没有找到,则找bucket链的下一个bucket b = b.overflow(t) if b == nil { // 返回零值 return unsafe.Pointer(&zeroVal[0]) } } }</code></pre>

map的更新/插入过程

<pre class="has"><code class="language-Go">// Like mapaccess, but allocates a slot for the key if it is not present in the map. func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { // 如果已经达到了load factor的最大值,那我们就继续开始扩容 if !h.growing() && (overLoadFactor(int64(h.count), h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) { hashGrow(t, h) goto again } if inserti == nil { // burrent满了的话,需要申请一个新的 newb := h.newoverflow(t, b) inserti = &newb.tophash[0] insertk = add(unsafe.Pointer(newb), dataOffset) val = add(insertk, bucketCnt*uintptr(t.keysize)) } // 在插入的位置,存储键值 if t.indirectkey { kmem := newobject(t.key) *(*unsafe.Pointer)(insertk) = kmem insertk = kmem } if t.indirectvalue { vmem := newobject(t.elem) *(*unsafe.Pointer)(val) = vmem } typedmemmove(t.key, insertk, key) *inserti = top h.count }</code></pre>

map的删除过程

<pre class="has"><code class="language-Go">func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) { // 找key 。。。 // 若找到把对应的tophash里面的打上空的标记 b.tophash[i] = empty h.count-- }</code></pre>

map的扩容机制

map判断扩容的函数:

<pre class="has"><code class="language-Go">// overLoadFactor reports whether count items placed in 1<<B buckets is over loadFactor. func overLoadFactor(count int, B uint8) bool { // 注意这里有一个loadFactorNum/loadFactorDen return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen) } func bucketShift(b uint8) uintptr { if sys.GoarchAmd64|sys.GoarchAmd64p32|sys.Goarch386 != 0 { b &= sys.PtrSize*8 - 1 // help x86 archs remove shift overflow checks } return uintptr(1) << b }</code></pre>

每次map进行更新或者新增的时候,会先通过以上函数判断一下load factor。来决定是否扩容。

    扩容白话文:如果之前为2^n ,那么下一次扩容是2^(n 1),每次扩容都是之前的两倍。扩容后需要重新计算每一项在hash中的位置,新表为老的两倍,此时前文的oldbacket用上了,用来存同时存在的两个心就map,等数据迁移完毕就可以释放oldbacket了

好处:均摊扩容时间,一定程度上缩短了扩容时间(是不是和gc的引用计数法有点像,都是均摊~)

那么overLoadFactor函数中有一个常量6.5(loadFactorNum/loadFactorDen)来进行影响扩容时机。这个值的来源是测试取中的结果。

<blockquote>

  loadFactor    %overflow  bytes/entry     hitprobe    missprobe
        4.00         2.13        20.77         3.00         4.00
        4.50         4.05        17.30         3.25         4.50
        5.00         6.85        14.77         3.50         5.00
        5.50        10.55        12.94         3.75         5.50
        6.00        15.27        11.67         4.00         6.00
        6.50        20.90        10.79         4.25         6.50
        7.00        27.14        10.15         4.50         7.00
        7.50        34.03         9.73         4.75         7.50
        8.00        41.10         9.40         5.00         8.00

</blockquote>

字段     说明
%overflow     溢出率,平均一个bucket有多少个kv的时候会溢出
bytes/entry     平均存一个kv需要额外存储多少字节的数据
hitprobe     找到一个存在的key平均需要找几下
missprobe     找到一个不存在的key平均需要找几下

<h3>并发中的map</h3>

安全性

这里呢,实现一个小功能来证明下并发不是安全的
并发起两个goroutine,分别对map进行数据的增加
 

<pre class="has"><code class="language-Go">func main() { test := map[int]int {1:1} go func() { i := 0 for i < 10000 { test[1]=1 i } }() go func() { i := 0 for i < 10000 { test[1]=1 i } }() time.Sleep(2*time.Second) fmt.Println(test) }</code></pre>

会发现有这样的报错:

fatal error: concurrent map read and map write

根本原因就是:并发的去读写map结构的数据了。
处理方案 & 优缺点

那解决方案就是加锁。上代码

<pre class="has"><code class="language-Go">func main() { test := map[int]int {1:1} var s sync.RWMutex go func() { i := 0 for i < 10000 { s.Lock() test[1]=1 s.Unlock() i } }() go func() { i := 0 for i < 10000 { s.Lock() test[1]=1 s.Unlock() i } }() time.Sleep(2*time.Second) fmt.Println(test) }</code></pre>

    优点:实现简单粗暴,好理解
    缺点:锁的粒度为整个map,存在优化空间
    适用场景:all

官方处理方案 & 优缺点

想一想,在程序设计中,想增加运行的速度,那么必然要有另外的牺牲,很容易想到“空间换时间”的方案,现在来实战体验一把。

<pre class="has"><code class="language-Go">func main() { test := sync.Map{} test.Store(1, 1) go func() { i := 0 for i < 10000 { test.Store(1, 1) i } }() go func() { i := 0 for i < 10000 { test.Store(1, 1) i } }() time.Sleep(time.Second) fmt.Println(test.Load(1)) }</code></pre>

运行完呢,会发现,其实是不会报错的。因为sync.Map里头已经实现了一套加锁的机制,让你更方便地使用map。

sync.Map的原理介绍:sync.Map里头有两个map一个是专门用于读的read map,另一个是才是提供读写的dirty map;优先读read map,若不存在则加锁穿透读dirty map,同时记录一个未从read map读到的计数,当计数到达一定值,就将read map用dirty map进行覆盖。

    优点:是官方出的,是亲儿子;通过空间换时间的方式;读写分离;
    缺点:不适用于大量写的场景,这样会导致read map读不到数据而进一步加锁读取,同时dirty map也会一直晋升为read map,整体性能较差。
    适用场景:大量读,少量写

额外的处理机制

想一想,mysql加锁,是不是有表级锁、行级锁,前文的sync.RWMutex加锁方式相当于表级锁。

而sync.Map其实也是相当于表级锁,只不过多读写分了两个map,本质还是一样的。

既然这样,那就自然知道优化方向了:就是把锁的粒度尽可能降低来提高运行速度。

思路:对一个大map进行hash,其内部是n个小map,根据key来来hash确定在具体的那个小map中,这样加锁的粒度就变成1/n了。
网上找了下,真有大佬实现了:点这里https://github.com/orcaman/concurrent-map/blob/master/README-zh.md

<h3>map的gc回收机制</h3>

实战代码 && 处理机制

我们知道呢,map在golang里头是只增不减的一种数组结构,他只会在删除的时候进行打标记说明该内存空间已经empty了,不会回收的。

show my code,no bb:

<pre class="has"><code class="language-Go">var intMap map[int]int func main() { printMemStats("初始化") // 添加1w个map值 intMap = make(map[int]int, 10000) for i := 0; i < 10000; i { intMap[i] = i } // 手动进行gc操作 runtime.GC() // 再次查看数据 printMemStats("增加map数据后") log.Println("删除前数组长度:", len(intMap)) for i := 0; i < 10000; i { delete(intMap, i) } log.Println("删除后数组长度:", len(intMap)) // 再次进行手动GC回收 runtime.GC() printMemStats("删除map数据后") // 设置为nil进行回收 intMap = nil runtime.GC() printMemStats("设置为nil后") } func printMemStats(mag string) { var m runtime.MemStats runtime.ReadMemStats(&m) log.Printf("%v:分配的内存 = %vKB, GC的次数 = %v\n", mag, m.Alloc/1024, m.NumGC) }</code></pre>

会输出:

初始化:分配的内存 = 65KB, GC的次数 = 0
增加map数据后:分配的内存 = 381KB, GC的次数 = 1
删除前数组长度: 10000
删除后数组长度: 0
删除map数据后:分配的内存 = 381KB, GC的次数 = 2
设置为nil后:分配的内存 = 68KB, GC的次数 = 3

很明显可以看到delete是不会真正的把map释放的,所以要回收map还是需要设为nil
 

到此这篇关于“golang中map的一些注意事项”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
Golang 空map和未初始化map注意事项
Golang 中使用多维 map
golang map key 正则表达_Golang中的Map
golang中map的一些注意事项
Golang从入门到放弃200618--Map(1)Map的初始化和基本操作
golang map中结构体元素是无法取地址的
golang map
golang中map地址改变示例
基于jquery循环map功能的代码
golang 并发访问map遇到的问题

[关闭]
~ ~