GO學習筆記——包和封裝(17)
既然GO支援封裝,那麼相比於C++的三大訪問屬性:private,protected,public,GO只有private和public,因為protected是因繼承而產生的。而C++的訪問屬性是根據類來說的,在一個類中相當於外界的屬性;但是GO中沒有類,GO中的訪問屬性是相對於包來說的,這一章就來講一講GO中的包,以及包是怎麼做到封裝的。
包其實相當於一個目錄,如果我們把所有的原始檔寫在一個檔案中,那麼會不方便我們管理。這其實也是,在C++中我們也分標頭檔案和原始檔,而且會有多個原始檔。
包用於組織GO原始碼,提供了對程式碼更好的管理與重用性。
總之,這麼理解,把一些功能相近的原始檔封裝到一個包中,那麼這個包其實就是這些檔案的一個目錄
一個目錄只能有一個包
因為上面的理解,包其實就是一個目錄,那麼GO就規定了一個目錄就只能有一個包,如果有兩個包就會報錯,下面來看一下。
我新建了一個目錄叫newpackage,在這個目錄下建立了一個test1檔案,那麼我的GoLand會自動幫我給我的這個檔案新增這麼一句話
package newpackage
意思是,我這個test1檔案是封裝在newpackage這個包中的。這邊包名預設就是這個目錄名。但其實包名不一定要和目錄名一樣,可以改,只要在同一個目錄下只有一個包名就可以了(但是預設約定包名和目錄名相同)。
我再建立了一個test2檔案,也自動添加了上面那句話。但是我把包名改成了newpackage1,這個是編譯器就報錯了。
表示在同一個目錄中有多個包。
所以包就是一個目錄,一個目錄就是一個包,包名可以不和目錄名相同,但是一個目錄下只能有一個包。
main包包含可執行程式入口
要生成Go語言可執行程式,必須要有main的package包,且必須在該包下有main函式
就像C++要有main函式一樣,GO也要有main函式,像之前的文章的測試程式碼都有一個main函式,且都沒有建立別的包,都是在main包中測試的。
所以一個GO程式如果需要執行:
- 必須要有main包(包名必須是main,不可以是別的名)
- main包內必須要有main入口函式
- 如果main包中沒有入口函式不能執行,如果只是別的名的包中有入口函式也不能執行
為結構體定義的方法必須在一個包內
GO的面向物件使用結構體來做的,對一個結構體可以定義很多方法。
因為需要封裝的原因,所以一個結構體中的方法必須在一個包內定義。也就是說,如果結構體定義在了包A中,那麼它對應的方法也必須都定義在包A中,不可以在別的包中定義,這樣就破壞了封裝的概念。但是可以是不同的檔案,即可以在同一個包中的不同原始檔中定義結構體的方法。
所以,為了保證封裝,為結構體定義的方法必須在一個包內。
建立自定義的包
下面我們把之前一章的測試用例,封裝成一個List包,來建立一個我們自定義的包。
先來看一下目錄結構
這邊將定義的List放到了一個List包的一個原始檔list.go中
package List
import "fmt"
//定義一個單鏈表的節點
type Node struct {
Next *Node
Val int
}
//定義一個方法來遍歷這個單鏈表
func (node *Node) PrintList(){
for node != nil{
fmt.Print(node.Val)
if node.Next != nil {
fmt.Print("->")
}
node = node.Next
}
}
為了對別的包可見,因此我們將變數名改為了首字母大寫,GO語言變數名首字母大寫表示對別的包可見(public),首字母小寫表示對別的包不可見(private)
剩下的main.go中,import了我們自定義的這個包,且它裡面只有一個main函式
package main
import "struct/List"
func main() {
root := List.Node{Val:1}
root.Next = &List.Node{Val:2}
root.Next.Next = &List.Node{Val:3}
root.Next.Next.Next = &List.Node{Val:4}
root.PrintList() //使用該方法
}
執行程式
1->2->3->4
Process finished with exit code 0
這樣,我們就把定義的List放到了一個包中,更方便我們管理我們的原始檔。
我們要匯入一個別的包都要用到import關鍵字,下面來說說匯入包時的技巧以及import關鍵字的使用。
init函式
說import之前先說一下init函式。每個包都可以包含一個init函式,這其實就是一個初始化函式,在呼叫這個包之前,先做一些對其的初始化操作,這樣在我們使用這個包之前,一些必要的init操作就已經被做完了,不用我們再手動去執行一遍。
init 函式不應該有任何返回值型別和引數,在我們的程式碼中也不能顯式地呼叫它。
我們為上面的List包建一個init函式。
package List
import "fmt"
//加入的init函式
func init(){
fmt.Println("before use package List")
}
//定義一個單鏈表的節點
type Node struct {
Next *Node
Val int
}
//定義一個方法來遍歷這個單鏈表
func (node *Node) PrintList(){
for node != nil{
fmt.Print(node.Val)
if node.Next != nil {
fmt.Print("->")
}
node = node.Next
}
}
執行結果
before use package List
1->2->3->4
可以看到這裡也執行了init函式,並且在程式main函式執行之前執行了,其實它就是在匯入這個包時就已經自動執行了。
import匯入包初始化順序
這邊再說一下import匯入其他包時的一些初始化順序:
- 如果一個main包裡面匯入其他包,其他的包將被順序匯入(按宣告的順序匯入)
- 如果匯入的包中有依賴包(匯入包A依賴包B),會首先匯入B包,然後初始化B包中的常量和變數,最後如果B包中有init,會自動執行init()(也就是說,會先初始化B包中的一些函式體外的常量和變數,再執行init函式)
- 所有包匯入完成後才會對main中常量和變數進行初始化,然後執行main中的init函式(如果有的話),最後執行main函式(所有包都是先執行init函式)
- 如果一個包被匯入多次,則該包只會被匯入一次(就像C++裡的pragma once),不會被重複匯入
import特殊用法
1.別名,將匯入的包名命名為另一個容易記的別名
有時候當包名很長時我們可以對該包起一個簡單容易記的別名,這裡我們對上述main包中匯入的List包進行一個重新命名做測試
package main
import A "struct/List" //起別名A
func main() {
root := List.Node{Val:1}
root.Next = &List.Node{Val:2}
root.Next.Next = &List.Node{Val:3}
root.Next.Next.Next = &List.Node{Val:4}
root.PrintList() //使用該方法
}
這裡我們起了別名A,但是上述程式碼程式會出錯
.\main.go:4:10: undefined: List
所以一旦起了別名,原來的名字就不可以用了,必須要全部變為別名。
package main
import A "struct/List"
func main() {
root := A.Node{Val:1}
root.Next = &A.Node{Val:2}
root.Next.Next = &A.Node{Val:3}
root.Next.Next.Next = &A.Node{Val:4}
root.PrintList() //使用該方法
}
上面這樣就可以了。
2.點(.)操作
點(.)標識的包匯入後,呼叫該包中的函式時可以省略字首包名(不建議,容易引起衝突,其實就是相當於C++中的 using namespace,使用某個名稱空間),還是拿之前的程式做測試。
package main
import . "struct/List" //在呼叫該包時省略包名
func main() {
root := List.Node{Val:1}
root.Next = &List.Node{Val:2}
root.Next.Next = &List.Node{Val:3}
root.Next.Next.Next = &List.Node{Val:4}
root.PrintList() //使用該方法
}
但是上述程式碼還是會出錯
.\main.go:3:8: imported and not used: "struct/List"
.\main.go:6:10: undefined: List
所以和之前一樣,一旦一個包名被點(.)操作了,那麼原來的包名也不可以用了,用到的地方必須全部省略包名
package main
import . "struct/List" //在呼叫該包時省略包名
func main() {
root := Node{Val:1} //在呼叫時,省略包名
root.Next = &Node{Val:2}
root.Next.Next = &Node{Val:3}
root.Next.Next.Next = &Node{Val:4}
root.PrintList() //使用該方法
}
上面這樣就可以了。
3.下劃線(_)操作
匯入了包,卻不在程式碼中使用它,這在 Go 中是非法的。當這麼做時,編譯器是會報錯的。其原因是為了避免匯入過多未使用的包,從而導致編譯時間顯著增加。
然而,在程式開發的活躍階段,又常常會先匯入包,而暫不使用它,所以這裡就可以使用下劃線(_)操作:
匯入該包,但不匯入整個包,而是執行該包中的init函式,因此無法通過包名來呼叫包中的其他函式。
package main
import _ "struct/List"
func main() {
root := List.Node{Val:1}
root.Next = &List.Node{Val:2}
root.Next.Next = &List.Node{Val:3}
root.Next.Next.Next = &List.Node{Val:4}
root.PrintList() //使用該方法
}
上述程式碼會出現,因為我們呼叫了該包中的內容。
.\main.go:10:10: undefined: List
package main
import _ "struct/List"
func main() {
}
這邊我們並沒有使用該包中的任何內容,程式不會編譯出錯,而會去執行該包中的init函式。
執行結果
before use package List