go語言之結構體和方法

胡洋®發表於2019-01-10

前言

關於物件導向程式設計大家肯定都十分熟悉了,物件導向程式設計的三個要素就是封裝、繼承和多型。但相對其他程式語言而言,go語言僅支援封裝,不支援繼承和多型,它沒有class概念,只有struct(結構體),本文主要總結了關於golang中結構體的建立和方法,通過建立一個二叉樹的樹結構並簡單實現其遍歷的方法觀察下在golang中是如何貫徹物件導向程式設計的理念的。

結構的建立

結構體定義

二叉樹是每個結點最多有兩個子樹的樹結構,它由一個一個樹節點組成,每個節點包含當前節點的值和左右兩個節點的地址,一個個節點連線組成一顆完整的樹,它的樹節點結構體型別可以定義如下:

type treeNode struct {
	value       int
	left, right *treeNode
}
複製程式碼

結構體初始化

golang中可以通過宣告式語法建立treeNode{value:666,left:nil,right:nil}(這裡若不指定某一結構體成員的值,該成員的值將預設用零值填充)或者直接將引數按結構體定義成員順序傳入treeNode{5, nil, nil}(這種初始化形式必須要給所有成員都進行賦值),除此之外還可以通過new(treeNode)內建函式進行初始化。

需要注意的是用new(T)內建函式進行初始化時,它返回的是一個分配了零值填充的結構體的記憶體空間的地址。

簡單建立一個樹結構如下述程式碼所示:

root := treeNode{value: 666}
root.left = &treeNode{value: 1}
root.right = &treeNode{5, nil, nil}
root.left.left = new(treeNode)
root.left.right = &treeNode{888, nil, nil}
複製程式碼

這裡需要注意的是root節點的right成員儲存的是地址,但仍然通過.操作符的形式去訪問其成員,這在go語言當中是可行的。因為go語言中規定:無論是地址還是結構本身,一律使用.操作法來訪問成員。

通過工廠函式建立結構體

golang中沒有建構函式的說法,其提供的struct已經滿足絕大多數場景下的應用,但是有些時候我們想要控制結構體的構造可以使用自定義的工廠函式返回區域性變數的地址,具體程式碼如下:

func createTreeNode(value int) *treeNode {
	return &treeNode{value: value}
}
複製程式碼

如果有c++程式設計經驗的同學可能會覺得上述程式碼有些奇怪,因為c++中區域性變數是分配在棧上的,函式退出後就會及時銷燬,而如果要傳出去則需要在堆上分配,而在堆上分配就必須要手動進行釋放,因此c++中是不允許函式中返回區域性變數的地址供外部程式進行使用的,而在golang中就不存在該限制。

結構是建立在堆上還是棧上

關於這個問題的答案是不需要知道,因為結構是建立在堆上還是棧上是由go語言的編譯器和它的執行環境決定的。比如說如果上述的工廠函式程式碼返回的treeNode沒有取地址而是直接返回值得話,編譯器很可能就認為這個變數不需要被外部程式使用,那麼它就會在棧上分配。反之如果treeNode取得是地址的話,那麼它就會在堆上進行分配,並參與垃圾回收機制。

因此我們需要注意和其他語言不同:go語言中的區域性變數不一定在退出函式就銷燬了

結構體方法

在結構體定義方法,不是寫在結構體花括號裡面,而是在結構體外面的。假設我們要給結構體定義一個方法讓其列印當前節點的值為後續進行遍歷做準備可以這樣做:

func (node treeNode) print() {
	fmt.Println(node.value)
}
複製程式碼

我們可以發現,結構體方法和普通函式的語法是非常類似的,唯一不同的是,結構體的方法在函式名前有一個接收者,意味著這個方法是由指定接收者接收的。go語言當中沒有this指標的概念,而是由接收者來代替this。

其實結構體方法本質上就是函式,我們可以把它的接收者看成函式引數的形式,因此結構體方法和下述寫法是等價的:

func print(node treeNode) {
	fmt.Println(node.value)
}
複製程式碼

這樣當想要呼叫此方法時,直接將接收者通過引數的形式傳入就可以了print(root),而通過結構體方法需要呼叫則是通過點操作符的形式root.print(),可以看出以上兩種寫法本質上其實是一樣的,只是呼叫語法上不同。

那麼結構體方法中接收者是按值傳遞還是按引用地址傳遞呢?

我們都知道,go語言中函式的引數都是按值傳遞的,既然接收者可以類比為函式的引數,那麼同理接收者也是一樣。如果接收者定義為指標接收者,那麼就會直接傳入呼叫者的地址,如果定義為值接收者,就會將呼叫者的地址解析出來拿到值後拷貝一份再傳入,非常靈活。

接下來分別通過值接收者和指標接收者實現一個給樹節點設定值的方法,觀察下二者的不同: 假設節點原有的值為666,分別看下呼叫setValue方法後二者的輸出結果。

// 值接收者
func (node treeNode) setValue(val int) {
	node.value = val
}
root.setValue(8)
// 輸出:666
複製程式碼
// 指標接收者
func (node *treeNode) setValue(val int) {
	node.value = val
}
root.setValue(8)
// 輸出:8
複製程式碼

由此我們可以看出,要想改變結構體內容時就需要使用指標接收者。

值接收者 vs 指標接收者

那什麼時候該使用值接收者,什麼時候使用指標接收者呢,可歸納為以下幾點:

  • 要更改內容的時候必須使用指標接收者
  • 值接收者是go語言特有,因為它函式傳參過程是通過值的拷貝,因此需要考慮效能問題,結構體過大也需要考慮使用指標接收者
  • 一致性:如果有指標接收者,最好都使用指標接收者
  • 值/指標接收者均可接受值/指標,定義方法的人可以隨意改動接收者的型別,這並不會改變呼叫方式

樹遍歷方法實現

掌握了結構體方法的定義後,來簡單實現一下二叉樹的前、中、後序遍歷。

首先來看看前序、中序、後序遍歷的特性:

前序遍歷

  1. 訪問根節點
  2. 前序遍歷左子樹
  3. 前序遍歷右子樹

中序遍歷

  1. 中序遍歷左子樹
  2. 訪問根節點
  3. 中序遍歷右子樹

後序遍歷

  1. 後序遍歷左子樹
  2. 後序遍歷右子樹
  3. 訪問根節點

通過上述二叉樹結構初始化程式碼後,建立的二叉樹結構如下所示:

image

// 前序遍歷
func (node *treeNode) traverse() {
	if node == nil {
		return
	}
	node.print()
	node.left.traverse()
	node.right.traverse()
}
// 輸出 666 1 0 888 5
複製程式碼
// 中序遍歷
func (node *treeNode) traverse() {
	if node == nil {
		return
	}
	node.left.traverse()
	node.print()
	node.right.traverse()
}
// 輸出 0 1 888 666 5
複製程式碼
// 後序遍歷
func (node *treeNode) traverse() {
	if node == nil {
		return
	}
	node.left.traverse()
	node.right.traverse()
	node.print()
}
// 輸出 0 888 1 5 666
複製程式碼

這裡需要注意的是:go語言中nil指標也可以呼叫方法,也就是說接收者允許空指標,因此我們不需要在呼叫方法前判斷呼叫者是否為空指標,但是在方法中需要判斷接收者是否為空指標,如果為空指標則直接中斷程式。

相關文章