1. 程式人生 > 其它 >Golang語言--包的概念、匯入與可見性

Golang語言--包的概念、匯入與可見性

go中包的概念、匯入與可見性

包是結構化程式碼的一種方式:每個程式都由包(通常簡稱為 pkg)的概念組成,可以使用自身的包或者從其它包中匯入內容。

如同其它一些程式語言中的類庫或名稱空間的概念,每個 Go 檔案都屬於且僅屬於一個包。一個包可以由許多以 .go 為副檔名的原始檔組成,因此檔名和包名一般來說都是不相同的。

你必須在原始檔中非註釋的第一行指明這個檔案屬於哪個包,如:package mainpackage main表示一個可獨立執行的程式,每個 Go 應用程式都包含一個名為 main 的包。

一個應用程式可以包含不同的包,而且即使你只使用 main 包也不必把所有的程式碼都寫在一個巨大的檔案裡:你可以用一些較小的檔案,並且在每個檔案非註釋的第一行都使用 package main

來指明這些檔案都屬於 main 包。如果你打算編譯包名不是為 main 的原始檔,如 pack1,編譯後產生的物件檔案將會是 pack1.a 而不是可執行程式。另外要注意的是,所有的包名都應該使用小寫字母。

標準庫

在 Go 的安裝檔案裡包含了一些可以直接使用的包,即標準庫。在 Windows 下,標準庫的位置在 Go 根目錄下的子目錄pkgwindows_386 中;在 Linux 下,標準庫在 Go 根目錄下的子目錄 pkglinux_amd64 中(如果是安裝的是 32 位,則在 linux_386 目錄中)。一般情況下,標準包會存放在 $GOROOT/pkg/$GOOS_$GOARCH/ 目錄下。

Go 的標準庫包含了大量的包(如:fmt 和 os),但是你也可以建立自己的包。

如果想要構建一個程式,則包和包內的檔案都必須以正確的順序進行編譯。包的依賴關係決定了其構建順序。

屬於同一個包的原始檔必須全部被一起編譯,一個包即是編譯時的一個單元,因此根據慣例,每個目錄都只包含一個包。

如果對一個包進行更改或重新編譯,所有引用了這個包的客戶端程式都必須全部重新編譯。

Go 中的包模型採用了顯式依賴關係的機制來達到快速編譯的目的,編譯器會從字尾名為 .o 的物件檔案(需要且只需要這個檔案)中提取傳遞依賴型別的資訊。

如果 A.go 依賴 B.go,而 B.go 又依賴 C.go

  • 編譯 C.go
    , B.go, 然後是 A.go.
  • 為了編譯 A.go, 編譯器讀取的是 B.o 而不是 C.o.

這種機制對於編譯大型的專案時可以顯著地提升編譯速度。

每一段程式碼只會被編譯一次

一個 Go 程式是通過 import 關鍵字將一組包連結在一起。

import "fmt" 告訴 Go 編譯器這個程式需要使用 fmt 包(的函式,或其他元素),fmt 包實現了格式化 IO(輸入/輸出)的函式。包名被封閉在半形雙引號 "" 中。如果你打算從已編譯的包中匯入並載入公開宣告的方法,不需要插入已編譯包的原始碼。

如果需要多個包,它們可以被分別匯入:

import "fmt"import "os"

或:

import "fmt"; import "os"

但是還有更短且更優雅的方法(被稱為因式分解關鍵字,該方法同樣適用於 const、var 和 type 的宣告或定義):

import (   "fmt"   "os"
)

它甚至還可以更短的形式,但使用 gofmt 後將會被強制換行:

import ("fmt"; "os")

當你匯入多個包時,匯入的順序會按照字母排序。

如果包名不是以 ./ 開頭,如 "fmt" 或者 "container/list",則 Go 會在全域性檔案進行查詢;如果包名以 ./ 開頭,則 Go 會在相對目錄中查詢;如果包名以 / 開頭(在 Windows 下也可以這樣使用),則會在系統的絕對路徑中查詢。

匯入包即等同於包含了這個包的所有的程式碼物件。

除了符號 _,包中所有程式碼物件的識別符號必須是唯一的,以避免名稱衝突。但是相同的識別符號可以在不同的包中使用,因為可以使用包名來區分它們。

包通過下面這個被編譯器強制執行的規則來決定是否將自身的程式碼物件暴露給外部檔案:

可見性規則

當識別符號(包括常量、變數、型別、函式名、結構欄位等等)以一個大寫字母開頭,如:Group1,那麼使用這種形式的識別符號的物件就可以被外部包的 程式碼所使用(客戶端程式需要先匯入這個包),這被稱為匯出(像面嚮物件語言中的 public);識別符號如果以小寫字母開頭,則對包外是不可見的,但是他們在整個包的內部是可見並且可用的(像面嚮物件語言中的 private )。

(大寫字母可以使用任何 Unicode 編碼的字元,比如希臘文,不僅僅是 ASCII 碼中的大寫字母)。

因此,在匯入一個外部包後,能夠且只能夠訪問該包中匯出的物件。

假設在包 pack1 中我們有一個變數或函式叫做 Thing(以 T 開頭,所以它能夠被匯出),那麼在當前包中匯入 pack1 包,Thing 就可以像面嚮物件語言那樣使用點標記來呼叫:pack1.Thing(pack1 在這裡是不可以省略的)。

因此包也可以作為名稱空間使用,幫助避免命名衝突(名稱衝突):兩個包中的同名變數的區別在於他們的包名,例如pack1.Thingpack2.Thing

你可以通過使用包的別名來解決包名之間的名稱衝突,或者說根據你的個人喜好對包名進行重新設定,如:import fm "fmt"。下面的程式碼展示瞭如何使用包的別名:

示例 4.2 alias.go

package mainimport fm "fmt" // alias3
func main() {
   fm.Println("hello, world")
}

注意事項

如果你匯入了一個包卻沒有使用它,則會在構建程式時引發錯誤,如 imported and not used: os,這正是遵循了 Go 的格言:“沒有不必要的程式碼!“。

包的分級宣告和初始化

你可以在使用 import 匯入包之後定義或宣告 0 個或多個常量(const)、變數(var)和型別(type),這些物件的作用域都是全域性的(在本包範圍內),所以可以被本包中所有的函式呼叫(如 gotemplate.go 原始檔中的 c 和 v),然後宣告一個或多個函式(func)。

Go 程式的一般結構

下面的程式可以被順利編譯但什麼都做不了,不過這很好地展示了一個 Go 程式的首選結構。這種結構並沒有被強制要求,編譯器也不關心 main 函式在前還是變數的宣告在前,但使用統一的結構能夠在從上至下閱讀 Go 程式碼時有更好的體驗。

所有的結構將在這一章或接下來的章節中進一步地解釋說明,但總體思路如下:

  • 在完成包的 import 之後,開始對常量、變數和型別的定義或宣告。
  • 如果存在 init 函式的話,則對該函式進行定義(這是一個特殊的函式,每個含有該函式的包都會首先執行這個函式)。
  • 如果當前包是 main 包,則定義 main 函式。
  • 然後定義其餘的函式,首先是型別的方法,接著是按照 main 函式中先後呼叫的順序來定義相關函式,如果有很多函式,則可以按照字母順序來進行排序。

示例 4.4 gotemplate.go

package main
import (   "fmt"
)
const c = "C"
var v int = 5
type T struct{}
func init() { // initialization of package
}
func main() {   var a int   Func1()   
   // ...
   fmt.Println(a)
}
func (t T) Method1() {   
//...
}
func Func1() { // exported function Func1   
//...
}

Go 程式的執行(程式啟動)順序如下:

  1. 按順序匯入所有被 main 包引用的其它包,然後在每個包中執行如下流程:
  2. 如果該包又匯入了其它的包,則從第一步開始遞迴執行,但是每個包只會被匯入一次。
  3. 然後以相反的順序在每個包中初始化常量和變數,如果該包含有 init 函式的話,則呼叫該函式。
  4. 在完成這一切之後,main 也執行同樣的過程,最後呼叫 main 函式開始執行程式。