從main入口開始談golang

一、問題

1.我們知道一個可獨立運行的golang程序,一定要有個main.main(),因爲main()是程序的入口。可你知道爲什麼一定要求是main.main()嗎?

2.main()在執行前,根據package的初始化順序,會先初始化依賴的package,然後初始化main,這些操作是在什麼時候完成的呢?

二、溯源

1.源碼

以下代碼位於go/src/runtime/proc.go,這些源碼將會給我們答案。

//go:linkname main_inittask main..inittask
var main_inittask initTask

...

//go:linkname main_main main.main
func main_main()

...

// The main goroutine.
func main() {

    ...

    gcenable()//啓動gc,sweeping and scavenging

    main_init_done = make(chan bool)

    ...

    doInit(&main_inittask)//此處進行初始化任務

    close(main_init_done)

    ...

    if isarchive || islibrary {
        // A program compiled with -buildmode=c-archive or c-shared
        // has a main, but it is not executed.
        return
    }
    fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
    fn()
    ...
}

2.go:link

在正式理解源碼之前我們需要學習下go:link,以下是官方說法:

//go:linkname localname [importpath.name]
The //go:linkname directive instructs the compiler to use “importpath.name” as the object file symbol name for the variable or function declared as “localname” in the source code. If the “importpath.name” argument is omitted, the directive uses the symbol's default object file symbol name and only has the effect of making the symbol accessible to other packages. Because this directive can subvert the type system and package modularity, it is only enabled in files that have imported "unsafe".

簡單來說通過這種機制,可以實現調用其他包不能導出的內容。

3.main的入口問題

根據go:link,我們知道

//go:linkname main_main main.main
func main_main()//調用的就是我們熟悉的main package的main()

因此,

fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()//此處即爲執行main(),正式進入我們自己開發的程序邏輯

代碼中的fn()即爲main(),執行main()即進入我們自己開發的程序邏輯,此處回答了問題1。

4.初始化問題

我們在go:link中提過

我們先看下main_inittask:main_main()即爲main.main(),那var main_inittask initTask又是什麼呢?我們的main中是沒有initTask的,有的只是init,這兩者有什麼聯繫呢?問題比較複雜,已經涉及到compile中的內容,有興趣的可以去看下位於go/src/cmd/compile/internal/gc/init.go中fninit相關的內容,此處不再詳細說明。

// fninit makes an initialization record for the package.
// See runtime/proc.go:initTask for its layout.
// The 3 tasks for initialization are:
//   1) Initialize all of the packages the current package depends on.
//   2) Initialize all the variables that have initializers.
//   3) Run any init functions.
func fninit(n []*Node) {
    nf := initOrder(n)

    var deps []*obj.LSym // initTask records for packages the current package depends on
    var fns []*obj.LSym  // functions to call for package initialization

    // Find imported packages with init tasks.
    for _, s := range types.InitSyms {
        deps = append(deps, s.Linksym())
    }

    // Make a function that contains all the initialization statements.
    if len(nf) > 0 {//打包所有的init
        lineno = nf[0].Pos // prolog/epilog gets line number of first init stmt
        initializers := lookup("init")
        disableExport(initializers)
        fn := dclfunc(initializers, nod(OTFUNC, nil, nil))
        for _, dcl := range dummyInitFn.Func.Dcl {
            dcl.Name.Curfn = fn
        }
        fn.Func.Dcl = append(fn.Func.Dcl, dummyInitFn.Func.Dcl...)
        dummyInitFn.Func.Dcl = nil

        fn.Nbody.Set(nf)
        funcbody()

        fn = typecheck(fn, ctxStmt)
        Curfn = fn
        typecheckslice(nf, ctxStmt)
        Curfn = nil
        funccompile(fn)
        fns = append(fns, initializers.Linksym())
    }
    if dummyInitFn.Func.Dcl != nil {
        // We only generate temps using dummyInitFn if there
        // are package-scope initialization statements, so
        // something's weird if we get here.
        Fatalf("dummyInitFn still has declarations")
    }

    // Record user init functions.
    for i := 0; i < renameinitgen; i++ {
        s := lookupN("init.", i)
        fns = append(fns, s.Linksym())
    }

    if len(deps) == 0 && len(fns) == 0 && localpkg.Name != "main" && localpkg.Name != "runtime" {
        return // nothing to initialize
    }

    // Make an .inittask structure.
    sym := lookup(".inittask")
    nn := newname(sym)
    nn.Type = types.Types[TUINT8] // dummy type
    nn.SetClass(PEXTERN)
    sym.Def = asTypesNode(nn)
    exportsym(nn)
    lsym := sym.Linksym()
    ot := 0
    //以下封裝了initTask對應的參數
    ot = duintptr(lsym, ot, 0) // state: not initialized yet
    ot = duintptr(lsym, ot, uint64(len(deps)))
    ot = duintptr(lsym, ot, uint64(len(fns)))
    for _, d := range deps {
        ot = dsymptr(lsym, ot, d, 0)
    }
    for _, f := range fns {
        ot = dsymptr(lsym, ot, f, 0)
    }
    // An initTask has pointers, but none into the Go heap.
    // It's not quite read only, the state field must be modifiable.
    ggloblsym(lsym, int32(ot), obj.NOPTR)
}

initTask包含了3個參數,狀態,初始化deps,初始化方法數,根據這後兩個參數可以完成初始化的遞進進行,見doInit。如此我們可以說明第二個問題,同時也可以發現初始化確實是在main之前執行的。

// An initTask represents the set of initializations that need to be done for a package.
type initTask struct {
    // TODO: pack the first 3 fields more tightly?
    state uintptr // 0 = uninitialized, 1 = in progress, 2 = done
    ndeps uintptr
    nfns  uintptr
    // followed by ndeps instances of an *initTask, one per package depended on
    // followed by nfns pcs, one per init function to run
}

func doInit(t *initTask) {
    switch t.state {
    case 2: // fully initialized
        return
    case 1: // initialization in progress
        throw("recursive call during initialization - linker skew")
    default: // not initialized yet
        t.state = 1 // initialization in progress
        for i := uintptr(0); i < t.ndeps; i++ {//存在依賴,繼續下一層
            p := add(unsafe.Pointer(t), (3+i)*sys.PtrSize)
            t2 := *(**initTask)(p)
            doInit(t2)
        }
        for i := uintptr(0); i < t.nfns; i++ {
            p := add(unsafe.Pointer(t), (3+t.ndeps+i)*sys.PtrSize)
            f := *(*func())(unsafe.Pointer(&p))
            f()//執行init
        }
        t.state = 2 // initialization done
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章