當前位置:
首頁 > 科技 > 深入淺出Go語言的庫源碼文件

深入淺出Go語言的庫源碼文件

作者 | 郝林

出處 | 極客時間

你用過 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!
", 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!
", 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 語言基本編碼規則,即:代碼包聲明規則、代碼包導入規則以及程序實體的訪問許可權規則。在進行模塊化編程時,你必須記住這些規則,否則你的代碼很可能無法通過編譯。

思考題

這次的思考題都是關於代碼包導入的,如下。



  1. 如果你需要導入兩個代碼包,而這兩個代碼包的導入路徑的最後一級是相同的,比如:

    dep/lib/flag

    flag

    ,那麼會產生衝突嗎?


  2. 如果會產生衝突,那麼怎樣解決這種衝突,有幾種方式?

第一個問題比較簡單,你一試便知。強烈建議你編寫個例子,然後運行go命令構建它,並看看會有什麼樣的提示。而第二個問題涉及了代碼包導入語句的高級寫法,你可能需要去查閱一下 Go 語言規範。不過也不難。你最多能想出幾種解決辦法呢?你可以在專欄給我留言,我們一起討論。

如果你對 Go 語言的學習感興趣,歡迎訂閱我的專欄

《Go 語言核心 36 講》

,在這個專欄里,我會將我十多年的實戰經驗和心得感想與你分享

課程原價 68 元,優惠期間 45 元,掃描下方二維碼購買還能返現金 6 元,相當於 39 元購買

優惠倒計時 3 天!


今日薦文

點擊下方圖片即可閱讀


架構師不寫代碼,能行嗎?




喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 InfoQ 的精彩文章:

外國月亮也不圓?在矽谷,只有失敗者才朝九晚五
在頂尖架構師眼裡,你遇到的坑都是小問題

TAG:InfoQ |