既然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