Go第八篇之包的使用

 

Go 語言的源碼複用創建在包(package)基礎之上。Go 語言的入口 main() 函數所在的包(package)叫 main,main 包想要引用別的代碼,必須一樣以包的方式進行引用,本章內容將詳細講解如何導出包的內容及如何導入其餘包。
Go 語言的包與文件夾一一對應,全部與包相關的操做,必須依賴於工做目錄(GOPATH)。java

Go語言GOPATH

GOPATH 是 Go 語言中使用的一個環境變量,它使用絕對路徑提供項目的工做目錄。

工做目錄是一個工程開發的相對參考目錄,比如當你要在公司編寫一套服務器代碼,你的工位所包含的桌面、計算機及椅子就是你的工做區。工做區的概念與工做目錄的概念也是相似的。若是不使用工做目錄的概念,在多人開發時,每一個人有一套本身的目錄結構,讀取配置文件的位置不統一,輸出的二進制運行文件也不統一,這樣會致使開發的標準不統一,影響開發效率。

GOPATH 適合處理大量 Go 語言源碼、多個包組合而成的複雜工程。linux

提示

C、C++Java、C# 及其餘語言發展到後期,都擁有本身的 IDE(集成開發環境),而且工程(Project)、解決方案(Solution)和工做區(Workspace)等概念將源碼和資源組織了起來,方便編譯和輸出。golang

使用命令行查看GOPATH信息

在安裝過 Go 開發包的操做系統中,可使用命令行查看 Go 開發包的環境變量配置信息,這些配置信息裏能夠查看到當前的 GOPATH 路徑設置狀況。在命令行中運行go env後,命令行將提示如下信息:緩存

$ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/davy/go"
GORACE=""
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0"
CXX="g++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"服務器

命令行說明以下:架構

  • 第 1 行,執行 go env 指令,將輸出當前 Go 開發包的環境變量狀態。
  • 第 2 行,GOARCH 表示目標處理器架構。
  • 第 3 行,GOBIN 表示編譯器和連接器的安裝位置。
  • 第 7 行,GOOS 表示目標操做系統。
  • 第 8 行,GOPATH 表示當前工做目錄。
  • 第 10 行,GOROOT 表示 Go 開發包的安裝目錄。


從命令行輸出中,能夠看到 GOPATH 設定的路徑爲:/home/davy/go(davy 爲筆者的用戶名)。

在 Go 1.8 版本以前,GOPATH 環境變量默認是空的。從 Go 1.8 版本開始,Go 開發包在安裝完成後,將 GOPATH 賦予了一個默認的目錄,參見下表。
編輯器

GOPATH 在不一樣平臺上的安裝路徑
平  臺 GOPATH 默認值 舉 例
Windows 平臺 %USERPROFILE%/go C:\Users\用戶名\go
Unix 平臺 $HOME/go /home/用戶名/go

使用GOPATH的工程結構

在 GOPATH 指定的工做目錄下,代碼老是會保存在 $GOPATH/src 目錄下。在工程通過 go build、go install 或 go get 等指令後,會將產生的二進制可執行文件放在 $GOPATH/bin 目錄下,生成的中間緩存文件會被保存在 $GOPATH/pkg 下。

若是須要將整個源碼添加到版本管理工具(Version Control System,VCS)中時,只須要添加 $GOPATH/src 目錄的源碼便可。bin 和 pkg 目錄的內容均可以由 src 目錄生成。ide

設置和使用GOPATH

本節以 Linux 爲演示平臺,爲你們演示使用 GOPATH 的方法。函數

1) 設置當前目錄爲GOPATH

選擇一個目錄,在目錄中的命令行中執行下面的指令:工具

export GOPATH=`pwd`

該指令中的 pwd 將輸出當前的目錄,使用反引號`將 pwd 指令括起來表示命令行替換,也就是說,使用`pwd`將得到 pwd 返回的當前目錄的值。例如,假設你的當前目錄是「/home/davy/go」,那麼使用`pwd`將得到返回值「/home/davy/go」。

使用 export 指令能夠將當前目錄的值設置到環境變量 GOPATH中。

2) 創建GOPATH中的源碼目錄

使用下面的指令建立 GOPATH 中的 src 目錄,在 src 目錄下還有一個 hello 目錄,該目錄用於保存源碼。

mkdir -p src/hello

mkdir 指令的 -p 能夠連續建立一個路徑。

3) 添加main.go源碼文件

使用 Linux 編輯器將下面的源碼保存爲 main.go 並保存到 $GOPATH/src/hello 目錄下。

  1. package main
  2. import "fmt"
  3. func main(){
  4. fmt.Println("hello")
  5. }

4) 編譯源碼並運行

此時咱們已經設定了 GOPATH,所以在 Go 語言中能夠經過 GOPATH 找到工程的位置。

在命令行中執行以下指令編譯源碼:

go install hello

編譯完成的可執行文件會保存在 $GOPATH/bin 目錄下。

在 bin 目錄中執行 ./hello,命令行輸出以下:
hello world

在多項目工程中使用GOPATH

在不少與 Go 語言相關的書籍、文章中描述的 GOPATH 都是經過修改系統全局的環境變量來實現的。然而,根據筆者多年的 Go 語言使用和實踐經驗及周邊朋友、同事的反饋,這種設置全局 GOPATH 的方法可能會致使當前項目錯誤引用了其餘目錄的 Go 源碼文件從而形成編譯輸出錯誤的版本或編譯報出一些沒法理解的錯誤提示。

好比說,將某項目代碼保存在 /home/davy/projectA 目錄下,將該目錄設置爲 GOPATH。隨着開發進行,須要再次獲取一份工程項目的源碼,此時源碼保存在 /home/davy/projectB 目錄下,若是此時須要編譯 projectB 目錄的項目,但開發者忘記設置 GOPATH 而直接使用命令行編譯,則當前的 GOPATH 指向的是 /home/davy/projectA 目錄,而不是開發者編譯時指望的 projectB 目錄。編譯完成後,開發者就會將錯誤的工程版本發佈到外網。

所以,建議你們不管是使用命令行或者使用集成開發環境編譯 Go 源碼時,GOPATH 跟隨項目設定。在 Jetbrains 公司的 GoLand 集成開發環境(IDE)中的 GOPATH 設置分爲全局 GOPATH 和項目 GOPATH,以下圖所示。


圖:全局和項目GOPATH


圖中的 Global GOPATH 表明全局 GOPATH,通常來源於系統環境變量中的 GOPATH;Project GOPATH 表明項目所使用的 GOPATH,該設置會被保存在工做目錄的 .idea 目錄下,不會被設置到環境變量的 GOPATH 中,但會在編譯時使用到這個目錄。建議在開發時只填寫項目 GOPATH,每個項目儘可能只設置一個 GOPATH,不使用多個 GOPATH 和全局的 GOPATH。

提示

Visual Studio 早期在設計時,容許 C++ 語言在全局擁有一個包含路徑。當一個工程多個版本的編譯,或者兩個項目混雜有不一樣的共享全局包含時,會發生難以察覺的錯誤。在新版本 Visual Studio 中已經廢除了這種全局包含的路徑設計,並建議開發者將包含目錄與項目關聯。

Go 語言中的 GOPATH 也是一種相似全局包含的設計,所以鑑於 Visual Studio 在設計上的失誤,建議開發者不要設置全局的 GOPATH,而是隨項目設置 GOPATH。

 

 

Go語言package

包(package)是多個 Go 源碼的集合,是一種高級的代碼複用方案,Go 語言默認爲咱們提供了不少包,如 fmt、os、io 包等,開發者能夠根據本身的須要建立本身的包。

包要求在同一個目錄下的全部文件的第一行添加以下代碼,以標記該文件歸屬的包:

package 包名

包的特性以下:

    • 一個目錄下的同級文件歸屬一個包。
    • 包名能夠與其目錄不一樣名。
    • 包名爲 main 的包爲應用程序的入口包,編譯源碼沒有 main 包時,將沒法編譯輸出可執行的文件。

 

 

 

 

Go語言導出包中的標識符

Go 語言中,若是想在一個包裏引用另一個包裏的標識符(如類型、變量、常量等)時,必須首先將被引用的標識符導出,將要導出的標識符的首字母大寫就可讓引用者能夠訪問這些標識符了。

導出包內標識符

下面代碼中包含一系列未導出標識符,它們的首字母都爲小寫,這些標識符能夠在包內自由使用,可是包外沒法訪問它們,代碼以下:

  1. package mypkg
  2. var myVar = 100
  3. const myConst = "hello"
  4. type myStruct struct {
  5. }

將 myStruct 和 myConst 首字母大寫,導出這些標識符,修改後代碼以下:

  1. package mypkg
  2. var myVar = 100
  3. const MyConst = "hello"
  4. type MyStruct struct {
  5. }

此時,MyConst 和 MyStruct 能夠被外部訪問,而 myVar 因爲首字母是小寫,所以只能在 mypkg 包內使用,不能被外部包引用。

導出結構體及接口成員

在被導出的結構體或接口中,若是它們的字段或方法首字母是大寫,外部能夠訪問這些字段和方法,代碼以下:

  1. type MyStruct struct {
  2. // 包外能夠訪問的字段
  3. ExportedField int
  4. // 僅限包內訪問的字段
  5. privateField int
  6. }
  7. type MyInterface interface {
  8. // 包外能夠訪問的方法
  9. ExportedMethod()
  10. // 僅限包內訪問的方法
  11. privateMethod()
  12. }

在代碼中,MyStruct 的 ExportedField 和 MyInterface 的 ExportedMethod() 能夠被包外訪問。

 

Go語言import導入包

要引用其餘包的標識符,可使用 import 關鍵字,導入的包名使用雙引號包圍,包名是從 GOPATH 開始計算的路徑,使用/進行路徑分隔。

默認導入的寫法

導入有兩種基本格式,即單行導入和多行導入,兩種導入方法的導入代碼效果是一致的。

1) 單行導入

單行導入格式以下:

import "包1"
import "包2"

2) 多行導入

當多行導入時,包名在 import 中的順序不影響導入效果,格式以下:

import(
    "包1"
    "包2"
    …
)

參考代碼 8-1 的例子來理解 import 的機制。

 

代碼 8-1 的目錄層次以下:

.
└── src
    └── chapter08
        └── importadd
            ├── main.go
            └── mylib
                └── add.go


代碼8-1 加函數(具體文件:…/chapter08/importadd/mylib/add.go)

  1. package mylib
  2. func Add(a, b int) int {
  3. return a + b
  4. }

第 3 行中的 Add() 函數以大寫 A 開頭,表示將 Add() 函數導出供包外使用。當首字母小寫時,爲包內使用,包外沒法引用到。

add.go 在 mylib 文件夾下,習慣上將文件夾的命名與包名一致,命名爲 mylib 包。

代碼8-2 導入包(具體文件:…/chapter08/importadd/main.go)

  1. package main
  2. import (
  3. "chapter08/importadd/mylib"
  4. "fmt"
  5. )
  6. func main() {
  7. fmt.Println(mylib.Add(1, 2))
  8. }

代碼說明以下:

  • 第 4 行,導入 chapter08/importadd/mylib 包。
  • 第 9 行,使用 mylib 做爲包名,並引用 Add() 函數調用。


在命令行中運行下面代碼:

export GOPATH=/home/davy/golangbook/code
go install chapter08/importadd
$GOPATH/bin/importadd

命令說明以下:

  • 第 1 行,根據你的 GOPATH 不一樣,設置 GOPATH。
  • 第 2 行,使用 go install 指令編譯並安裝 chapter08/code8-1 到 GOPATH 的 bin 目錄下。
  • 第 3 行,執行 GOPATH 的 bin 目錄下的可執行文件 code8-1。


運行代碼,輸出結果以下:
3

導入包後自定義引用的包名

在默認導入包的基礎上,在導入包路徑前添加標識符便可造成自定義引用包,格式以下:

customName "path/to/package"

其中,path/to/package 爲要導入的包路徑,customName 爲自定義的包名。

在 code8-1 的基礎上,在 mylib 導入的包名前添加一個標識符,代碼以下:

  1. package main
  2. import (
  3. renameLib "chapter08/importadd/mylib"
  4. "fmt"
  5. )
  6. func main() {
  7. fmt.Println(renameLib.Add(1, 2))
  8. }

代碼說明以下:

  • 第 4 行,將 chapter08/importadd/mylib 包導入,而且使用 renameLib 進行引用。
  • 第 9 行,使用 renameLib 調用 chapter08/importadd/mylib 包中的 Add() 函數。

匿名導入包——只導入包但不使用包內類型和數值

若是隻但願導入包,而不使用任何包內的結構和類型,也不調用包內的任何函數時,可使用匿名導入包,格式以下:

  1. import (
  2. _ "path/to/package"
  3. )

其中,path/to/package 表示要導入的包名,下畫線_表示匿名導入包。

匿名導入的包與其餘方式導入包同樣會讓導入包編譯到可執行文件中,同時,導入包也會觸發 init() 函數調用。

包在程序啓動前的初始化入口:init

在某些需求的設計上須要在程序啓動時統一調用程序引用到的全部包的初始化函數,若是須要經過開發者手動調用這些初始化函數,那麼這個過程可能會發生錯誤或者遺漏。咱們但願在被引用的包內部,由包的編寫者得到代碼啓動的通知,在程序啓動時作一些本身包內代碼的初始化工做。

例如,爲了提升數學庫計算三角函數的執行效率,能夠在程序啓動時,將三角函數的值提早在內存中建成索引表,外部程序經過查表的方式迅速得到三角函數的值。可是三角函數索引表的初始化函數的調用不但願由每個外部使用三角函數的開發者調用,若是在三角函數的包內有一個機制能夠告訴三角函數包程序什麼時候啓動,那麼就能夠解決初始化的問題。

Go 語言爲以上問題提供了一個很是方便的特性:init() 函數。

init() 函數的特性以下:

  • 每一個源碼可使用 1 個 init() 函數。
  • init() 函數會在程序執行前(main() 函數執行前)被自動調用。
  • 調用順序爲 main() 中引用的包,以深度優先順序初始化。


例如,假設有這樣的包引用關係:main→A→B→C,那麼這些包的 init() 函數調用順序爲:

C.init→B.init→A.init→main

說明:

  • 同一個包中的多個 init() 函數的調用順序不可預期。
  • init() 函數不能被其餘函數調用。

理解包導入後的init()函數初始化順序

Go 語言包會從 main 包開始檢查其引用的全部包,每一個包也可能包含其餘的包。Go 編譯器由此構建出一個樹狀的包引用關係,再根據引用順序決定編譯順序,依次編譯這些包的代碼。

在運行時,被最後導入的包會最早初始化並調用 init() 函數。

經過下面的代碼理解包的初始化順序。

代碼8-3 包導入初始化順序入口(…/chapter08/pkginit/main.go)

  1. package main
  2. import "chapter08/code8-2/pkg1"
  3. func main() {
  4. pkg1.ExecPkg1()
  5. }

代碼說明以下:

  • 第 3 行,導入 pkg1 包。
  • 第 7 行,調用 pkg1 包的 ExecPkg1() 函數。


代碼8-4 包導入初始化順序pkg1(…/chapter08/pkginit/pkg1/pkg1.go)

  1. package pkg1
  2. import (
  3. "chapter08/code8-2/pkg2"
  4. "fmt"
  5. )
  6. func ExecPkg1() {
  7. fmt.Println("ExecPkg1")
  8. pkg2.ExecPkg2()
  9. }
  10. func init() {
  11. fmt.Println("pkg1 init")
  12. }

代碼說明以下:

  • 第 4 行,導入 pkg2 包。
  • 第 8 行,聲明 ExecPkg1() 函數。
  • 第 12 行,調用 pkg2 包的 ExecPkg2() 函數。
  • 第 15 行,在 pkg1 包初始化時,打印 pkg1 init。


代碼8-5 包導入初始化順序pkg2(…/chapter08/pkginit/pkg2/pkg2.go)

  1. package pkg2
  2. import "fmt"
  3. func ExecPkg2() {
  4. fmt.Println("ExecPkg2")
  5. }
  6. func init() {
  7. fmt.Println("pkg2 init")
  8. }

代碼說明以下:

  • 第 5 行,聲明 ExecPkg2() 函數。
  • 第 10 行,在 pkg2 包初始化時,打印 pkg2 init。


執行代碼,輸出以下:
pkg2 init
pkg1 init
ExecPkg1
ExecPkg2

Go語言工廠模式自動註冊

本例利用包的 init 特性,將 cls1 和 cls2 兩個包註冊到工廠,使用字符串建立這兩個註冊好的結構實例。

完整代碼的結構以下:

.
└── src
    └── chapter08
        └── clsfactory
            ├── main.go
            └── base
                └── factory.go
            └── cls1
                └── reg.go
            └── cls2
                └── reg.go

類工廠(具體文件:…/chapter08/clsfactory/base/factory.go)

  1. package base
  2. // 類接口
  3. type Class interface {
  4. Do()
  5. }
  6. var (
  7. // 保存註冊好的工廠信息
  8. factoryByName = make(map[string]func() Class)
  9. )
  10. // 註冊一個類生成工廠
  11. func Register(name string, factory func() Class) {
  12. factoryByName[name] = factory
  13. }
  14. // 根據名稱建立對應的類
  15. func Create(name string) Class {
  16. if f, ok := factoryByName[name]; ok {
  17. return f()
  18. } else {
  19. panic("name not found")
  20. }
  21. }

這個包叫base,負責處理註冊和使用工廠的基礎代碼,該包不會引用任何外部的包。

如下是對代碼的說明:

  • 第 4 行定義了「產品」:類。
  • 第 10 行使用了一個 map 保存註冊的工廠信息。
  • 第 14 行提供給工廠方註冊使用,所謂的「工廠」,就是一個定義爲func() Class的普通函數,調用此函數,建立一個類實例,實現的工廠內部結構體會實現 Class 接口。
  • 第 19 行定義經過名字建立類實例的函數,該函數會在註冊好後調用。
  • 第 20 行在已經註冊的信息中查找名字對應的工廠函數,找到後,在第 21 行調用並返回接口。
  • 第 23 行是若是建立的名字沒有找到時,報錯。


類1及註冊代碼(具體文件:…/chapter08/clsfactory/cls1/reg.go)

  1. package cls1
  2. import (
  3. "chapter08/clsfactory/base"
  4. "fmt"
  5. )
  6. // 定義類1
  7. type Class1 struct {
  8. }
  9. // 實現Class接口
  10. func (c *Class1) Do() {
  11. fmt.Println("Class1")
  12. }
  13. func init() {
  14. // 在啓動時註冊類1工廠
  15. base.Register("Class1", func() base.Class {
  16. return new(Class1)
  17. })
  18. }

上面的代碼展現了Class1的工廠及產品定義過程。

  • 第 9~15 行定義 Class1 結構,該結構實現了 base 中的 Class 接口。
  • 第 20 行,Class1 結構的實例化過程叫 Class1 的工廠,使用 base.Register() 函數在 init() 函數被調用時與一個字符串關聯,這樣,方便之後經過名字從新調用該函數並建立實例。


類2及註冊代碼(具體文件:…/chapter08/clsfactory/cls2/reg.go)

  1. package cls2
  2. import (
  3. "chapter08/clsfactory/base"
  4. "fmt"
  5. )
  6. // 定義類2
  7. type Class2 struct {
  8. }
  9. // 實現Class接口
  10. func (c *Class2) Do() {
  11. fmt.Println("Class2")
  12. }
  13. func init() {
  14. // 在啓動時註冊類2工廠
  15. base.Register("Class2", func() base.Class {
  16. return new(Class2)
  17. })
  18. }

Class2 的註冊與 Class1 的定義和註冊過程相似。

類工程主流程(具體文件:…/chapter08/clsfactory/main.go)

  1. package main
  2. import (
  3. "chapter08/clsfactory/base"
  4. _ "chapter08/clsfactory/cls1" // 匿名引用cls1包, 自動註冊
  5. _ "chapter08/clsfactory/cls2" // 匿名引用cls2包, 自動註冊
  6. )
  7. func main() {
  8. // 根據字符串動態建立一個Class1實例
  9. c1 := base.Create("Class1")
  10. c1.Do()
  11. // 根據字符串動態建立一個Class2實例
  12. c2 := base.Create("Class2")
  13. c2.Do()
  14. }

下面是對代碼的說明:

  • 第 5 和第 6 行使用匿名引用方法導入了 cls1 和 cls2 兩個包。在 main() 函數調用前,這兩個包的 init() 函數會被自動調用,從而自動註冊 Class1 和 Class2。
  • 第 12 和第 16 行,經過 base.Create() 方法查找字符串對應的類註冊信息,調用工廠方法進行實例建立。
  • 第 13 和第 17 行,調用類的方法。


執行下面的指令進行編譯:

export GOPATH=/home/davy/golangbook/code
go install chapter08/clsfactory
$GOPATH/bin/clsfactory

代碼輸出以下: Class1 Class2

相關文章
相關標籤/搜索