Go語言 代碼包和包引入

2023-02-16 17:36 更新

和很多現代編程語言一樣,Go代碼包(package)來組織管理代碼。 我們必須先引入一個代碼包(除了?builtin?標準庫包)才能使用其中導出的代碼要素(比如函數、類型、變量和具名常量等)。 此篇文章將講解Go代碼包和代碼包引入(import)。

包引入

下面這個簡短的程序(假設它存在一個名為simple-import-demo.go的源文件中)引入了一個標準庫包。

package main

import "fmt"

func main() {
	fmt.Println("Go has", 25, "keywords.")
}

對此程序的一些解釋:

  • 第一行指定了源文件simple-import-demo.go所處的包名為main。 程序入口main函數必須處于一個名為main的代碼包中。
  • 第三行通過使用import關鍵字引入了fmt標準庫包。 在此源文件中,fmt標準庫包將用fmt標識符來表示。 標識符fmt稱為fmt標準庫包的引入名稱。(后續(xù)某節(jié)將詳述代碼包的引入名稱)。
  • fmt標準庫包中聲明了很多終端打印函數供其它代碼包使用。 Println函數是其中之一。 它可以將不定數量參數的字符串表示形式輸出到標準輸出中。 第六行調用了此Println函數。 注意在此調用中,函數名之前需要帶上前綴fmt.,其中fmtPrintln函數所處的代碼包的引入名稱。 aImportName.AnExportedIdentifier這種形式稱為一個限定標識符( qualified identifier)。
  • fmt.Println函數調用接受任意數量的實參并且對實參的類型沒有任何限制。 所以此程序中的此函數調用的三個實參的類型將被推斷為它們各自的默認類型:string、intstring
  • 對于一個fmt.Println函數調用,任何兩個相鄰的實參的輸出之間將被插入一個空格字符,并且在最后將輸出一個空行字符。

下面是上面這個程序的運行結果:

$ go run simple-import-demo.go
Go has 25 keywords.

當一個代碼包被引入一個Go源文件時,只有此代碼包中的導出代碼要素(名稱為大寫字母的變量、常量、函數、定義類型和類型別名等)可以在此源文件被使用。 比如上例中的Println函數即為一個導出代碼要素,所以它可以在上面的程序源文件中使用。

前面幾篇文章中使用的內置函數printprintln提供了和fmt標準庫包中的對應函數相似的功能。 內置函數可以不用引入任何代碼包而直接使用。

注意:printprintln這兩個內置函數不推薦使用在生產環(huán)境,因為它們不保證一定會出現在以后的Go版本中。

我們可以訪問Go官網墻內版)來查看各個標準庫包的文檔, 我們也可以開啟一個本地文檔服務器來查看這些文檔。

一個包引入也可稱為一個包聲明。一個包聲明只在當前包含此聲明的源文件內可見。

另外一個例子:

package main

import "fmt"
import "math/rand"

func main() {
	fmt.Printf("下一個偽隨機數總是%v。\n", rand.Uint32())
}

這個例子多引入了一個math/rand標準庫包。 此包是math標準庫包中的一個子包。 此包提供了一些函數來產生偽隨機數序列。

一些解釋:

  • 在此例中,math/rand標準庫包的引入名是randrand.Uint32()函數調用將返回一個uint32類型的隨機數。
  • Printf函數是fmt標準庫包中提供的另外一個常用終端打印函數。 一個Printf函數調用必須帶有至少一個實參,并且第一個實參的類型必須為string。 此第一個實參指定了此調用的打印格式。此格式中的%v在打印結果將被對應的后續(xù)實參的字符串表示形式所取代。 比如上列中的%v在打印結果中將被rand.Uint32()函數調用所返回的隨機數所取代。 打印格式中的\n表示一個換行符,這在基本類型和它們的字面量表示一文中已經解釋過。

上面這個程序的輸出如下:

下一個偽隨機數總是2596996162。

如果我們希望上面的程序每次運行的時候輸出一個不同的隨機數,我們需要在程序啟動的時候使用調用rand.Seed函數來設置一個不同的隨機數種子。

多個包引入語句可以用一對小括號來合并成一個包引入語句。比如下面這例。

package main

// 一條包引入語句引入了三個代碼包。
import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	rand.Seed(time.Now().UnixNano()) // 設置隨機數種子
	fmt.Printf("下一個偽隨機數是%v。\n", rand.Uint32())
}

一些解釋:

  • 此例多引入了一個time標準庫包。 此包提供了很多和時間相關的函數和類型。 其中time.Timetime.Duration是兩個最常用的類型。
  • 函數調用time.Now()將返回一個表示當前時間的類型為time.Time的值。
  • UnixNano是類型time.Time的一個方法。 我們可以把方法看作是特殊的函數。方法將在Go中的方法一文中詳述。 方法調用aTime.UnixNano()將返回從UTC時間的1970年一月一日到aTime所表示的時間之間的納秒數。 返回結果的類型為int64。 在上例中,此方法調用的結果用來設置隨機數種子。

更多關于fmt.Printf函數調用的輸出格式

從上面的例子中,我們已經了解到fmt.Printf函數調用的第一個實參中的%v在輸出中將替換為后續(xù)的實參的字符串表示形式。 實際上,這種百分號開頭的占位字符組合還有很多。下面是一些常用的占位字符組合:

  • %v:將被替換為對應實參字符串表示形式。
  • %T:將替換為對應實參的類型的字符串表示形式。
  • %x:將替換為對應實參的十六進制表示。實參的類型可以為字符串、整數、整數數組(array)或者整數切片(slice)等。 (數組和切片將在以后的文章中講解。)
  • %s:將被替換為對應實參的字符串表示形式。實參的類型必須為字符串或者字節(jié)切片(byte slice)類型。
  • %%:將被替換為一個百分號。

一個例子:

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%

請閱讀fmt標準庫包的文檔以了解更多的占位字符組合。 我們也可以運行go doc fmt命令來在終端中查看fmt標準庫包的文檔。 運行go doc fmt.Printf命令可以查看fmt.Printf函數的文檔。

代碼包目錄、代碼包引入路徑和代碼包依賴關系

一個代碼包可以由若干Go源文件組成。一個代碼包的源文件須都處于同一個目錄下。 一個目錄(不包含子目錄)下的所有源文件必須都處于同一個代碼包中,亦即這些源文件開頭的package pkgname語句必須一致。 所以,一個代碼包對應著一個目錄(不包含子目錄),反之亦然。 對應著一個代碼包的目錄稱為此代碼包的目錄。 一個代碼包目錄下的每個子目錄對應的都是另外一個獨立的代碼包。

對于Go官方工具鏈來說,一個引入路徑中包含有internal目錄名的代碼包被視為一個特殊的代碼包。 它只能被此internal目錄的直接父目錄(和此父目錄的子目錄)中的代碼包所引入。 比如,代碼包.../a/b/c/internal/d/e/f.../a/b/c/internal只能被引入路徑含有.../a/b/c前綴的代碼包引入。

當一個代碼包中的某個文件引入了另外一個代碼包,則我們說前者代碼包依賴于后者代碼包。

Go不支持循環(huán)引用(依賴)。 如果一個代碼包a依賴于代碼包b,同時代碼包b依賴于代碼包c,則代碼包c中的源文件不能引入代碼包a和代碼包b,代碼包b中的源文件也不能引入代碼包a

當然,一個代碼包中的源文件不能也沒必要引入此代碼包本身。

今后,我們稱一個程序中含有main入口函數的名稱為main的代碼包為程序代碼包(或者命令代碼包),稱其它代碼包為庫代碼包。 程序代碼包不能被其它代碼包引入。一個程序只能有一個程序代碼包。

代碼包目錄的名稱并不要求一定要和其對應的代碼包的名稱相同。 但是,庫代碼包目錄的名稱最好設為和其對應的代碼包的名稱相同。 因為一個代碼包的引入路徑中包含的是此包的目錄名,但是此包的默認引入名為此包的名稱。 如果兩者不一致,會使人感到困惑。

另一方面,最好給每個程序代碼包目錄指定一個有意義的名字,而不是它的包名main

init函數

在一個代碼包中,甚至一個源文件中,可以聲明若干名為init的函數。 這些init函數必須不帶任何輸入參數和返回結果。

注意:我們不能聲明名為init的包級變量、常量或者類型。

在程序運行時刻,在進入main入口函數之前,每個init函數在此包加載的時候將被(串行)執(zhí)行并且只執(zhí)行一遍。

下面這個簡單的程序中有兩個init函數:

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")

此程序的運行結果:

hi, Mr. Bob
hello, Mr. Smith
bye

程序代碼要素初始化順序

一個程序中所涉及到的所有的在運行時刻要用到的代碼包的加載是串行執(zhí)行的。 在一個程序啟動時,每個包中總是在它所有依賴的包都加載完成之后才開始加載。 程序代碼包總是最后一個被加載的代碼包。每個被用到的包會被而且僅會被加載一次。

在加載一個代碼包的過程中,所有的聲明在此包中的init函數將被串行調用并且僅調用執(zhí)行一次。 一個代碼包中聲明的init函數的調用肯定晚于此代碼包所依賴的代碼包中聲明的init函數。 所有的init函數都將在調用main入口函數之前被調用執(zhí)行。

在同一個源文件中聲明的init函數將按從上到下的順序被調用執(zhí)行。 對于聲明在同一個包中的兩個不同源文件中的兩個init函數,Go語言白皮書推薦(但不強求)按照它們所處于的源文件的名稱的詞典序列(對英文來說,即字母順序)來調用。 所以最好不要讓聲明在同一個包中的兩個不同源文件中的兩個init函數存在依賴關系。

在加載一個代碼包的時候,此代碼包中聲明的所有包級變量都將在此包中的任何一個init函數執(zhí)行之前初始化完畢。

在同一個包內,包級變量將盡量按照它們在代碼中的出現順序被初始化,但是一個包級變量的初始化肯定晚于它所依賴的其它包級變量。 比如,在下面的代碼片段中,四個包級變量的初始化順序依次為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ī)則一文。

完整的引入聲明語句形式

事實上,一個引入聲明語句的完整形式為:

import importname "path/to/package"

其中引入名importname是可選的,它的默認值為被引入的包的包名(不是目錄名)。

事實上,在本文上面的例子中的包引入聲明中,importname部分都被省略掉了,因為它們都分別和引入的代碼包的包名相同。 這些引入聲明等價于下面這些:

import fmt "fmt"        // <=> import "fmt"
import rand "math/rand" // <=> import "math/rand"
import time "time"      // <=> import "time"

如果一個包引入聲明中的importname沒有省略,則限定標識符使用的前綴必須為importname,而不是被引入的包的名稱。

引入聲明語句的完整形式在日常編程中使用的頻率不是很高。 但是在某些情況下,完整形式必須被使用。 比如,如果一個源文件引入的兩個代碼包的包名一樣,為了防止使編譯器產生困惑,我們至少需要用完整形式為其中一個包指定一個不同的引入名以區(qū)分這兩個包。

下面是一個使用了完整引入聲明語句形式的例子。

package main

import (
	format "fmt"
	random "math/rand"
	"time"
)

func main() {
	random.Seed(time.Now().UnixNano())
	format.Print("一個隨機數:", random.Uint32(), "\n")

	// 下面這兩行編譯不通過,因為rand不可識別。
	/*
	rand.Seed(time.Now().UnixNano())
	fmt.Print("一個隨機數:", rand.Uint32(), "\n")
	*/
}

一些解釋:

  • 我們必須使用formatrandom,而不是fmtrand,來做為限定標識符的前綴。
  • Printfmt標準庫包中的另外一個函數。 和Println函數調用一樣,一個Print函數調用也接受任意數量實參。 它將逐個打印出每個實參的字符串表示形式。如果相鄰的兩個實參都不是字符串類型,則在它們中間會打印一個空格字符。

一個完整引入聲明語句形式的引入名importname可以是一個句點(.)。 這樣的引入稱為句點引入。使用被句點引入的包中的導出代碼要素時,限定標識符的前綴必須省略。

例子:

package main

import (
	. "fmt"
	. "time"
)

func main() {
	Println("Current time:", Now())
}

在上面這個例子中,PrintlnNow函數調用不需要帶任何前綴。

一般來說,句點引入不推薦使用,因為它們會導致較低的代碼可讀性。

一個完整引入聲明語句形式的引入名importname可以是一個空標識符(_)。 這樣的引入稱為匿名引入。一個包被匿名引入的目的主要是為了加載這個包,從而使得這個包中的代碼要素得以初始化。 被匿名引入的包中的init函數將被執(zhí)行并且僅執(zhí)行一遍。

在下面這個例子中,net/http/pprof標準庫包中的所有init函數將在main入口函數開始執(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"包
}

模塊

一個模塊(module)為的若干代碼包的集合。當被下載至本地后,這些代碼包處于同一個目錄(此模塊的根目錄)下。 一個模塊可以有很多版本(版本號遵從Semantic Versioning規(guī)范)。 更多關于模塊的概念和使用,請閱讀官方文檔。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號