一、初始化
var m1 map[int]bool //未初始化,当前未nil,这里和slice有点不一样,map必须要初始化以后才能添加数据 var m2 = make(map[int]bool) //初始化 var m3 = make(map[int]bool,5) //初始化+容量 如果这里容量小于8其实没啥意义,因为一个bmap存储8个元素(1 << 3) var m4 = map[int]bool{ //初始化+赋值 1:true, 2:false, } //底层初始化的几个函数 //func makemap_small() *hmap //func makemap64(t *maptype, hint int64, h *hmap) *hmap //func makemap(t *maptype, hint int, h *hmap) *hmap //makemap_small:当 hint 小于 8 时,会调用 makemap_small 来初始化 hmap。主要差异在于是否会马上初始化 hash table //makemap64:当 hint 类型为 int64 时的特殊转换及校验处理,后续调用 makemap //makemap:实现了标准的 map 初始化
注意:初始化时指定一个合适的容量,可以提升性能,如容量过小,新增的键值对比较多,会导致频繁分配buckets,进行扩容迁移等,这样就会造成性能下降。
二、取值
var m = map[int]bool{ 1:true, 2:false, } v1 := m[3] log.Println(v1) //输出:false v2,ok := m[3] log.Println(v2,ok) //输出:false false //底层函数 //func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer //func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool)
三、平时使用需要注意的地方
1、作为函数参数传递(make map 返回的是一个指针 *hmap)
func main(){ var m = make(map[int]bool) f(m) log.Println(m) //输出:map[1:true] } func f(m map[int]bool){ if m != nil{ m[1] = true } }
2、map非线程安全,再查询、赋值、遍历、删除的时候会检查写标志,如果发现写标志位置等于1,直接panic。
if h.flags&hashWriting != 0 { throw("concurrent map writes") }
3、map中的key是无序的
这里主要有两点造成了每次遍历的数据顺序不一样:
(1)、map在扩容以后,发生了旧bucket中的key部分迁移或全部迁移到新bucket中,这时候已经就无法还原以前的顺序了。
(2)、go故意的,开始遍历的时候不是固定0 bucket开始的,每次都会随机值的bucket开始遍历,并且从这个bucket中随机一个元素开始遍历,这样做以为为了让程序员知道不管这样遍历map的顺序都是不一样的。
var m = make(map[int]bool) for i := 0; i < 5; i++ { if i%2 == 0 { m[i] = true } else { m[i] = false } } for k, v := range m { log.Println(k, v) //每次输出可以能都不一样 } [源码] //生成随机数 r r := uintptr(fastrand()) if h.B > 31-bucketCntBits { r += uintptr(fastrand()) << 31 } // 从哪个 bucket 开始遍历 it.startBucket = r & (uintptr(1)<<h.B - 1) // 从 bucket 的哪个 cell 开始遍历 it.offset = uint8(r >> h.B & (bucketCnt - 1))
四、hmap+bmap的一些结构理解
网上对这部分都写得比较多了,这里主要看看bmap的结构,再存储k 和 v 的载体并不是用 k/v/k/v/k/v/k/v 的模式,而是 k/k/k/k/v/v/v/v 的形式去存储,我们来调试看看。
const ( // Maximum number of key/elem pairs a bucket can hold. bucketCntBits = 3 bucketCnt = 1 << bucketCntBits ) // 这些struct都是在源码中复制出来的 type hmap struct { count int // 元素个数,调用 len(map) 时,直接返回此值 flags uint8 //状态标识,主要是 goroutine 写入和扩容机制的相关状态控制。并发读写的判断条件之一就是该值 B uint8 // 桶,最大可容纳的元素数量,值为 负载因子(默认 6.5) * 2 ^ B,是 2 的指数 noverflow uint16 // 溢出桶的数量 hash0 uint32 // 哈希因子(哈希函数的算法与key的类型一一对应的。根据 key 的类型, maptype结构体的 key字段的alg 字段会被设置对应类型的 hash 和 equal 函数,go 里面有aeshash算法和memhash算法,amd64 使用aeshash算法) buckets unsafe.Pointer // 保存当前桶数据的指针地址(指向一段连续的内存地址,主要存储键值对数据) oldbuckets unsafe.Pointer // 保存旧桶的指针地址 nevacuate uintptr // 迁移进度 //extra *mapextra // 当 key 和 value 都可以 inline 的时候,就会用这个字段 } //源码中的结构,再编译的时候会在struct中增加几个字段 //type bmap struct { // tophash [bucketCnt]uint8 //} type bmap struct { topbits [bucketCnt]uint8 keys [bucketCnt]int //keytype 这里测试写的int values [bucketCnt]bool //valuetype 这里测试写的bool pad uintptr overflow uintptr } func main() { //初始化一个map var m = make(map[int]bool) //赋值 for i := 0; i < 5; i++ { if i%2 == 0 { m[i] = true } else { m[i] = false } } //强制转换获取一个hmap对象 h := **(**hmap)(unsafe.Pointer(&m)) log.Println(h.count) //输出:5 //获取第一个bucket b := (*bmap)(unsafe.Pointer(h.buckets)) log.Println(b) }
b keys 和 values的存储
以后遇到其它情况再来补充。