聊聊 Go 語言中的物件導向程式設計

lotus_ruan發表於2021-09-09

作者:
程式設計拯救世界(ID: CodeWarrior_):專注於程式設計基礎與服務端研發。

我們知道,在 Go 語言中沒有類(Class)的概念,但這並不意味著 Go 語言不支援,畢竟物件導向只是一種程式設計思想。

讓我們回憶一下物件導向的三大基本特徵:

  1. 封裝:隱藏物件的屬性和實現細節,僅對外提供公共訪問方式
  2. 繼承:使得子類具有父類的屬性和方法或者重新定義、追加屬性和方法等
  3. 多型:不同物件中同種行為的不同實現方式

我們一起來看看 Go 語言是如何在沒有類(Class)的情況下實現這三大特徵的。

封裝

「類」

在 Go 語言中可以使用(Structs)對屬性進行封裝,結構體就像是類的一種簡化形式。

例如,我們要定義一個矩形,每個矩形都有長和寬,我們可以這樣進行封裝:

type Rectangle struct {
	Length int
	Width int
}

方法

既然有了「類」,你可能會問了,那「類」的方法在哪呢?

Go 語言中也有(Methods):Go 方法是作用在接收者(receiver)上的一個函式,接收者是某種型別的變數。因此方法是一種特殊型別的函式。

定義方法的格式如下:

func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }

上文中我們已經定義了一個矩形 Rectangle,現在我們要定義一個方法 Area() 來計算它的面積:

package main

import (
	"fmt"
)

// 矩形結構體
type Rectangle struct {
	Length int
	Width  int
}

// 計算矩形面積
func (r *Rectangle) Area() int {
	return r.Length * r.Width
}

func main() {
	r := Rectangle{4, 2}
	// 呼叫 Area() 方法,計算面積
	fmt.Println(r.Area())
}

上面的程式碼片段輸出結果為 8。

訪問許可權

我們常會說一個類的屬性是公共的還是私有的,在其他程式語言中,我們常用 publicprivate 關鍵字來表達這樣一種訪問許可權。

在 Go 語言中沒有 publicprivateprotected 這樣的訪問控制修飾符,它是透過字母大小寫來控制可見性的。

如果定義的常量、變數、型別、介面、結構、函式等的名稱是大寫字母開頭,這表示它們能被其它包訪問或呼叫(相當於 public);非大寫開頭就只能在包內使用(相當於 private)。

訪問未匯出欄位

當遇到只能在包內使用的未匯出欄位時,我們又該如何訪問呢?

和其他面嚮物件語言一樣,Go 語言也有實現 gettersetter 的方式:

  • 對於 setter 方法使用 Set 字首
  • 對於 getter 方法只使用成員名

例如我們現在有一個處於 person 包中的 Person 結構體:

package person

type Person struct {
	firstName string
	lastName  string
}

我們可以看到,它的兩個成員變數都是非大寫字母開頭,只能在包內使用,現在我們為其中的 firstName 來定義 settergetter

// 獲取 firstName
func (p *Person) FirstName() string {
	return p.firstName
}

// 設定 firstName
func (p *Person) SetFirstName(newName string) {
	p.firstName = newName
}

這樣一來,我們就可以在 main 包裡設定和獲取 firstName 的值了:

package main

import (
	"fmt"

	"./person"
)

func main() {
	p := new(person.Person)
	p.SetFirstName("firstName")
	fmt.Println(p.FirstName())
}

/* Output:
firstName
*/

繼承

在 Go 語言中沒有 extends 關鍵字,它使用在結構體中內嵌匿名型別的方法來實現繼承。

匿名型別:即這些型別沒有顯式的名字。

我們定義一個 Engine 介面型別,一個 Car 結構體,讓 Car 結構體包含一個 Engine 型別的匿名欄位:

type Engine interface {
	Start()
	Stop()
}

type Car struct {
	Engine // 包含 Engine 型別的匿名欄位
}

此時,匿名欄位 Engine 上的方法「晉升」成為了外層型別 Car 的方法。我們可以構建出如下程式碼:

func (c *Car) GoToWorkIn() {
	// get in car
	c.Start()
	// drive to work
	c.Stop()
	// get out of car
}

多型

在物件導向中,多型的特徵為:不同物件中同種行為的不同實現方式。在 Go 語言中可以使用實現這一特徵。

我們先定義一個正方形 Square 和一個長方形 Rectangle

// 正方形
type Square struct {
	side float32
}

// 長方形
type Rectangle struct {
	length, width float32
}

然後,我們希望可以計算出這兩個幾何圖形的面積。但由於他們的面積計算方式不同,我們需要定義兩個不同的 Area() 方法。

於是,我們可以定義一個包含 Area() 方法的介面 Shaper,讓 SquareRectangle 都實現這個介面裡的 Area()

// 介面 Shaper
type Shaper interface {
	Area() float32
}

// 計算正方形的面積
func (sq *Square) Area() float32 {
	return sq.side * sq.side
}

// 計算長方形的面積
func (r *Rectangle) Area() float32 {
	return r.length * r.width
}

我們可以在 main() 函式中這樣呼叫 Area()

func main() {
	r := &Rectangle{10, 2}
	q := &Square{10}

	// 建立一個 Shaper 型別的陣列
	shapes := []Shaper{r, q}
	// 迭代陣列上的每一個元素並呼叫 Area() 方法
	for n, _ := range shapes {
		fmt.Println("圖形資料: ", shapes[n])
		fmt.Println("它的面積是: ", shapes[n].Area())
	}
}

/*Output:
圖形資料:  &{10 2}
它的面積是:  20
圖形資料:  &{10}
它的面積是:  100
*/

由以上程式碼輸出結果可知:不同物件呼叫 Area() 方法產生了不同的結果,展現了多型的特徵。

總結

  • 物件導向的三大特徵是:封裝、繼承和多型
  • Go 語言使用結構體對屬性進行封裝,結構體就像是類的一種簡化形式
  • 在 Go 語言中,方法是作用在接收者(receiver)上的一個函式,接收者是某種型別的變數
  • 名稱首字母的大小寫決定了該變數/常量/型別/介面/結構/函式……能否被外部包匯入
  • 無法被匯入的欄位可以使用 gettersetter 的方式來訪問
  • Go 語言使用在結構體中內嵌匿名型別的方法來實現繼承
  • 使用介面可以實現多型

參考書籍

  • 《Go 入門指南》:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1762/viewspace-2824393/,如需轉載,請註明出處,否則將追究法律責任。

相關文章