你已經使用過 Go 語言撰寫了小命令(或者說微型程式)嗎?
當你在撰寫“Hello, world”的時候,一個原始碼檔案就足夠了,雖然這種小玩意兒沒什么用,最多能給你一點點莫名的成就感,如果你對這一點點并不滿足,別著急,跟著學,我肯定你也可以寫出很厲害的程式,
我們在上一篇的文章中學到了命令原始碼檔案的相關知識,那么除了命令原始碼檔案,你還能用 Go 語言撰寫庫原始碼檔案,那么什么是庫原始碼檔案呢?
在我的定義中,庫原始碼檔案是不能被直接運行的原始碼檔案,它僅用于存放程式物體,這些程式物體可以被其他代碼使用(只要遵從 Go 語言規范的話),
這里的“其他代碼”可以與被使用的程式物體在同一個原始碼檔案內,也可以在其他原始碼檔案,甚至其他代碼包中,
那么程式物體是什么呢?在 Go 語言中,程式物體是變數、常量、函式、結構體和介面的統稱,我們總是會先宣告(或者說定義)程式物體,然后再去使用,比如在上一篇的例子中,我們先定義了變數name,然后在main函式中呼叫fmt.Printf函式的時候用到了它,再多說一點,程式物體的名字被統稱為識別符號,識別符號可以是任何 Unicode 編碼可以表示的字母字符、數字以及下劃線“_”,但是其首字母不能是數字,從規則上說,我們可以用中文作為變數的名字,但是,我覺得這種命名方式非常不好,自己也會在開發團隊中明令禁止這種做法,作為一名合格的程式員,我們應該向著撰寫國際水準的程式無限逼近,
回到正題,
我們今天的問題是:怎樣把命令原始碼檔案中的代碼拆分到其他庫原始碼檔案?
我們用代碼演示,把這個問題說得更具體一些,
如果在某個目錄下有一個命令原始碼檔案 demo4.go,如下:
package main
import (
"flag"
)
var name string
func init() {
flag.StringVar(&name, "name", "everyone", "The greeting object.")
}
func main() {
flag.Parse()
hello(name)
}
其中的代碼你應該比較眼熟了,我在講命令原始碼檔案的時候貼過很相似的代碼,那個原始碼檔案名為 demo2.go,
這兩個檔案的不同之處在于,demo2.go 直接通過呼叫fmt.Printf函式列印問候語,而當前的 demo4.go 在同樣位置呼叫了一個叫作hello的函式,
函式hello被宣告在了另外一個原始碼檔案中,我把它命名為 demo4_lib.go,并且放在與 demo4.go 相同的目錄下,如下:
// 需在此處添加代碼,[1]
import "fmt"
func hello(name string) {
fmt.Printf("Hello, %s!\n", name)
}
那么問題來了:注釋 1 處應該填入什么代碼?
典型回答
答案很簡單,填入代碼包宣告陳述句package main,為什么?我之前說過,在同一個目錄下的原始碼檔案都需要被宣告為屬于同一個代碼包,
如果該目錄下有一個命令原始碼檔案,那么為了讓同在一個目錄下的檔案都通過編譯,其他原始碼檔案應該也宣告屬于main包,
如此一來,我們就可以運行它們了,比如,我們可以在這些檔案所在的目錄下運行如下命令并得到相應的結果,
$ go run demo4.go demo4_lib.go Hello, everyone!
或者,像下面這樣先構建當前的代碼包再運行,
$ go build puzzlers/article3/q1 $ ./q1 Hello, everyone!
在這里,我把 demo4.go 和 demo4_lib.go 都放在了一個相對路徑為puzzlers/article3/q1的目錄中,
在默認情況下,相應的代碼包的匯入路徑會與此一致,我們可以通過代碼包的匯入路徑參考其中宣告的程式物體,但是,這里的情況是不同的,
注意,demo4.go 和 demo4_lib.go 都宣告自己屬于main包,我在前面講 Go 語言原始碼的組織方式的時候提到過這種用法,即:原始碼檔案宣告的包名可以與其所在目錄的名稱不同,只要這些檔案宣告的包名一致就可以,
順便說一下,我為本專欄創建了一個名為“Golang_Puzzlers”的專案,該專案的 src 子目錄下會存有我們涉及的所有代碼和相關檔案,
也就是說,正確的用法是,你需要把該專案的打包檔案下載到本地的任意目錄下,然后經解壓縮后把“Golang_Puzzlers”目錄加入到環境變數GOPATH中,還記得嗎?這會使“Golang_Puzzlers”目錄成為作業區之一,
問題決議
這個問題考察的是代碼包宣告的基本規則,這里再總結一下,
第一條規則,同目錄下的原始碼檔案的代碼包宣告陳述句要一致,也就是說,它們要同屬于一個代碼包,這對于所有原始碼檔案都是適用的,
如果目錄中有命令原始碼檔案,那么其他種類的原始碼檔案也應該宣告屬于main包,這也是我們能夠成功構建和運行它們的前提,
第二條規則,原始碼檔案宣告的代碼包的名稱可以與其所在的目錄的名稱不同,在針對代碼包進行構建時,生成的結果檔案的主名稱與其父目錄的名稱一致,
對于命令原始碼檔案而言,構建生成的可執行檔案的主名稱會與其父目錄的名稱相同,這在我前面的回答中也驗證過了,
好了,經過我的反復強調,相信你已經記住這些規則了,下面的內容也將會與它們相關,
在撰寫真正的程式時,我們僅僅把代碼拆分到幾個原始碼檔案中是不夠的,我們往往會用模塊化編程的方式,根據代碼的功能和用途把它們放置到不同的代碼包中,不過,這又會牽扯進一些 Go 語言的代碼組織規則,我們一起來往下看,
知識精講
1. 怎樣把命令原始碼檔案中的代碼拆分到其他代碼包?
我們先不用關注拆分代碼的技巧,我在這里仍然依從前面的拆分方法,我把 demo4.go 另存為 demo5.go,并放到一個相對路徑為puzzlers/article3/q2的目錄中,
然后我再創建一個相對路徑為puzzlers/article3/q2/lib的目錄,再把 demo4_lib.go 復制一份并改名為 demo5_lib.go 放到該目錄中,
現在,為了讓它們通過編譯,我們應該怎樣修改代碼?你可以先思考一下,我在這里給出一部分答案,我們一起來看看已經過修改的 demo5_lib.go 檔案,
package lib5
import "fmt"
func Hello(name string) {
fmt.Printf("Hello, %s!\n", name)
}
可以看到,我在這里修改了兩個地方,第一個改動是,我把代碼包宣告陳述句由package main改為了package lib5,注意,我故意讓宣告的包名與其所在的目錄的名稱不同,第二個改動是,我把全小寫的函式名hello改為首字母大寫的Hello,
基于以上改動,我們再來看下面的幾個問題,
2. 代碼包的匯入路徑總會與其所在目錄的相對路徑一致嗎?
庫原始碼檔案 demo5_lib.go 所在目錄的相對路徑是puzzlers/article3/q2/lib,而它卻宣告自己屬于lib5包,在這種情況下,該包的匯入路徑是puzzlers/article3/q2/lib,還是puzzlers/article3/q2/lib5?
這個問題往往會讓 Go 語言的初學者們困惑,就算是用 Go 開發程序式的人也不一定清楚,我們一起來看看,
首先,我們在構建或者安裝這個代碼包的時候,提供給go命令的路徑應該是目錄的相對路徑,就像這樣:
go install puzzlers/article3/q2/lib
該命令會成功完成,之后,當前作業區的 pkg 子目錄下會產生相應的歸檔檔案,具體的相對路徑是:
pkg/darwin_amd64/puzzlers/article3/q2/lib.a
其中的darwin_amd64就是我在講作業區時提到的平臺相關目錄,可以看到,這里與原始碼檔案所在目錄的相對路徑是對應的,
為了進一步說明問題,我需要先對 demo5.go 做兩個改動,第一個改動是,在以import為前導的代碼包匯入陳述句中加入puzzlers/article3/q2/lib,也就是試圖匯入這個代碼包,
第二個改動是,把對hello函式的呼叫改為對lib.Hello函式的呼叫,其中的lib.叫做限定符,旨在指明右邊的程式物體所在的代碼包,不過這里與代碼包匯入路徑的完整寫法不同,只包含了路徑中的最后一級lib,這與代碼包宣告陳述句中的規則一致,
現在,我們可以通過運行go run demo5.go命令試一試,錯誤提示會類似于下面這種,
./demo5.go:5:2: imported and not used: "puzzlers/article3/q2/lib" as lib5 ./demo5.go:16:2: undefined: lib
第一個錯誤提示的意思是,我們匯入了puzzlers/article3/q2/lib包,但沒有實際使用其中的任何程式物體,這在 Go 語言中是不被允許的,在編譯時就會導致失敗,
注意,這里還有另外一個線索,那就是“as lib5”,這說明雖然匯入了代碼包puzzlers/article3/q2/lib,但是使用其中的程式物體的時候應該以lib5.為限定符,這也就是第二個錯誤提示的原因了,Go 命令找不到lib.這個限定符對應的代碼包,
為什么會是這樣?根本原因就是,我們在原始碼檔案中宣告所屬的代碼包與其所在目錄的名稱不同,請記住,原始碼檔案所在的目錄相對于 src 目錄的相對路徑就是它的代碼包匯入路徑,而實際使用其程式物體時給定的限定符要與它宣告所屬的代碼包名稱對應,
有兩個方式可以使上述構建成功完成,我在這里選擇把 demo5_lib.go 檔案中的代碼包宣告陳述句改為package lib,理由是,為了不讓該代碼包的使用者產生困惑,我們總是應該讓宣告的包名與其父目錄的名稱一致,
3. 什么樣的程式物體才可以被當前包外的代碼參考?
你可能會有疑問,我為什么要把 demo5_lib.go 檔案中的那個函式名稱hello的首字母大寫?實際上這涉及了 Go 語言中對于程式物體訪問權限的規則,
超級簡單,名稱的首字母為大寫的程式物體才可以被當前包外的代碼參考,否則它就只能被當前包內的其他代碼參考,
通過名稱,Go 語言自然地把程式物體的訪問權限劃分為了包級私有的和公開的,對于包級私有的程式物體,即使你匯入了它所在的代碼包也無法參考到它,
4. 對于程式物體,還有其他的訪問權限規則嗎?
答案是肯定的,在 Go 1.5 及后續版本中,我們可以通過創建internal代碼包讓一些程式物體僅僅能被當前模塊中的其他代碼參考,這被稱為 Go 程式物體的第三種訪問權限:模塊級私有,
具體規則是,internal代碼包中宣告的公開程式物體僅能被該代碼包的直接父包及其子包中的代碼參考,當然,參考前需要先匯入這個internal包,對于其他代碼包,匯入該internal包都是非法的,無法通過編譯,
“Golang_Puzzlers”專案的puzzlers/article3/q4包中有一個簡單的示例,可供你查看,你可以改動其中的代碼并體會internal包的作用,
總結
我們在本篇文章中詳細討論了把代碼從命令原始碼檔案中拆分出來的方法,這包括拆分到其他庫原始碼檔案,以及拆分到其他代碼包,
這里涉及了幾條重要的 Go 語言基本編碼規則,即:代碼包宣告規則、代碼包匯入規則以及程式物體的訪問權限規則,在進行模塊化編程時,你必須記住這些規則,否則你的代碼很可能無法通過編譯,
思考題
- 如果你需要匯入兩個代碼包,而這兩個代碼包的匯入路徑的最后一級是相同的,比如:dep/lib/flag和flag,那么會產生沖突嗎?
- 如果會產生沖突,那么怎樣解決這種沖突,有幾種方式?
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/252432.html
標籤:Go
