本文永久链接 – https://tonybai.com/2022/03/15/the-underlying-of-a-map-type-variable

切片(slice)和map是Go语言中最常用的两种原生复合数据类型,同时也是最容易使初学者感觉迷惑和“掉坑”的两个类型,这很大程度是因为Go runtime层的存在。什么是Go runtime层?可以参考我在《Go语言第一课FAQ》中的解释。

我们在Go用户源码层看到的切片与map是这样的:

var sl = make([]int, 3)
var m = make(map[string]int)

但在runtime层,它们又是另一幅“样子”。Go用户源码层的切片和map类型的变量,我常将它们称为“描述符”,因为它们和linux平台上通过open系统调用打开的文件描述符的功用十分类似,都是某个大块头儿数据(比如:一个500M的文本数据)的“代言人”,避免了和外界交互时对底层数据的搬动与拷贝。

很多人知道,在runtime层,切片是一个三元组结构(在我的“Go语言第一课”专栏中有单独一讲详细讲解),这里假定这个三元组结构为T,那么上面例子中通过make创建的切片m是类型T的实例还是*T的实例呢?很多人都知道答案:类型T的实例

同样看过我的专栏《Go语言精进之路》一书的读者也都知道:map类型在runtime层的表示为runtime.hmap,那么,上面通过make创建的map[string]int类型变量m究竟就是hmap类型实例还是*hmap类型实例呢?可能有些朋友还不明确,这里我们就来简单探究一下。

注:探究方法同样适用于切片类型。

m是hmap类型实例还是*hmap类型实例呢?最直接的方法是看runtime包的源码。在runtime/map.go中,我们找到了对应make(map[string]int)的源码makemap(或makemap_small):

func makemap(t *maptype, hint int, h *hmap) *hmap
func makemap_small() *hmap

我们看到:无论哪个函数返回的都是*hmap类型。到这里你的心里似乎有点倾向了,应该是*hmap。但还不那么确认。

我们假设m是*hmap,那么根据Go指针类型的定义(关于Go指针,我在专栏《聊聊Go语言中的指针》一讲中有较为全面讲解),Go为变量m分配的内存块中存储的值就应该是一个hmap实例的地址:

也就是说给m分配一块可以存储指针值的内存块儿即可。这样我们就可以通过相邻变量间的地址间隔来判定m是否仅仅是一个指针大小的内存块了。我们看下面例子:

package main

func main() {
    var a int = 5
    println("&a=", &a)
    var m1 = make(map[string]int)
    println("&m1=", &m1)
    var m2 = make(map[string]int)
    println("&m2=", &m2)
}

运行这个程序,输出结果如下:

&a=  0xc000046558
&m1= 0xc000046568
&m2= 0xc000046560

由于这些变量都分配在栈上(通过go build -gcflags ‘-m’可判断是否逃逸),我们用一幅图来展示一下上面示例中各个变量的内存块排列情况:

从m1与m2两个map类型变量的地址间隔情况来看,间隔8个字节,也就是一个指针大小,基本可以断定m2是指针类型实例了。

那么m是否是*hmap类型实例呢?如果是,我们是否可以通过对m的“解引用”得到该实例的值呢?我们下面试一下。

由于hmap是runtime包的非导出类型,所以我们无法在用户层直接使用,考虑到hmap都是由一些基本类型字段组成并且与runtime包的其他类型关联不多,我这里直接将其相关源码copy到示例源码中备用了。

package main

import (
    "fmt"
    "runtime"
    "unsafe"
)

type hmap struct {
    count     int // # live cells == size of map.  Must be first (used by len() builtin)
    flags     uint8
    B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
    noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
    hash0     uint32 // hash seed

    buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
    oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
    nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

    extra *mapextra // optional fields
}

// mapextra holds fields that are not present on all maps.
type mapextra struct {
    overflow    *[]*bmap
    oldoverflow *[]*bmap

    nextOverflow *bmap
}

// A bucket for a Go map.
type bmap struct {
    tophash [bucketCnt]uint8
}

const (
    // Maximum number of key/elem pairs a bucket can hold.
    bucketCntBits = 3
    bucketCnt     = 1 << bucketCntBits
)

func main() {
    m := make(map[string]int)
    m["tony"] = 11
    m["bai"] = 12
    p := (*hmap)(unsafe.Pointer(*(*uintptr)((unsafe.Pointer)(&m))))
    fmt.Printf("%#v\n", *p)
}

这个例子中最难理解的就是变量p的声明与赋初值那一行,对于这一行我们分解来讲一下。

首先,前面我们说过:map类型变量m是指针,其存储的是一个hmap类型实例的地址。通过

*(*uintptr)((unsafe.Pointer)(&m))

我们得到的是m指向的那个hmap类型实例的地址。

然后通过将其转换为*hmap类型,我们就相当于直接得到了一个指向hmap类型实例地址的*hmap类型变量p。通过对p进行解引用,我们就能看到hmap结构体的内容了。运行上面代码我们得到下面输出结果:

main.hmap{count:2, flags:0x0, B:0x0, noverflow:0x0, hash0:0x42833520, buckets:(unsafe.Pointer)(0xc000072ea0), oldbuckets:(unsafe.Pointer)(nil), nevacuate:0x0, extra:(*main.mapextra)(nil)}

当我们看到输出结果中hmap.count这个字段(表示当前map中存储的键值对的个数)的值为2,我们就可以确定:m就是一个执行hmap结构体实例的指针这一结论是正确的。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}

img{512x368}
img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

© 2022, bigwhite. 版权所有.

Related posts:

  1. Go GC如何检测内存对象中是否包含指针
  2. Go 1.17新特性详解:支持将切片转换为数组指针
  3. 为什么这个T类型实例无法调用*T类型的方法
  4. Go 1.17中值得关注的几个变化
  5. Go语言的“黑暗角落”:盘点学习Go语言时遇到的那些陷阱[译](第二部分)