go map

一、初始化

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的存储

 以后遇到其它情况再来补充。

原文地址:https://www.cnblogs.com/fanxp/p/13776887.html