Go 學習筆記(3)— 包概念、包特點、包名約束、main 包、包的聲明、包的引用、包初始化

1. 包的概念

Go 語言是使用包來組織源代碼的,並實現命名空間的管理。任何源代碼文件必須屬於某個包。源碼文件的第一行有效代碼必須是 package pacakgeName 語句,通過該語句聲明自己所在的包。

 

2. 包的特徵

所有的 .go 文件,除了空行和註釋,都應該在第一行聲明自己所屬的包。即所有代碼都必須組織在 package 中。包的結構特點有:

  • 源文件頭部以 package 聲明包名稱;
  • 包由同一目錄下的多個源碼文件組成,即一個目錄下的同級文件屬於同一個包;
  • 每個包都在一個單獨的目錄裏;
  • 包所在的目錄名最好不用 mainallstd 這三個保留名稱;
  • 可執行文件必須包含 package main 和入口函數 main main 包是 Go 語言程序的入口包,一個 Go 語言程序必須有且僅有一個 main 包,並且,一個 main 包中也必須有且僅有一個 main 函數。如果一個程序沒有 main 包,那麼編譯時將會出錯,無法生成可執行文件;
  • 不能把多個包放到同一個目錄中,也不能把同一個包的文件分拆到多個不同目錄中。這意味着,同一個目錄下的所有 .go 文件必須聲明同一個包名;

包中成員以名稱首字母大小寫決定訪問權限。

  • Public : 首字母大寫,可被包外訪問;
  • internal : 首字母小寫,僅包內成員可以訪問;

該規則適用於全局變量、全局常量、類型、結構字段、函數、方法等

 

3. 包名約束

給包命名的慣例是使用包所在目錄的名字。給包及其目錄命名時,應該使用簡潔、清晰且全小寫的名字,這有利於開發時頻繁輸入包名。

記住,並不需要所有包的名字都與別的包不同,因爲導入包時是使用全路徑的,所以可以區分同名的不同包。一般情況下,包被導入後會使用你的包名作爲默認的名字,不過這個導入後的名字可以修改。這個特性在需要導入不同目錄的同名包時很有用。

關於默認包名一般採用導入路徑名的最後一段的約定也有三種例外情況。

  1. 包對應一個可執行程序,也就是 main 包,這時候 main 包本身的導入路徑是無關緊要的。名字爲 main 的包是給 go build 構建命令一個信息,這個包編譯完之後必須調用連接器生成一個可執行程序。
  2. 包所在的目錄中可能有一些文件名是以 _test.go爲後綴的 Go 源文件(譯註:前面必須有其它的字符,因爲以 _.開頭的源文件會被構建工具忽略),並且這些源文件聲明的包名也是以 _test爲後綴名的。這種目錄可以包含兩種包:一種是普通包,另一種則是測試的外部擴展包。所有以 _test爲後綴包名的測試外部擴展包都由 go test 命令獨立編譯,普通包和測試的外部擴展包是相互獨立的。測試的外部擴展包一般用來避免測試代碼中的循環導入依賴。
  3. 一些依賴版本號的管理工具會在導入路徑後追加版本號信息,例如“gopkg.in/yaml.v2” 這種情況下包的名字並不包含版本號後綴,而是 yaml

4. main 包

Go 語言裏,命名爲 main 的包具有特殊的含義。 Go 語言的編譯程序會試圖把這種名字的包編譯爲二進制可執行文件。所有用 Go 語言編譯的可執行程序都必須有一個名叫 main 的包

當編譯器發現某個包的名字爲 main 時,它一定也會發現名爲 main() 的函數,否則不會創建可執行文件。 main() 函數是程序的入口,所以,如果沒有這個函數,程序就沒有辦法開始執行。

程序編譯時,會使用聲明 main 包代碼所在目錄的目錄名作爲二進制可執行文件的文件名。

而且通常來說,main 包應該很簡潔。我們在 main 包中會做一些命令行參數解析、資源初始化、日誌設施初始化、數據庫連接初始化等工作,之後就會將程序的執行權限交給更高級的執行控制對象。

5. 包的聲明

Go 語言中,代碼包中的源碼文件名可以是任意的,這些任意名稱的源碼文件都必須以包聲明語句作爲文件中代碼的第一行。比如 src 目錄下的代碼包 common/upload 包中的所有源碼文件都要先聲明自己屬於common/upload 包:

package upload

packageGo 語言中用於包聲明語句的關鍵字。 Go 語言規定包聲明中的包名爲代碼包路徑的最後一個元素。如上,common/upload 包的包路徑爲 common/upload ,而包聲明中的包名則爲 upload

而針對命令源碼文件(即包含 main 函數的 .go 文件),無論存放在哪個包中,它都必須聲明爲屬於 main

6. 包的引用

標準包的源碼位於 $GOROOT/src/ 下面,標準包可以直接引用。自定義的包和第三方包的源碼必須放到 $GOPATH/src/ 目錄下才能被引用。導入包需要使用關鍵字 import ,它會告訴編譯器你想引用該位置的包內的代碼。如果需要導入多個包,習慣上是將 import 語句包裝在一個導入塊中。

包的引用路徑有兩種寫法, 一種是絕對路徑,另一種是相對路徑。

要在代碼中引用其他包的內容,需要使用 import 關鍵字導入使用的包。具體語法如下:

import "包的路徑"

注意事項:

  • import 導入語句通常放在源碼文件開頭包聲明語句的下面;
  • 導入的包名需要使用雙引號包裹起來;
  • 包名是從GOPATH/src/後開始計算的,使用/進行路徑分隔。

包的導入有兩種寫法,分別是單行導入和多行導入。

  • 單行導入:
import "包 1 的路徑"
import "包 2 的路徑"
  • 多行導入:
import (
    "包 1 的路徑"
    "包 2 的路徑"
)

 

6.1 絕對路徑引用

包的絕對路徑就是 $GOROOT/src$GOPATH/src 後面包的源碼的全路徑,比如下面的包引用:

import "common/upload"
import "database/sql/driver"
import "database/sql"

upload 包是自定義的包,其源碼位於 $GOPATH/src/common/upload 目錄下,代碼包導入使用的路徑就是代碼包在工作區的 src 目錄下的相對路徑,比如 upload 的絕對路徑爲 /home/wohu/gocode/src/common/upload ,而 /home/wohu/gocode 是被包含在環境變量 GOPATH 中的工作區目錄路徑,則其代碼包導入路徑就是common/upload

sqldriver 包的源碼分別位於 $GOROOT/src/database/sql$GOROOT/src/database/sql/driver 下。

編譯器會首先查找 Go 的安裝目錄,然後纔會按順序查找 GOPATH 變量裏列出的目錄。一旦編譯器找到一個滿足 import 語句的包,就停止進一步查找。

 

6.2 相對路徑引用


相對路徑只能用於引用 $GOPATH 下的包,標準包的引用只能使用全路徑引用。比如下面兩個包:
a 的路徑是 $GOPATH/src/lab/a ,包 b 的源碼路徑爲 $GOPATH/src/lab/b ,假設 b 引用了 a 包,則可以使用相對路徑引用方式。示例如下:

// 相對路徑引用
import "../a" 

// 絕對路徑引用
import "lab/a"

 

6.3 引用格式

常用的包引用有以下 4 種格式,我們以 fmt 包爲例進行說明。

  1. 標準引用方式
import "fmt”

此時可以用 fmt. 作爲前綴引用包內可導出元素,這是常用的一種方式。

  1. 別名引用方式
import F "fmt”

此時相當於給包 fmt 起了個別名 F ,用 F.代替標準的 fmt.作爲前綴引用 fmt 包內可導出元素。

  1. 省略引用方式
import . "fmt"

此時相當於把包 fmt 的命名空間直接合併到當前程序的命名空間中,使用 fmt 包內可導出元素可以不用前綴 fmt. ,直接引用。示例如下:

package main
import . "fmt"
func main() {
    // 不需要加前級fmt.
    Println("hello , world”)
}
  1. 僅執行包初始化 init 函數

使用標準格式引用包,但是代碼中卻沒有使用包,編譯器會報錯。如果包中有 init 初始化函數,則通過 import packageName 這種方式引用包,僅執行包的初始化函數,即使包沒有 init 初始化函數,也不會引發編譯器報錯。示例如下:

import  _ "fmt"

下劃線字符 _Go 語言裏稱爲空白標識符,這個標識符用來拋棄不想繼續使用的值,如給導入的包賦予一個空名字,或者忽略函數返回的你不感興趣的值。

  1. 遠程導入

Go 工具鏈會使用導入路徑確定需要獲取的代碼在網絡的什麼地方。

import "github.com/net/http"

用導入路徑編譯程序時, go build 命令會使用 GOPATH 的設置,在磁盤上搜索這個包。

事實上,這個導入路徑代表一個 URL ,指向 GitHub 上的代碼庫。如果路徑包含 URL ,可以使用 Go 工具鏈從 分佈式版本控制系統獲取包,並把包的源代碼保存在 GOPATH 指向的路徑裏與 URL 匹配的目錄裏。

這個獲取過程使用 go get 命令完成。go get 將獲取任意指定的 URL 的包,或者一個已經導入的包所依賴的其它包。由於 go get 的這種遞歸特性,這個命令會掃描某個包的源碼樹,獲取能找到的所有依賴包。

 

6.4 綜合實踐

當導入多個代碼包時,需要用圓括號括起它們,且每個代碼包名獨佔一行。在調用被導入代碼包中的函數或使用其中的結構體、變量或常量時,需要使用包路徑的最後一個元素加 . 的方式指定代碼所在的包。

例如,如果我們有兩個包 logginggo_lib/logging , 並且有相同的方法 logging_print() ,且有一個源碼文件需要導入這兩個包(標準引用):

import (
    "logging"
    "go_lib/logging"
)

則這句代碼 logging.logging_print() 就會引起衝突, Go 語言無法知道 logging. 代表的是哪一個包。所以,在 Go 語言中,如果在同一個源碼文件中使用上述方法導入多個代碼包,那麼代碼包路徑的最後一個元素不可以重複。

如果用這段代碼包導入代碼,在編譯代碼時,Go 語言會拋出

”logging redeclared as imported package name”

的錯誤。如果確實需要導入,當有這類重複時,我們可以給它們起個別名來區別(別名引用):

import (
    la "logging"
    lb "go_lib/logging"
)

調用包中的代碼:

var logger la.Logger = la.logging_print()

這裏不必給每個引起衝突的代碼包都起一個別名,只要能夠區分它們就可以了。

如果我們想直接調用某個依賴包的程序,就可以用 . 來代替別名(省略引用)。

import (
    . "logging"
    lb "go_lib/logging"
)

在當前源碼文件中,可以直接進行代碼調用了:

var logger Logger = logging_print()

Go 語言把變量、常量、函數、結構體和接口統稱爲程序實體,而把它們的名字統稱爲標識符。標識符可以是任何 Unicode 編碼可以表示的字母字符、數字以及下劃線 ”_”,並且,首字母不能是數字。標識符的首字母的大小寫控制着對應程序實體的訪問權限。

如果標識符的首字母是大寫的,那麼它對應的程序實體就可以被本代碼包之外的代碼訪問到,也可以稱其爲可導出的。否則對應的程序實體就只能被本包內的代碼訪問。當然,還需要有以下兩個額外條件:

  • (1)、程序實體必須是非局部的。局部程序實體是被定義在函數或結構體的內部。
  • (2)、代碼包所在的目錄必須被包含在環境變量 GOPATH 中的工作區目錄中。

如果代碼包 logging 中有一個叫做 getSimpleLogger 的函數,那麼光從這個函數的名字上我們就可以看出,這個函數是不能被包外代碼調用的。

如果我們只想初始化某個代碼包而不需要在當前源碼文件中使用那個代碼包中的任何代碼,即可以用 _ 來代替別名(僅執行包初始化 init 函數的引用方式)。

import (
    _ "logging"
)

 

6.5 注意事項

  1. 一個包可以有多個 init 函數,包加載會執行全部的 init 函數,但並不能保證執行順序,所以不建議在一個包中放入多個 init 函數,將需要初始化的邏輯放到一個 init 函數裏面。
  2. 包不能出現循環引用。比如包 a 引用了包 b ,包 b 引用了包 c,如果包 c 又引用了包 a,則編譯不能通過。
  3. 包的重複引用是允許的。比如包 a 引用了包 b 和包 c ,包 b 和包 c 都引用了包 d 。這種場景相當於重複引用了d,這種情況是允許的, 並且 Go 編譯器保證 d 的 init 函數只會執行一次。

 

7. 包初始化

Go 語言中,可以有專門的函數負責代碼包初始化。這個函數需要無參數聲明和結果聲明,且名稱必須爲 init ,如下:

func init() {
	println("Initialize")
}

Go 語言會在程序真正執行前對整個程序的依賴進行分析,並初始化相關的代碼包。也就是說,所有的代碼包初始化函數都會在 main 函數(命令源碼文件中的入口函數)之前執行完成,而且只會執行一次。並且,當前代碼包中的所有全局變量的初始化都會在代碼包初始化函數執行前完成。這就避免了在代碼包初始化函數對某個變量進行賦值之後又被該變量聲明中賦予的值覆蓋掉的問題。

每個包可以包含任意多個 init 函數,這些函數都會在程序執行開始的時候被調用。所有被編譯器發現的 init 函數都會安排在 main 函數之前執行。 init 函數用在設置包、初始化變量或者其他要在程序運行前優先完成的引導工作。

Go 裏面有兩個保留的函數: init 函數(能夠應用於所有的 package )和 main 函數(只能應用於 package main )。這兩個函數在定義時不能有任何的參數和返回值

雖然一個 package 裏面可以寫任意多個 init 函數,但這無論是對於可讀性還是以後的可維護性來說,我們都強烈建議用戶在一個 package 中每個文件只寫一個 init 函數。

Go 程序會自動調用 init()main() ,所以不需要在任何地方調用這兩個函數。每個 package 中的 init 函數都是可選的,但 package main 只能包含一個 main 函數

程序的初始化和執行都起始於 main 包。如果 main 包還導入了其它的包,那麼就會在編譯時將它們依次導入。有時一個包會被多個包同時導入,那麼它只會被導入一次(例如很多包可能都會用到 fmt 包,但它只會被導入一次,因爲沒有必要導入多次)。

當一個包被導入時,如果該包還導入了其它的包,那麼會先將其它包導入進來,然後再對這些包中的包級常量和變量進行初始化,接着執行 init 函數(如果有的話),依次類推。等所有被導入的包都加載完畢了,就會開始對 main 包中的包級常量和變量進行初始化,然後執行 main 包中的 init 函數(如果存在的話),最後執行 main 函數。下圖詳細地解釋了整個執行過程:

在這裏插入圖片描述

見 go 語言基礎 《main函數和init函數》 78 頁說明

init 函數特徵總結:

  • 每個源文件都可以定義一個或多個初始化函數,但強烈建議只定義一個
  • 編譯器不保證多個初始化函數執行次序
  • 初始化函數在單一線程被用,僅執行一次
  • 初始化函數在包所有全局變量初始化後執行
  • 在所有初始化函數結束後才執行 main.main
  • init() 函數不能被其他函數調用

init所以簡而言之,你只需要記住這三點就可以了:

  • 依賴包按“深度優先”的次序進行初始化;
  • 每個包內按以“常量 -> 變量 -> init 函數”的順序進行初始化;
  • 包內的多個 init 函數按出現次序進行自動調用;(待確認?)

這裏舉出《Go併發編程實戰》中的例子,幫助理解上面的包初始化,如下:

package main // 命令源碼文件必須在這裏聲明自己屬於main包

import ( // 引入了代碼包fmt和runtime
    "fmt"
    "runtime"
)

func init() { // 包初始化函數
    fmt.Printf("Map: %v\n", m) // 先格式化再打印
    // 通過調用runtime包的代碼獲取當前機器所運行的操作系統以及計算架構
    // 而後通過fmt包的Sprintf方法進行字符串格式化並賦值給變量info
    info = fmt.Sprintf("OS: %s, Arch: %s", runtime.GOOS, runtime.GOARCH)
}

// 非局部變量,map類型,且已初始化
var m map[int]string = map[int]string{1: "A", 2: "B", 3: "C"}
var info string // 非局部變量,string類型,未被初始化

func main() { // 命令源碼文件必須有的入口函數
    fmt.Println(info) // 打印變量info
}

輸出

Map: map[1:A 2:B 3:C]
OS: windows, Arch: amd64

在同一個代碼包中,可以存在多個代碼包初始化函數,甚至代碼包內的每一個源碼文件都可以定義多個代碼包初始化函數。

Go 語言編譯器不能保證同一個代碼包中的多個代碼包初始化函數的執行順序。如果要求按特定順序執行的話,可以考慮使用 Channel

8. 編譯速度

當我們修改了一個源文件,我們必須重新編譯該源文件對應的包和所有依賴該包的其他包

即使是從頭構建, Go 語言編譯器的編譯速度也明顯快於其它編譯語言。 Go 語言的閃電般的編譯速度主要得益於三個語言特性。

  1. 所有導入的包必須在每個文件的開頭顯式聲明,這樣的話編譯器就沒有必要讀取和分析整個源文件來判斷包的依賴關係。
  2. 禁止包的環狀依賴,因爲沒有循環依賴,包的依賴關係形成一個有向無環圖,每個包可以被獨立編譯,而且很可能是被併發編譯。
  3. 編譯後包的目標文件不僅僅記錄包本身的導出信息,目標文件同時還記錄了包的依賴關係。因此,在編譯一個包的時候,編譯器只需要讀取每個直接導入包的目標文件,而不需要遍歷所有依賴的的文件(譯註:很多都是重複的間接依賴)。
 
參考:https://blog.csdn.net/wohu1104/article/details/104387100
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章