查阅源码

写代码的时候,有时会纠结于语言提供的一些机制是否符合预期,这个时候就需要百度搜CSDN了。。。哦不,是需要阅读源码了。
golang的源码有一点特殊,以下简单记录下源码查阅方式,附带几个之前查阅源码的经历。
本文源码基于go-1.17.2。

源码位置

最初以为源码都在官方下载的src包(例如:go1.17.2.linux-amd64.tar.gz)里,实际上分了两部分,基础编译器在1.4的源码里,从1.5开始只包含go和汇编了。

Go 1.5 Release Notes
The compiler and runtime are now written entirely in Go (with a little assembler). C is no longer involved in the implementation, and so the C compiler that was once necessary for building the distribution is gone.

官方文档( https://golang.org/doc/install/source )也给出了指引,关于如何从源码编译golang,要先用1.4编译bootstrap toolchain,然后编译最新版,而1.4的这部分代码会持续维护,以支持后续的更新。

Go 1.4 was the last distribution in which the toolchain was written in C.

因此会发现当前新版的源码包里几乎没有c语言的代码,而且很多代码是找不到的。
查阅源码的时候要准备两份,一份1.4,一份当前版本。

源码:unsafe.Sizeof

函数入口:cmd/gc/unsafe.c->unsafenmagic
可以看到代码里实际还是基于type来计算的:

if(strcmp(s->name, "Sizeof") == 0) {
        typecheck(&r, Erv);
        defaultlit(&r, T); 
        tr = r->type;
        if(tr == T)
            goto bad;
        dowidth(tr);
        v = tr->width;
        goto yes;
    }

实际功能:cmd/gc/align.c->dowidth
代码里有一个switch case,根据数据类型设置w,记录到t->width:

1. 基本类型是固定width
2. map直接作为指针占1个word
3. array是m*n
4. string是初始化时固定计算的值,sizeof_String = rnd(Array_nel+widthint, widthptr)
5. interface{}固定2个指针,2个word
6. slice和其他struct通过函数widstruct计算,会把所有fields累加起来,同时计算了alignment,fields计算仍然是递归到dowidth函数

package main

import (
    "fmt"
    "unsafe"
)

type ST struct {
    b bool
    n int
    s string
}

func main() {
    // linux64 platform
    // unsafe.Sizeof(map)
    m := make(map[string]string)
    m = map[string]string{}
    fmt.Printf("%d\n", unsafe.Sizeof(m))    // 8
    m = map[string]string{"a": "a", "b": "bcd", "ccc": "efg"}
    fmt.Printf("%d\n", unsafe.Sizeof(m))    // 8
    // unsafe.Sizeof(array)
    var a [3]int16
    fmt.Printf("%d\n", unsafe.Sizeof(a))    // 6
    // unsafe.Sizeof(string)
    str := "abcdefghijklmn"
    fmt.Printf("%d\n", unsafe.Sizeof(str))  // 16
    str = ""
    fmt.Printf("%d\n", unsafe.Sizeof(str))  // 16
    // unsafe.Sizeof(interface{})
    i := uint16(0)
    fmt.Printf("%d\n", unsafe.Sizeof(interface{}(i)))   // 16
    // unsafe.Sizeof(slice)
    sl := make([]interface{}, 4, 6)
    fmt.Printf("%d\n", unsafe.Sizeof(sl))   // 24
    sl = []interface{}{uint16(1), "abcdefg", }
    fmt.Printf("%d\n", unsafe.Sizeof(sl))   // 24
    // unsafe.Sizeof(struct)
    st := ST{}
    fmt.Printf("%d\n", unsafe.Sizeof(st))   // 32
}

源码:time.AfterFunc

time.AfterFunc提供了一个很简洁的延时调用,传入duration、func即可工作。
然而用到它的时候,一丝隐隐的不安让我不由得思考:

他是怎么做到异步回调的呢?  
他在背后自动创建了任务协程吗?
以及进一步的,time.Sleep会直接让线程进入S状态吗?这样岂不是很费线程?

这些问题可以在源码中找到答案。
p管理了timers实现延时调用,其运行独立于协程。

time.AfterFunc并不创建协程,而是创建timer,p在schedule时处理timers,其在协程执行之前
相关源文件:time/sleep.go、runtime/time.go、runtime/proc.go、runtime/runtime2.go
调用链:AfterFunc - startTimer - addtimer - doaddtimer - append(pp.timers, t)
timer处理:runtime/proc.go->main - Gosched - gosched_m - goschedImpl - schedule - checkTimers - runtimer

time.Sleep也并不是直接把线程阻塞,而是创建了唤醒timer,并出让goroutine的执行权。

// timeSleep puts the current goroutine to sleep for at least ns nanoseconds.
//go:linkname timeSleep time.Sleep
func timeSleep(ns int64) {
    if ns <= 0 {
        return
    }    

    gp := getg()
    t := gp.timer
    if t == nil {
        t = new(timer)
        gp.timer = t
    }    
    t.f = goroutineReady
    t.arg = gp 
    t.nextwhen = nanotime() + ns 
    if t.nextwhen < 0 { // check for overflow.
        t.nextwhen = maxWhen
    }    
    gopark(resetForSleep, unsafe.Pointer(t), waitReasonSleep, traceEvGoSleep, 1)
}