golang中的面向"對象"

寫在前面

Go語言中的面向"對象"和其他語言非常不同,僅僅支持封裝,不支持繼承和多態。那麼你可能要問了,僅僅依靠封裝能實現一些較為複雜的事情麼?Go語言通過接口和封裝來實現較為複雜的事,所以更多的是成為接口編程。

既然只有封裝,就沒有class(類),只有struct(結構體)。

結構體

結構體是用戶定義的類型,表示若干個字段的集合。當需要將多個數據分組到一個整體,而不是將每個數據作為單獨的類型進行維護時,可以使用結構體。是不是有點類的概念?

二分搜索樹例子理解結構體知識

下面嘗試通過一個二分搜索樹的例子來介紹關於結構體的知識。二分搜索樹分為3部分,某個節點的值,節點的左子樹,節點的右子樹。


golang中的面向


其實結構體的聲明和麵向對象中類的聲明非常類似:

//定義一個二分搜索樹
type treeNode struct{
value int //節點值為int類型
left, right *treeNode //左右子樹為指針類型
}

在聲明好結構體後,接下來就是定義它了:

func main() {
var root treeNode //定義一個二分搜索樹對象
root = treeNode{value: 2} //二分搜索樹root節點初始化
root.left = &treeNode{} //二分搜索樹root節點左子樹初始化
root.right = &treeNode{value: 6, left: nil, right: nil} //二分搜索樹root節點右子樹初始化
// root.right = &treeNode{6,nil, nil}
root.left.left =new(treeNode) //給二分搜索樹root節點的左子樹的左側創建一個節點
nodes :=[]treeNode{
{value:3},
{},
{5,nil,nil},
{8,nil,&root},
}
fmt.Println(nodes)
}
//運行結果:
[{3 } {0 } {5
} {8 0xc000048420}]

你發現了麼,聲明結構體就相當於Java中的創建一個類,然後實例化這個結構體就是Java中類的實例化過程。在Go語言中,不論是地址還是結構體本身,一律使用.來訪問成員。

var root treeNode //定義一個二分搜索樹對象
root = treeNode{value: 2} //二分搜索樹root節點初始化
root.left = &treeNode{} //二分搜索樹root節點左子樹初始化
root.right = &treeNode{value: 6, left: nil, right: nil} //二分搜索樹root節點右子樹初始化
// root.right = &treeNode{6,nil, nil}
root.left.left =new(treeNode) //給二分搜索樹root節點的左子樹的左側創建一個節點

Go語言提供了很多實例化結構體的方法,因此結構體是沒有構造方法的。當然如果你可以創建一個工廠方法用於實例化構造體:

//用於創建一個結構體對象
func createTreeNode(value int) *treeNode{

return &treeNode{value:value} //這是一個局部變量的地址,但是Go語言允許返回局部變量
}

相信聰明的你發現這個createTreeNode函數返回了一個局部對象的地址,這在C++中是不允許的,但是Go語言支持允許返回局部變量地址。然後使用該方法創建一個結構體對象:

 root.left.right = createTreeNode(9)
//運行結果:
&{9 }

看到這裡你可能會問,返回的局部對象是存在於堆上還是棧上呢?像C++,它的局部變量是分配在棧中,函數一旦退出,則局部變量會被銷燬,只有定義在堆上的變量才能傳遞出去,不過這樣就有一個麻煩,這個變量就需要你手動釋放。而在Java中,通過New關鍵詞生成的對象一般都在堆上,然後等到不使用的時候由垃圾回收機制回收。在Go語言中,你不需要知道它具體分配在何處,因為它是由Go語言編譯器和運行環境決定的。

例如下面的treeNode沒有取地址且不用返回出去,則這個treeNode可以在棧上分配它;當這個treeNode取了地址且返回出去給其他使用時,這個treeNode就可以在堆上分配,然後這個treeNode就會參與垃圾回收,當這個treeNode的指針不再使用的時候就會被回收。因此不能說函數退出這個局部變量就銷燬了,這個在Go語言中是不一定的。既然能返回局部變量,那就不用考慮對象到底在哪裡分配了,程序相對來說就好寫一些:

func createTreeNode(value int) {
return treeNode{value:value} //這是一個局部變量的地址,但是Go語言允許返回局部變量
}

接下來猜猜這段代碼,創建了一個怎樣的二分搜索樹:

var root treeNode 
root = treeNode{value: 2}
root.left = &treeNode{}
root.right = &treeNode{6, nil, nil}
root.right.left =new(treeNode)
root.left.right = createTreeNode(9)


golang中的面向


接下來介紹如何遍歷這個二分搜索樹,但在此之前先介紹如何為結構體定義方法。注意結構體方法並不是寫在結構體中的,而是寫在結構體外面的,它有一個接收者,其他和普通函數差別不大:

//定義結構體方法,用於輸出二分搜索樹的信息
func (tnode treeNode)print(){
fmt.Println(tnode.value)
}

注意到這個func (tnode treeNode)print(){}沒有?普通的方法都是func print(){},這裡多了由小括號包含的(tnode treeNode),我們稱之為接收者。其實也就是告訴我們這個函數就是treeNode對象使用的:

root.print()

當然如果你理解不了這個意思,可以使用普通函數的寫法:

func uprint(tnode treeNode){
fmt.Println(tnode.value)
}
uprint(root)

看到沒有,這個就是區別,使用前者指定了接收者,故無需再次輸入參數,使用後者則需傳入指定參數。

Go語言中只有值傳遞。我們嘗試修改一下之前創建的那個空子樹:

root.right.left =new(treeNode) //給二分搜索樹root節點的左子樹的左側創建一個節點

就是上面那個,我們定義一個方法,看看能不能將其結點的值修改為8:

func (tnode treeNode)setValue(value int){
tnode.value=value
}
root.right.left.setValue(8)
root.right.left.print()
//運行結果:
0

再次強調一點Go語言中只有值傳遞。因此這樣做是無法修改root.right.left節點的值的,此時可以藉助於指針來完成:

func (tnode *treeNode)setValueByPointer(value int){
tnode.value=value
}
root.right.left.setValue(8)
root.right.left.print()
//運行結果:
8

通過指針傳入對象(其實就是原來對象的地址,最後結果反映到原來對象上)就能修改其值。

總結一下為結構體定義方法,如下所示,注意就是將普通的函數返回到方法名稱之前罷了,其實是普通方法沒有什麼區別?不過這樣寫能讓大家一眼就能找到哪些是結構體方法,增強了辨識度:

func (tnode treeNode)print(){
fmt.Println(tnode.value)
}

結構體定義方法顯示定義和命名方法接收者,只有使用指針作為方法接收者時才能修改結構的內容。nil指針其實也是可以調用方法的

怎麼理解nil指針也可以調用方法呢?我們嘗試進行一個判斷,並輸出後測試一下:

func (tnode *treeNode)setValueByPointer(value int){
if tnode == nil{
fmt.Println("你傳入的是空指針")
}
tnode.value=value
}
var testnil *treeNode
testnil.setValueByPointer(99999)
testnil = &root
testnil.setValueByPointer(2345)
testnil.print()
//運行結果:
你傳入的是空指針
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x1 addr=0x0 pc=0x49122d]

出錯是意料之中的事,因為第一次傳進去的testnil是一個空指針nil,而setValueByPointer函數是需要有返回值的,而空指針nil是沒有值的,因此會報錯,其實你只需要在裡面添加一個return就可以的解決這個問題:

func (tnode *treeNode)setValueByPointer(value int){
if tnode == nil{
fmt.Println("你傳入的是空指針")
return
}
tnode.value=value
}
//運行結果:
你傳入的是空指針
2345

不過需要說明的是並不是每次都需要判斷傳入的對象是不是nil,然後才進行後續操作,這個需要結合具體場景來的。

接下來介紹如何遍歷這個二分搜索樹:(學過二分搜索樹的人肯定知道中序遍歷結果是0 9 2 0 6):


golang中的面向


採用中序遍歷的方式(遍歷方式的名稱是由該節點的遍歷順序來決定的,節點在最前面是前序,中間是中序,最後是後序)因此這裡的中序就是先遍歷左子樹,再遍歷節點,最後遍歷右子樹:

//二分搜索樹的中序遍歷,其實採用了遞歸的思想
func (tnode *treeNode)reverse(){
if tnode ==nil{
return
}
tnode.left.reverse()
tnode.print()
tnode.right.reverse()
}
//運行結果:
0
9
2
0
6

結果和我們的預期完全吻合,但是你有沒有我們只是判斷了tnode節點是否是nil,但是對於其左右子樹沒有判斷,事實上在JavaScript和Java中這個是不用判斷的,但是C++中可能需要判斷。

接下來談談值接收者和指針接收者的區別:

1、需要修改結構體內容的必須使用指針接收者;

2、當結果過大時,也必須使用指針接收者;

3、在具有指針接收者的情況下,建議都採用指針接收者;

4、值接收者是Go語言獨有的;很多語言都有指針接收者如Python中的self,Java中的引用等;

封裝

接下來介紹封裝,在Java中就是使用一些關鍵詞如private、default、protected、public,按照前面的順序,自上而下,訪問範圍越來越大;自下而上,限制能力越來越強,它們所控制的範圍如下所示:


golang中的面向


但是在Go語言中就不一樣了,Go語言通過函數的名字來進行範圍控制的,名字一般使用CamelCase。首字母大寫表示public,首字母小寫表示private,這兩個都是針對包而言的

包package main這個就是一個main包,默認使用的就是這個main包,main包包含了可執行入口。在Go語言中,每個目錄都只能有一個包,包名不一定要和目錄名一致。為結構體定義的方法必須放在同一個包內。

嘗試將之前的關於二分搜索樹的代碼拆分成不同的文件,然後進行導包操作:


golang中的面向


tree包裡面包含一個包entry和文件node.go,而entry包中又包含entry.go文件。其中entry.go中只含有main方法,定義前面package為main包,當然也可以定義為entry包(每個目錄都只能有一個包,包名不一定要和目錄名一致。),但是我們只是讓他運行main方法,因此定義package為main包。既然這樣設置,那麼以後包entry所有的go文件的package都必須是mian,否則會出錯。同樣外面的node.go文件中的package定義為tree包,因此以後tree文件裡面所有go文件的package都必須定義為tree!

擴展已有類型

現在有一個問題,就是你在開發過程中需要使用別人的包,那應該怎樣使用呢?也就是如何擴展系統類型或別人的類型呢?你可以使用別名或者組合來解決這個問題。

這個需要配置GoPATH環境變量的,默認情況unix和linux是在,~/go下,Windows是在%USERPROFILE%\\go。官方建議所有項目和第三方庫都放在同一個GoPATH下面,但也可以將每個項目放在不同的GoPATH下面。Go語言會在編譯時去各個GoPATH中找到不同的包。

Go語言導包正確操作

那麼如何保證自己的go語言程序能正常運行呢?下面教大家如何設置(假設我準備所有go項目存放在I:\\Go\\GoTest文件夾下面,而我的Go語言安裝在G:\\Applications\\Go文件夾下面):

第一步:在I:\\Go\\GoTest文件夾下面新建src文件夾,注意必須是這個名字,不能隨意修改;

第二步:設置環境變量GOROOT=G:\\Applications\\Go(其實就是Go語言安裝路徑)和Path=G:\\Applications\\Go\\bin;及GOPATH=I:\\Go\\GoTest(項目存放的地址,注意不能寫成I:\\Go\\GoTest\\src,僅僅寫到GoTest文件夾為止)。

第三步:配置GoLand參數,File-->Settings-->Go,如下圖所示:


golang中的面向


golang中的面向


之後點擊確認,可能需要重啟GoLand,然後使用Alt+Enter鍵就能實現自動導包了!

獲取第三方庫

接下來介紹如何獲取Go語言的第三方庫。在Python中你可以使用pip install +庫名的方式,而在Go語言中可以使用go get +庫名的辦法。但是直接從谷歌服務器上下載庫在國內似乎不行,這時推薦使用gopm +庫名的方式:


golang中的面向


需要說明的是go get是內置的命令,而gopm是第三方工具,因此在使用前需要使用go get來安裝gopm:

go get github.com/gpmgo/gopm

之後會在你的src文件夾裡面多了兩個新的文件夾bin和github.com:


golang中的面向


還記得前面設置的Path=G:\\Applications\\Go\\bin;這個環境變量麼,打開該文件夾發現裡面都是可執行的exe文件:


golang中的面向


而我們剛才生成的bin目錄下有一個gopm.exe,因此需要將這個gopm.exe複製到

G:\\Applications\\Go\\bin文件夾下面才能保證其正常運行。如果你覺得這種操作很麻煩可以直接修改path參數為Path=%GOPATH%\\bin;這樣就不需要導入了,那這樣我們GOROOT下面的bin目錄中的go、godoc、gofmt就無法正常運行了,那沒事因為我們用到它的時候不多,手動使用他們也是可以接受的。


golang中的面向


這個src文件夾裡面會存有你的項目和你下載的第三方庫。關於Go內置的一些其他命令可以查看這裡GO 命令教程。

下面介紹gopm的使用。其實也是使用gopm get+庫名的方式,當然還可以使用gopm help查看各種參數實現自定義下載位置配置:

NAME:
Gopm - Go Package Manager
USAGE:
Gopm [global options] command [command options] [arguments...]
VERSION:
0.8.8.0307 Beta
COMMANDS:
list list all dependencies of current project
gen generate a gopmfile for current Go project
get fetch remote package(s) and dependencies
bin download and link dependencies and build binary
config configure gopm settings
run link dependencies and go run
test link dependencies and go test
build link dependencies and go build
install link dependencies and go install
clean clean all temporary files
update check and update gopm resources including itself
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--noterm, -n disable color output
--strict, -s strict mode
--debug, -d debug mode
--help, -h show help
--version, -v print the version

還可以使用go build來編譯,使用go install會產生pkg文件和可執行文件;使用go run會直接編譯且運行。

其實看到這裡有一個非常大的問題,就是有些文件夾裡面有多個main方法的入口,這是不允許的,特別是在go build時候,因此建議一個文件夾下面就僅僅只有一個go程序。

golang中的面向


分享到:


相關文章: