和很多現(xiàn)代編程語言一樣,Go代碼包(package)來組織管理代碼。 我們必須先引入一個(gè)代碼包(除了?builtin
?標(biāo)準(zhǔn)庫(kù)包)才能使用其中導(dǎo)出的代碼要素(比如函數(shù)、類型、變量和具名常量等)。 此篇文章將講解Go代碼包和代碼包引入(import)。
下面這個(gè)簡(jiǎn)短的程序(假設(shè)它存在一個(gè)名為simple-import-demo.go
的源文件中)引入了一個(gè)標(biāo)準(zhǔn)庫(kù)包。
package main
import "fmt"
func main() {
fmt.Println("Go has", 25, "keywords.")
}
對(duì)此程序的一些解釋:
simple-import-demo.go
所處的包名為main
。 程序入口main
函數(shù)必須處于一個(gè)名為main
的代碼包中。import
關(guān)鍵字引入了fmt
標(biāo)準(zhǔn)庫(kù)包。 在此源文件中,fmt
標(biāo)準(zhǔn)庫(kù)包將用fmt
標(biāo)識(shí)符來表示。 標(biāo)識(shí)符fmt
稱為fmt
標(biāo)準(zhǔn)庫(kù)包的引入名稱。(后續(xù)某節(jié)將詳述代碼包的引入名稱)。fmt
標(biāo)準(zhǔn)庫(kù)包中聲明了很多終端打印函數(shù)供其它代碼包使用。 Println
函數(shù)是其中之一。 它可以將不定數(shù)量參數(shù)的字符串表示形式輸出到標(biāo)準(zhǔn)輸出中。 第六行調(diào)用了此Println
函數(shù)。 注意在此調(diào)用中,函數(shù)名之前需要帶上前綴fmt.
,其中fmt
是Println
函數(shù)所處的代碼包的引入名稱。 aImportName.AnExportedIdentifier
這種形式稱為一個(gè)限定標(biāo)識(shí)符(
qualified identifier)。fmt.Println
函數(shù)調(diào)用接受任意數(shù)量的實(shí)參并且對(duì)實(shí)參的類型沒有任何限制。 所以此程序中的此函數(shù)調(diào)用的三個(gè)實(shí)參的類型將被推斷為它們各自的默認(rèn)類型:string
、int
和string
。fmt.Println
函數(shù)調(diào)用,任何兩個(gè)相鄰的實(shí)參的輸出之間將被插入一個(gè)空格字符,并且在最后將輸出一個(gè)空行字符。下面是上面這個(gè)程序的運(yùn)行結(jié)果:
$ go run simple-import-demo.go
Go has 25 keywords.
當(dāng)一個(gè)代碼包被引入一個(gè)Go源文件時(shí),只有此代碼包中的導(dǎo)出代碼要素(名稱為大寫字母的變量、常量、函數(shù)、定義類型和類型別名等)可以在此源文件被使用。 比如上例中的Println
函數(shù)即為一個(gè)導(dǎo)出代碼要素,所以它可以在上面的程序源文件中使用。
前面幾篇文章中使用的內(nèi)置函數(shù)print
和println
提供了和fmt
標(biāo)準(zhǔn)庫(kù)包中的對(duì)應(yīng)函數(shù)相似的功能。 內(nèi)置函數(shù)可以不用引入任何代碼包而直接使用。
注意:print
和println
這兩個(gè)內(nèi)置函數(shù)不推薦使用在生產(chǎn)環(huán)境,因?yàn)樗鼈儾槐WC一定會(huì)出現(xiàn)在以后的Go版本中。
我們可以訪問Go官網(wǎng)(墻內(nèi)版)來查看各個(gè)標(biāo)準(zhǔn)庫(kù)包的文檔, 我們也可以開啟一個(gè)本地文檔服務(wù)器來查看這些文檔。
一個(gè)包引入也可稱為一個(gè)包聲明。一個(gè)包聲明只在當(dāng)前包含此聲明的源文件內(nèi)可見。
另外一個(gè)例子:
package main
import "fmt"
import "math/rand"
func main() {
fmt.Printf("下一個(gè)偽隨機(jī)數(shù)總是%v。\n", rand.Uint32())
}
這個(gè)例子多引入了一個(gè)math/rand
標(biāo)準(zhǔn)庫(kù)包。 此包是math
標(biāo)準(zhǔn)庫(kù)包中的一個(gè)子包。 此包提供了一些函數(shù)來產(chǎn)生偽隨機(jī)數(shù)序列。
一些解釋:
math/rand
標(biāo)準(zhǔn)庫(kù)包的引入名是rand
。 rand.Uint32()
函數(shù)調(diào)用將返回一個(gè)uint32
類型的隨機(jī)數(shù)。Printf
函數(shù)是fmt
標(biāo)準(zhǔn)庫(kù)包中提供的另外一個(gè)常用終端打印函數(shù)。 一個(gè)Printf
函數(shù)調(diào)用必須帶有至少一個(gè)實(shí)參,并且第一個(gè)實(shí)參的類型必須為string
。 此第一個(gè)實(shí)參指定了此調(diào)用的打印格式。此格式中的%v
在打印結(jié)果將被對(duì)應(yīng)的后續(xù)實(shí)參的字符串表示形式所取代。 比如上列中的%v
在打印結(jié)果中將被rand.Uint32()
函數(shù)調(diào)用所返回的隨機(jī)數(shù)所取代。
打印格式中的\n
表示一個(gè)換行符,這在基本類型和它們的字面量表示一文中已經(jīng)解釋過。上面這個(gè)程序的輸出如下:
下一個(gè)偽隨機(jī)數(shù)總是2596996162。
如果我們希望上面的程序每次運(yùn)行的時(shí)候輸出一個(gè)不同的隨機(jī)數(shù),我們需要在程序啟動(dòng)的時(shí)候使用調(diào)用rand.Seed
函數(shù)來設(shè)置一個(gè)不同的隨機(jī)數(shù)種子。
多個(gè)包引入語句可以用一對(duì)小括號(hào)來合并成一個(gè)包引入語句。比如下面這例。
package main
// 一條包引入語句引入了三個(gè)代碼包。
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano()) // 設(shè)置隨機(jī)數(shù)種子
fmt.Printf("下一個(gè)偽隨機(jī)數(shù)是%v。\n", rand.Uint32())
}
一些解釋:
time
標(biāo)準(zhǔn)庫(kù)包。 此包提供了很多和時(shí)間相關(guān)的函數(shù)和類型。 其中time.Time
和time.Duration
是兩個(gè)最常用的類型。time.Now()
將返回一個(gè)表示當(dāng)前時(shí)間的類型為time.Time
的值。UnixNano
是類型time.Time
的一個(gè)方法。 我們可以把方法看作是特殊的函數(shù)。方法將在Go中的方法一文中詳述。 方法調(diào)用aTime.UnixNano()
將返回從UTC時(shí)間的1970年一月一日到aTime
所表示的時(shí)間之間的納秒數(shù)。
返回結(jié)果的類型為int64
。 在上例中,此方法調(diào)用的結(jié)果用來設(shè)置隨機(jī)數(shù)種子。從上面的例子中,我們已經(jīng)了解到fmt.Printf
函數(shù)調(diào)用的第一個(gè)實(shí)參中的%v
在輸出中將替換為后續(xù)的實(shí)參的字符串表示形式。 實(shí)際上,這種百分號(hào)開頭的占位字符組合還有很多。下面是一些常用的占位字符組合:
%v
:將被替換為對(duì)應(yīng)實(shí)參字符串表示形式。%T
:將替換為對(duì)應(yīng)實(shí)參的類型的字符串表示形式。%x
:將替換為對(duì)應(yīng)實(shí)參的十六進(jìn)制表示。實(shí)參的類型可以為字符串、整數(shù)、整數(shù)數(shù)組(array)或者整數(shù)切片(slice)等。 (數(shù)組和切片將在以后的文章中講解。)%s
:將被替換為對(duì)應(yīng)實(shí)參的字符串表示形式。實(shí)參的類型必須為字符串或者字節(jié)切片(byte slice)類型。%%
:將被替換為一個(gè)百分號(hào)。一個(gè)例子:
package main
import "fmt"
func main() {
a, b := 123, "Go"
fmt.Printf("a == %v == 0x%x, b == %s\n", a, a, b)
fmt.Printf("type of a: %T, type of b: %T\n", a, b)
fmt.Printf("1%% 50%% 99%%\n")
}
輸出:
a == 123 == 0x7b, b == Go
type of a: int, type of b: string
1% 50% 99%
請(qǐng)閱讀fmt
標(biāo)準(zhǔn)庫(kù)包的文檔以了解更多的占位字符組合。 我們也可以運(yùn)行go doc fmt
命令來在終端中查看fmt
標(biāo)準(zhǔn)庫(kù)包的文檔。 運(yùn)行go doc fmt.Printf
命令可以查看fmt.Printf
函數(shù)的文檔。
一個(gè)代碼包可以由若干Go源文件組成。一個(gè)代碼包的源文件須都處于同一個(gè)目錄下。 一個(gè)目錄(不包含子目錄)下的所有源文件必須都處于同一個(gè)代碼包中,亦即這些源文件開頭的package pkgname
語句必須一致。 所以,一個(gè)代碼包對(duì)應(yīng)著一個(gè)目錄(不包含子目錄),反之亦然。 對(duì)應(yīng)著一個(gè)代碼包的目錄稱為此代碼包的目錄。 一個(gè)代碼包目錄下的每個(gè)子目錄對(duì)應(yīng)的都是另外一個(gè)獨(dú)立的代碼包。
對(duì)于Go官方工具鏈來說,一個(gè)引入路徑中包含有internal
目錄名的代碼包被視為一個(gè)特殊的代碼包。 它只能被此internal
目錄的直接父目錄(和此父目錄的子目錄)中的代碼包所引入。 比如,代碼包.../a/b/c/internal/d/e/f
和.../a/b/c/internal
只能被引入路徑含有.../a/b/c
前綴的代碼包引入。
當(dāng)一個(gè)代碼包中的某個(gè)文件引入了另外一個(gè)代碼包,則我們說前者代碼包依賴于后者代碼包。
Go不支持循環(huán)引用(依賴)。 如果一個(gè)代碼包a
依賴于代碼包b
,同時(shí)代碼包b
依賴于代碼包c
,則代碼包c
中的源文件不能引入代碼包a
和代碼包b
,代碼包b
中的源文件也不能引入代碼包a
。
當(dāng)然,一個(gè)代碼包中的源文件不能也沒必要引入此代碼包本身。
今后,我們稱一個(gè)程序中含有main
入口函數(shù)的名稱為main
的代碼包為程序代碼包(或者命令代碼包),稱其它代碼包為庫(kù)代碼包。 程序代碼包不能被其它代碼包引入。一個(gè)程序只能有一個(gè)程序代碼包。
代碼包目錄的名稱并不要求一定要和其對(duì)應(yīng)的代碼包的名稱相同。 但是,庫(kù)代碼包目錄的名稱最好設(shè)為和其對(duì)應(yīng)的代碼包的名稱相同。 因?yàn)橐粋€(gè)代碼包的引入路徑中包含的是此包的目錄名,但是此包的默認(rèn)引入名為此包的名稱。 如果兩者不一致,會(huì)使人感到困惑。
另一方面,最好給每個(gè)程序代碼包目錄指定一個(gè)有意義的名字,而不是它的包名main
。
在一個(gè)代碼包中,甚至一個(gè)源文件中,可以聲明若干名為init
的函數(shù)。 這些init
函數(shù)必須不帶任何輸入?yún)?shù)和返回結(jié)果。
注意:我們不能聲明名為init
的包級(jí)變量、常量或者類型。
在程序運(yùn)行時(shí)刻,在進(jìn)入main
入口函數(shù)之前,每個(gè)init
函數(shù)在此包加載的時(shí)候?qū)⒈唬ù校﹫?zhí)行并且只執(zhí)行一遍。
下面這個(gè)簡(jiǎn)單的程序中有兩個(gè)init
函數(shù):
package main
import "fmt"
func init() {
fmt.Println("hi,", bob)
}
func main() {
fmt.Println("bye")
}
func init() {
fmt.Println("hello,", smith)
}
func titledName(who string) string {
return "Mr. " + who
}
var bob, smith = titledName("Bob"), titledName("Smith")
此程序的運(yùn)行結(jié)果:
hi, Mr. Bob
hello, Mr. Smith
bye
一個(gè)程序中所涉及到的所有的在運(yùn)行時(shí)刻要用到的代碼包的加載是串行執(zhí)行的。 在一個(gè)程序啟動(dòng)時(shí),每個(gè)包中總是在它所有依賴的包都加載完成之后才開始加載。 程序代碼包總是最后一個(gè)被加載的代碼包。每個(gè)被用到的包會(huì)被而且僅會(huì)被加載一次。
在加載一個(gè)代碼包的過程中,所有的聲明在此包中的init
函數(shù)將被串行調(diào)用并且僅調(diào)用執(zhí)行一次。 一個(gè)代碼包中聲明的init
函數(shù)的調(diào)用肯定晚于此代碼包所依賴的代碼包中聲明的init
函數(shù)。 所有的init
函數(shù)都將在調(diào)用main
入口函數(shù)之前被調(diào)用執(zhí)行。
在同一個(gè)源文件中聲明的init
函數(shù)將按從上到下的順序被調(diào)用執(zhí)行。 對(duì)于聲明在同一個(gè)包中的兩個(gè)不同源文件中的兩個(gè)init
函數(shù),Go語言白皮書推薦(但不強(qiáng)求)按照它們所處于的源文件的名稱的詞典序列(對(duì)英文來說,即字母順序)來調(diào)用。 所以最好不要讓聲明在同一個(gè)包中的兩個(gè)不同源文件中的兩個(gè)init
函數(shù)存在依賴關(guān)系。
在加載一個(gè)代碼包的時(shí)候,此代碼包中聲明的所有包級(jí)變量都將在此包中的任何一個(gè)init
函數(shù)執(zhí)行之前初始化完畢。
在同一個(gè)包內(nèi),包級(jí)變量將盡量按照它們?cè)诖a中的出現(xiàn)順序被初始化,但是一個(gè)包級(jí)變量的初始化肯定晚于它所依賴的其它包級(jí)變量。 比如,在下面的代碼片段中,四個(gè)包級(jí)變量的初始化順序依次為y
、z
、x
、w
。
func f() int {
return z + y
}
func g() int {
return y/2
}
var (
w = x
x, y, z = f(), 123, g()
)
關(guān)于更具體的包級(jí)變量的初始化順序,請(qǐng)閱讀表達(dá)式估值順序規(guī)則一文。
事實(shí)上,一個(gè)引入聲明語句的完整形式為:
import importname "path/to/package"
其中引入名importname
是可選的,它的默認(rèn)值為被引入的包的包名(不是目錄名)。
事實(shí)上,在本文上面的例子中的包引入聲明中,importname
部分都被省略掉了,因?yàn)樗鼈兌挤謩e和引入的代碼包的包名相同。 這些引入聲明等價(jià)于下面這些:
import fmt "fmt" // <=> import "fmt"
import rand "math/rand" // <=> import "math/rand"
import time "time" // <=> import "time"
如果一個(gè)包引入聲明中的importname
沒有省略,則限定標(biāo)識(shí)符使用的前綴必須為importname
,而不是被引入的包的名稱。
引入聲明語句的完整形式在日常編程中使用的頻率不是很高。 但是在某些情況下,完整形式必須被使用。 比如,如果一個(gè)源文件引入的兩個(gè)代碼包的包名一樣,為了防止使編譯器產(chǎn)生困惑,我們至少需要用完整形式為其中一個(gè)包指定一個(gè)不同的引入名以區(qū)分這兩個(gè)包。
下面是一個(gè)使用了完整引入聲明語句形式的例子。
package main
import (
format "fmt"
random "math/rand"
"time"
)
func main() {
random.Seed(time.Now().UnixNano())
format.Print("一個(gè)隨機(jī)數(shù):", random.Uint32(), "\n")
// 下面這兩行編譯不通過,因?yàn)閞and不可識(shí)別。
/*
rand.Seed(time.Now().UnixNano())
fmt.Print("一個(gè)隨機(jī)數(shù):", rand.Uint32(), "\n")
*/
}
一些解釋:
format
和random
,而不是fmt
和rand
,來做為限定標(biāo)識(shí)符的前綴。
Print
是fmt
標(biāo)準(zhǔn)庫(kù)包中的另外一個(gè)函數(shù)。
和Println
函數(shù)調(diào)用一樣,一個(gè)Print
函數(shù)調(diào)用也接受任意數(shù)量實(shí)參。
它將逐個(gè)打印出每個(gè)實(shí)參的字符串表示形式。如果相鄰的兩個(gè)實(shí)參都不是字符串類型,則在它們中間會(huì)打印一個(gè)空格字符。
一個(gè)完整引入聲明語句形式的引入名importname
可以是一個(gè)句點(diǎn)(.
)。 這樣的引入稱為句點(diǎn)引入。使用被句點(diǎn)引入的包中的導(dǎo)出代碼要素時(shí),限定標(biāo)識(shí)符的前綴必須省略。
例子:
package main
import (
. "fmt"
. "time"
)
func main() {
Println("Current time:", Now())
}
在上面這個(gè)例子中,Println
和Now
函數(shù)調(diào)用不需要帶任何前綴。
一般來說,句點(diǎn)引入不推薦使用,因?yàn)樗鼈儠?huì)導(dǎo)致較低的代碼可讀性。
一個(gè)完整引入聲明語句形式的引入名importname
可以是一個(gè)空標(biāo)識(shí)符(_
)。 這樣的引入稱為匿名引入。一個(gè)包被匿名引入的目的主要是為了加載這個(gè)包,從而使得這個(gè)包中的代碼要素得以初始化。 被匿名引入的包中的init
函數(shù)將被執(zhí)行并且僅執(zhí)行一遍。
在下面這個(gè)例子中,net/http/pprof
標(biāo)準(zhǔn)庫(kù)包中的所有init
函數(shù)將在main
入口函數(shù)開始執(zhí)行之前全部執(zhí)行一遍。
package main
import _ "net/http/pprof"
func main() {
... // 做一些事情
}
除了匿名引入,其它引入必須在代碼中被使用一次。 比如,下面的程序編譯不通過。
package main
import (
"net/http" // error: 引入未被使用
. "time" // error: 引入未被使用
)
import (
format "fmt" // okay: 下面被使用了一次
_ "math/rand" // okay: 匿名引入
)
func main() {
format.Println() // 使用"fmt"包
}
一個(gè)模塊(module)為的若干代碼包的集合。當(dāng)被下載至本地后,這些代碼包處于同一個(gè)目錄(此模塊的根目錄)下。 一個(gè)模塊可以有很多版本(版本號(hào)遵從Semantic Versioning規(guī)范)。 更多關(guān)于模塊的概念和使用,請(qǐng)閱讀官方文檔。
更多建議: