从基本结构了解GO语言设计及思想

学习GO语言,我们可以通过其基本结构,来学习GO语言的一些设计和思想。通过本质去了解GO语言的设计及思想,这样对GO语言的认识也更加彻底和清晰。本文会选取两类结构体来进行介绍,一类是用户开发经常会使用到的切片和哈希表的结构体,另一类是开发中几乎接触不到的GMP模型的结构体来展开介绍。前者可以让我们学习到常见数据结构在编译和运行时的表现,后者则可以通过结构体介绍可以让我们切身感受到为什么GO是一门高性能高并发的语言。

常见数据结构的结构体

在介绍基本数据类型的结构体前,可以先看一下以下的例子,猜测下面的结果会输出什么?

func sliceFunc(a []string, b []string, c *[]string){
    a = append(a, "zzz")
    a[0]="zzz"

    b[0]="zzz"
    b = append(b, "zzz")

    (*c)[0]="zzz"
    *c = append(*c,"zzz")
}

func main() {
    a := []string{"aaa", "aaa"}
    b := []string{"bbb", "bbb"}
    c := []string{"ccc", "ccc"}
    sliceFunc(a, b, &c)
    fmt.Printf("%v,%v,%v", a, b, c)
}
一、数组和切片的结构体

以下是来自GO 1.15.6源码的数组结构体(后续如无特殊说明,所有源码均来自于版本1.15.6)

// 编译期间的数组和切片类型结构体
// src/cmd/compile/internal/types/type.go
// Array contains Type fields specific to array types.
type Array struct {
    Elem  *Type // element type
    Bound int64 // number of elements; <0 if unknown yet
}
// src/cmd/compile/internal/types/type.go
// Slice contains Type fields specific to slice types.
type Slice struct {
    Elem *Type // element type
}

// 运行时的切片类型结构体
// SliceHeader is the runtime representation of a slice.
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

// StringHeader is the runtime representation of a string.
// string实质上是一个char数组
type StringHeader struct {
    Data uintptr
    Len  int
}

slice结构体

从上述的代码中可以发现,无论是数组还是slice,其本质上都是一个个结构体,结构体里保存着实际数据存储的内存空间的指针、长度,slice同时还会存有Cap字段,用于保存容量。由于GO语言的参数传递始终都是传值的。不管是传递结构体还是传指针,将实参传递到函数中的形参上,都会导致值的复制,只是区别在于,前者是复制整个结构体,后者是复制指针。所以会导致不同的差异。

二、哈希表的结构体
// A header for a Go map.
type hmap struct {
    // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
    // Make sure this stays in sync with the compiler's definition.
    count     int // 当前哈希表中的元素数量
    flags     uint8
    B         uint8  // 当前哈希表持有的buckets数量,即len(buckets) == 2^B
    noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
    hash0     uint32 // hash seed,随机种子,在创建哈希表时就确定下来了

    buckets    unsafe.Pointer //    指向当前桶数组[]bmap. may be nil if count==0.
    oldbuckets unsafe.Pointer // 扩容时不为空,旧的桶数量是当前桶数量的一半。
    nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

    extra *mapextra // optional fields
}

type mapextra struct {
    overflow    *[]*bmap //溢出桶
    oldoverflow *[]*bmap 

    // 指向空闲的 overflow bucket 的指针
    nextOverflow *bmap //溢出桶指针
}

// A bucket for a Go map.
type bmap struct {
    tophash [bucketCnt]uint8 // bucketCnt =8,意味这每个桶里面都能存储8个键值对
}

//在运行期间结构体其实不止包含 `tophash` 字段
//因为哈希表是可以存储不同类型的键值对,所以键值对占据的内存空间大小只能在编译时进行推导。
//bmap中的其他字段在运行时也都是通过计算内存地址的方式访问的,所以它的定义中就不包含这些字段。
//不过我们能根据编译期间的 cmd/compile/internal/gc.bmap`](https://draveness.me/golang/tree/cmd/compile/internal/gc.bmap函数的结构:
type bmap struct {
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr

}

哈希表的数据结构

从哈希表的结构体上我们知道,GO实现的哈希表使用的是拉链法。同时每一个桶对应的对象数组固定位8个,所以每个桶最多能存8个键值对,当桶超过8个键值对时,会创建一个溢出桶,存储在extra结构体中。当哈希表使用了太多的溢出桶时,会触发哈希表扩容(等量扩容)。另外,装载因子超过6.5也会触发扩容(翻倍扩容)。

访问一个元素时,会通过runtime.bucketMask和runtime.tophash返回该键值对所在的桶序号和哈希的高8位,进而找到在对应的桶,然后在bucketloop循环中依次遍历正常桶和溢出桶的高8位,如果相等才去比较键是否相等。

G-M-P模型对应的结构体

GO1.1后,调度器由G-M模型演变成了G-M-P模型


GMP模型
当我们使用go run main.go时:
  1. runtime创建M0,G0

  2. 调度器初始化:初始化M0,栈,垃圾回收,创建初始的长度为GOMAXPROCS的P列表(存入全局变量allp中)

  3. runtime.main创建代码的入口main对应的主goroutine,然后放到P的本地队列

  4. 启动M0, M0绑定至p上(allp[0])

  5. 根据goroutine的栈和调度信息,M0设置运行环境

  6. 在M中运行G

当我们使用 go func(){....}()时:

1.创建一个goroutine(G)
2.将G添加到当前P的本地队列或者全局队列中。
3.P中的调度器将G放到与之关联的M中执行。
4.将g添加到当前p的本地队列或者全局队列中。

一、G:Goroutine 结构体

Goroutine只存在于Go语言的运行时,所有的Goroutine调度都由Go语言调度器触发,能减少很多额外开销,所以它比内核态的线程切换更加高效和快速。
它使用runtime.g 结构体表示:

type g struct {
    goid         int64   //Goroutine的id,该字段不可导出,所以对开发者不可见
    stack       stack   // offset known to runtime/cgo 当前的栈内存范围[stack.lo, stack.hi)

    _panic       *_panic // 存储panic对应结构体的链表,头节点表示最内侧的panic结构体
    _defer       *_defer // 存储defer对应结构体的链表,头节点表示最内侧的defer结构体
    
    m            *m      // 当前Goroutine 占用的线程,可能为空
    sched        gobuf  //存储Goroutine 的调度相关信息的结构体,用于保存和恢复上下文,如 sp 栈指针,pc 程序计数器,ret 系统调用返回值等。  
    atomicstatus uint32  //Goroutine 的状态。_Gidle, _Grunnable, _Grunning, _Gsyscall, _Gwaiting等

    ...省略其它字段
}
二、M:Thread 结构体

Go 语言并发模型中的M是操作系统线程。调度器最多可以创建10 000个线程,但是大多数线程不会执行用户代码,最多只会有GOMAXPROCS个活跃线程能够正常运行。
Go语言使用runtime.mt结构体表示操作系统线程:


type m struct {
    id            int64
    g0            *g     // 持有调度栈的Goroutine,用来调度其他的Goroutine。会深度参与运行时的调度过程,包括Goroutine的创建、大内存分配和cgo函数的执行等。
    curg          *g     //当前线程上运行的用户Goroutine 

    p             puintptr // attached p for executing go code (nil if not executing go code)。正在运行代码的处理器p。
    nextp         puintptr  //暂存的处理器nextp。
    oldp          puintptr // the p that was attached before executing a syscall。执行系统调用之前使用线程的处理器oldp。
    
    ...省略其它字段
}

三、P:Process 结构体

处理器P能提供线程需要的上下文,负责调度线程上的等待队列,让每个内核线程都能执行多个Goroutine。它也能在Goroutine进行一些I/O操作时及时让出计算资源,提高线程的利用率。

type p struct {
    id          int32
    m           muintptr   // back-link to associated m (nil if idle)。反向存储着绑定的线程。
    // Queue of runnable goroutines. Accessed without lock.
    runqhead uint32 
    runqtail uint32 
    runq     [256]guintptr //本地Goroutine 队列。从队列大小可知,本地队列最多可以存256个Goroutine,当队列满时,会将一半的Goroutine放在全局队列中。

    status      uint32 // 当前处理器的状态 _Pidle、_Prunning、_Psyscall、_Pgcstop等
    link        puintptr
    schedtick   uint32     // incremented on every scheduler call
    syscalltick uint32     // incremented on every system call
    sysmontick  sysmontick // last tick observed by sysmon

    // 空闲Gouroutine,如果处理器中不存在空闲Goroutine,就会通过runtime.malg创建一个栈大小(2KB)足够的新结构体
    gFree struct {
        gList
        n int32
    }
    // 定时器的相关字段。 计时器由处理器的网络轮询器
    //(通过系统监控函数runtime.sysmon触发的runtime.netpoll完成)和调度器触发。
    timer0When uint64 
    timersLock mutex     //用于保护计时器的互斥锁
    timers []*timer      //存储计时器的最小四叉堆
    numTimers uint32     //处理器中的计时器数量
    adjustTimers uint32  //处于timerModifiedEarlier 状态的计时器数量
    deletedTimers uint32 //处于timerDeleted状态的计时器数量
    timerRaceCtx uintptr

    ...省略其它字段
}

总结

在阅读GO源码时,几乎所有GO中的概念和名词,都可以找到与之对应的结构体。无论是上述提到的slice,map,还是GMP模型中的Goroutine。还有本文中没有提到的channel(hchan)、interface(eface,iface)等。通过直接学习和了解结构体的各个字段及作用,我们可以抓住其本质,以便我们日后在开发中写出更加高效和优雅的代码,更少掉到GO语言的陷阱中。在遇到对应的问题时,也能更快定位并解决。



参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容