日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
一文帶你了解【Go】初始化函數(shù)

環(huán)境

 
 
 
 
  1. OS : Ubuntu 20.04.2 LTS; x86_64
  2. Go : go version go1.16.2 linux/amd64

包初始化

初始化函數(shù)與其他普通函數(shù)一樣,都隸屬于定義它的包(package),以下統(tǒng)稱為當(dāng)前包。

一般來講,一個(gè)包初始化過程分三步:

  1. 初始化當(dāng)前包依賴的所有包,包括依賴包的依賴包。
  2. 初始化當(dāng)前包所有具有初始值的全局變量。
  3. 執(zhí)行當(dāng)前包的所有初始化函數(shù)。

關(guān)于這個(gè)過程,本文會(huì)一一詳細(xì)介紹。

基本定義

在Golang中有一類特殊的初始化函數(shù),其定義格式如下:

 
 
 
 
  1. package pkg
  2. func init() {
  3.   // to do sth
  4. }

初始化函數(shù)一個(gè)特殊之處是:其在可執(zhí)行程序的main入口函數(shù)執(zhí)行之前自動(dòng)執(zhí)行,而且不可被直接調(diào)用!

重復(fù)聲明

初始化函數(shù)第二個(gè)特殊之處是:在同一個(gè)包下,可以重復(fù)定義多次。

普通函數(shù)在同一個(gè)包下不可以重名,否則變異失?。簒xx redeclared in this block。

編譯重命名

初始化函數(shù)第三個(gè)特殊之處是:編譯重命名規(guī)則與普通函數(shù)不同。

普通函數(shù)在編譯過程中一般重命名規(guī)則為“[模塊名].包名.函數(shù)名”。

初始化函數(shù)在源碼中雖然名稱為init,但在編譯過程中重命名規(guī)則為“[模塊名].包名.init.數(shù)字后綴”。

例如:

  • 在上述的 func_init.0.go 源文件編譯之后,init函數(shù)被重命名為:main.init.0。
  • 在上述的 func_init.1.go 源文件編譯之后,兩個(gè)init函數(shù)分別被重命名為:main.init.0、main.init.1。

如上所示,如果同一個(gè)包下有多個(gè)init函數(shù),重命名時(shí)后綴數(shù)字按順序增加一。

為什么會(huì)這樣呢?

那是因?yàn)镚olang編譯器對(duì) init 函數(shù)進(jìn)行了特殊處理,相關(guān)源碼位于 cmd/compile/internal/gc/init.go 文件中。

全局變量 renameinitgen 用于記錄當(dāng)前包名下init函數(shù)的數(shù)量以及下一個(gè)init函數(shù)后綴的值。

每當(dāng)Golang編譯器遇到一個(gè)名稱為 init 的函數(shù),就會(huì)調(diào)用一次 renameinit() 函數(shù),最終 init 函數(shù)變得不可被調(diào)用。

為什么重命名init函數(shù)?

如上述我們看到的,在同一個(gè)包下可以重復(fù)聲明 init 函數(shù),這可能是需要重命名的原因。

當(dāng)我們繼續(xù)探究時(shí),可能更加接近真相。

有一點(diǎn)需要明確并始終堅(jiān)信:除全局常量和全局變量的聲明之外,所有的可執(zhí)行代碼都必須在函數(shù)內(nèi)執(zhí)行。

通常情況下,代碼編譯之后,

  1. 聲明的全局常量可能被存儲(chǔ)在可執(zhí)行文件的.rodata section。
  2. 聲明的全局變量可能被存儲(chǔ)在可執(zhí)行文件的.data、.bss、.noptrdata等section。
  3. 聲明的函數(shù)或方法被編譯為機(jī)器指令存儲(chǔ)在可執(zhí)行文件的.text section。

那么,以下代碼中(func_init.go),聲明全局變量的同時(shí)進(jìn)行初始化賦值,該如何編譯呢?

以下代碼屬于變量聲明。

 
 
 
 
  1. var m
  2. var name

而以下代碼包含函數(shù)調(diào)用和初始化賦值,最終要被編譯為機(jī)器指令,并且需要在main函數(shù)之前執(zhí)行;這些指令最終必須占用一塊存儲(chǔ)空間并且能夠加載到內(nèi)存中。

 
 
 
 
  1. var m = map[string]int{
  2.     "Jack": 18,
  3.     "Rose": 16,
  4. }
  5. var name = flag.String("name", "", "user name")

它們被存儲(chǔ)在可執(zhí)行文件的什么地方了呢?

通過逆向分析,發(fā)現(xiàn)Go編譯器合并了函數(shù)外的代碼調(diào)用(全局變量的初始化賦值),自動(dòng)生成了一個(gè) init 函數(shù);很明顯,在func_init.go源文件中并沒有定義初始化函數(shù)。

這可能也是編譯器重命名自定義init函數(shù)的原因吧。

編譯存儲(chǔ)

所有的初始化函數(shù)都不可被直接調(diào)用!所有它們會(huì)被存儲(chǔ)起來并在程序啟動(dòng)時(shí)自動(dòng)執(zhí)行。

在代碼編譯過程中,當(dāng)前包的初始化函數(shù)及其依賴的包的初始化,會(huì)被存儲(chǔ)到一個(gè)特殊的結(jié)構(gòu)體中,該結(jié)構(gòu)體定義在runtime/proc.go源文件中,如下所示:

 
 
 
 
  1. type initTask struct {
  2.     state uintptr // 當(dāng)前包在程序運(yùn)行時(shí)的初始化狀態(tài):0 = uninitialized, 1 = in progress, 2 = done
  3.     ndeps uintptr // 當(dāng)前包的依賴包的數(shù)量
  4.     nfns  uintptr // 當(dāng)前包的初始化函數(shù)數(shù)量
  5. }

Go語言是一個(gè)語法糖很重的編程語言,在源碼中看到的往往不是真實(shí)的。

runtime.initTask結(jié)構(gòu)體是一個(gè)編譯時(shí)可修改的動(dòng)態(tài)結(jié)構(gòu)。其真實(shí)面貌如下所示:

 
 
 
 
  1. type initTask struct {
  2.     state uintptr // 當(dāng)前包在程序運(yùn)行時(shí)的初始化狀態(tài):0 = uninitialized, 1 = in progress, 2 = done
  3.     ndeps uintptr // 當(dāng)前包的依賴包的數(shù)量
  4.     nfns  uintptr // 當(dāng)前包的初始化函數(shù)數(shù)量
  5.     deps  [ndeps]*initTask // 當(dāng)前包的依賴包的initTask指針數(shù)組(不是slice)
  6.     fns   [nfns]func ()    // 當(dāng)前包的初始化函數(shù)指針數(shù)組(不是slice)
  7. }

每個(gè)包的依賴包數(shù)量可能不同(ndeps),每個(gè)包的初始化函數(shù)數(shù)量不同(nfns),所以最終生成的initTask對(duì)象大小可能不同。

具體編譯過程參考cmd/compile/internal/gc/init.go源文件中的fninit函數(shù),此處不再贅述。

Go編譯器為每個(gè)包生成一個(gè)runtime.initTask類型的全局變量,該變量的命名規(guī)則為“包名..inittask”,如下所示:

從上圖第三列可以看出,每個(gè)包的initTask對(duì)象大小不同。具體計(jì)算方法如下:

 
 
 
 
  1. size := (3 + ndeps + nfns) * 8

初始化過程

在可執(zhí)行程序啟動(dòng)的初始化過程中,優(yōu)先執(zhí)行runtime包及其依賴包的初始化,然后執(zhí)行main包及其依賴包的初始化。

一個(gè)包可能被多個(gè)包依賴,但是每個(gè)包的都只初始化一次,通過runtime.initTask.state字段進(jìn)行控制。

具體的初始化邏輯請(qǐng)參考runtime/proc.go源文件中的main函數(shù)和doInit函數(shù)。

在初始化過程中,runtime.doInit函數(shù)會(huì)被調(diào)用很多次,其具體執(zhí)行流程如本文開頭的“包初始化”一節(jié)所述一致。

如前圖所示的func_init.2.go源文件,編譯之后包含兩個(gè)初始化函數(shù):一個(gè)是編譯器自動(dòng)生成的,另一個(gè)是編譯器重命名的;自動(dòng)生成的初始化函數(shù)優(yōu)先執(zhí)行。

如前圖所示的func_init.2.go源文件,編譯之后生成的main..inittask全局變量的內(nèi)存地址是0x000000000054dc60。我們動(dòng)態(tài)調(diào)試runtime.doInit函數(shù),在其參數(shù)為main..inittask全局變量指針時(shí)暫停執(zhí)行,觀察參數(shù)的數(shù)據(jù)結(jié)構(gòu)。

從動(dòng)態(tài)調(diào)試時(shí)展示的內(nèi)存數(shù)據(jù)我們反推出如下偽代碼:

 
 
 
 
  1. package main
  2. var inittask = struct {
  3.   state uintptr    // 當(dāng)前包在程序運(yùn)行時(shí)的初始化狀態(tài):0 = uninitialized, 1 = in progress, 2 = done
  4.   ndeps uintptr    // 當(dāng)前包依賴的包的initTask數(shù)量
  5.   nfns  uintptr    // 當(dāng)前包的初始化函數(shù)數(shù)量
  6.   deps  [2]uintptr // 當(dāng)前包依賴的包的initTask指針數(shù)組(不是slice)
  7.   fns   [2]uintptr // 當(dāng)前包的初始化函數(shù)指針數(shù)組(不是slice)
  8. }{
  9.   state: 0,
  10.   ndeps: 2,
  11.   nfns:  2,
  12.   deps:  [2]uintptr{0x54ef60, 0x54eca0}, // flag..inittask,fmt..inittask
  13.   fns:   [2]uintptr{0x4a4ec0, 0x4a4d60}, // main.init,main.init.0
  14. }

在func_init.2.go源文件中,引用了flag、fmt兩個(gè)包,所以main包的初始化必須在這兩個(gè)包的初始化完成之后執(zhí)行。

 
 
 
 
  1. import "flag"
  2. import "fmt"

通常initTask.ndeps字段的值與import的數(shù)量相同。

編譯器自動(dòng)生成的init函數(shù)先于代碼源文件中自定義的init函數(shù)執(zhí)行。

結(jié)語

至此,本文完整地、詳細(xì)地介紹了Go中關(guān)于初始化函數(shù)相關(guān)的內(nèi)容。

相信在認(rèn)真刨析了初始化函數(shù)的所有細(xì)節(jié)之后,對(duì)Go有了更近一步的了解。

希望有助于減少開發(fā)編碼過程中的疑惑,更加得心應(yīng)手,游刃有余。

本文轉(zhuǎn)載自微信公眾號(hào)「Golang In Memory」


新聞標(biāo)題:一文帶你了解【Go】初始化函數(shù)
網(wǎng)頁路徑:http://www.dlmjj.cn/article/cdgdppg.html