1. 程式人生 > >GO學習筆記——包和封裝(17)

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匯入其他包時的一些初始化順序:

  1. 如果一個main包裡面匯入其他包,其他的包將被順序匯入(按宣告的順序匯入)
  2. 如果匯入的包中有依賴包(匯入包A依賴包B),會首先匯入B包,然後初始化B包中的常量和變數,最後如果B包中有init,會自動執行init()(也就是說,會先初始化B包中的一些函式體外的常量和變數,再執行init函式
  3. 所有包匯入完成後才會對main中常量和變數進行初始化,然後執行main中的init函式(如果有的話),最後執行main函式(所有包都是先執行init函式)
  4. 如果一個包被匯入多次,則該包只會被匯入一次(就像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