自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一文帶你了解【Go】初始化函數(shù)

開發(fā) 前端
本文完整地、詳細(xì)地介紹了Go中關(guān)于初始化函數(shù)相關(guān)的內(nèi)容。相信在認(rèn)真刨析了初始化函數(shù)的所有細(xì)節(jié)之后,對Go有了更近一步的了解。

[[425952]]

環(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)前包。

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

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

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

基本定義

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

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

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

重復(fù)聲明

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

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

編譯重命名

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

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

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

例如:

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

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

為什么會這樣呢?

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

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

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

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

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

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

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

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

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

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

以下代碼屬于變量聲明。

  1. var m 
  2. var name 

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

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

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

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

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

編譯存儲

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

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

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

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

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

  1. type initTask struct { 
  2.     state uintptr // 當(dāng)前包在程序運行時的初始化狀態(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) 

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

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

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

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

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

初始化過程

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

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

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

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

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

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

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

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

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

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

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

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

結(jié)語

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

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

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

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

 

責(zé)任編輯:姜華 來源: Golang In Memory
相關(guān)推薦

2023-11-20 08:18:49

Netty服務(wù)器

2019-08-06 09:00:00

JavaScript函數(shù)式編程前端

2023-11-06 08:16:19

APM系統(tǒng)運維

2022-11-11 19:09:13

架構(gòu)

2022-02-24 07:34:10

SSL協(xié)議加密

2023-11-08 08:15:48

服務(wù)監(jiān)控Zipkin

2023-10-27 08:15:45

2024-03-26 00:17:51

Go語言IO

2020-02-02 15:14:24

HTTP黑科技前端

2025-01-15 09:06:57

servlet服務(wù)器Java

2022-04-28 09:22:46

Vue灰度發(fā)布代碼

2020-10-08 14:32:57

大數(shù)據(jù)工具技術(shù)

2022-09-29 13:09:38

DataClassPython代碼

2024-04-26 00:01:00

Go語言類型

2018-10-22 08:14:04

2022-02-18 10:13:07

SolrElasticSea開源

2019-07-04 15:16:52

數(shù)據(jù)挖掘大數(shù)據(jù)算法

2022-09-06 11:21:49

光網(wǎng)絡(luò)光纖

2023-03-31 08:16:53

Flutter優(yōu)化內(nèi)存管理

2023-12-06 16:28:56

點贊
收藏

51CTO技術(shù)棧公眾號