你有物件類,我有結構體,Go lang1.18入門精煉教程,由白丁入鴻儒,go lang結構體(struct)的使用EP06

語言: CN / TW / HK

再續前文,在面向物件層面,Python做到了超神:萬物皆為物件,而Ruby,則乾脆就是神:飛花摘葉皆可物件。二者都提供物件類操作以及繼承的方式為面向物件張目,但Go lang顯然有一些特立獨行,因為它沒有傳統的類,也沒有繼承,取而代之的是結構和組合的方式,也就是結構體(struct)的方式來組織程式碼,達到類似類的效果。

結構體struct的宣告

在 Go lang中使用下面的語法是對結構體的宣告:

type struct_name struct {  
    attribute_name1   attribute_type  
    attribute_name2   attribute_type  
    ...  
}

假設定義一個名為 Lesson(課程) 的結構體:

type Lesson struct {  
	name   string //名稱  
	target string //學習目標  
	spend  int    //學習花費時間  
}

這裡聲明瞭一個結構體型別 Lesson ,它有 name 、 target 和 spend 三個屬性,相當於Python中類的私有屬性。

也可以把相同型別的屬性宣告在同一行,這樣可以使結構體變得更加緊湊:

type Lesson2 struct {  
    name, target    string  
    spend             int  
}

Lesson 稱為命名的結構體(Named Structure) ,這裡 Lesson 作為一種新的資料型別而存在,而它可以用於建立 Lesson 型別的結構體變數。

此外,宣告結構體時也可以不用宣告一個新型別,這樣的結構體型別稱為匿名結構體(Anonymous Structure) ,可以理解為結構體變數:

var MyLesson struct {  
    name, target    string  
    spend             int  
}

結構體struct的建立

聲明瞭結構體之後,我們可以根據宣告好的結構體型別來建立結構體,這個過程有點像Python語言中類的例項化:

import "fmt"  
  
type Lesson struct {  
	name, target string  
	spend        int  
}  
  
func main() {  
	// 使用欄位名建立結構體  
	lesson1 := Lesson{  
		name:   "go lang 1.18",  
		target: "學習Go lang,並完成web開發任務",  
		spend:  1,  
	}  
	// 不使用欄位名建立結構體  
	lesson2 := Lesson{"go lang 1.18", "學習Go lang,並完成web開發任務", 1}  
  
	fmt.Println("lesson1 ", lesson1)  
	fmt.Println("lesson2 ", lesson2)  
}

程式返回:

lesson1  {go lang 1.18 學習Go lang,並完成web開發任務 1}  
lesson2  {go lang 1.18 學習Go lang,並完成web開發任務 1}

這裡欄位名可以做省略操作,但要注意傳遞順序。

此外,也可以建立匿名結構體:

package main  
  
import "fmt"  
  
func main() {  
	// 建立匿名結構體變數  
	mylesson := struct {  
		name, target string  
		spend        int  
	}{  
		name:   "Go lang 1.18",  
		target: "學習go lang,完成web需求",  
		spend:  1,  
	}  
  
	fmt.Println("mylesson ", mylesson)  
}

當定義好的結構體沒有被顯式賦值時,結構體的欄位將會預設賦為相應型別的零值:

package main  
  
import "fmt"  
  
type Lesson struct {  
	name, target string  
	spend        int  
}  
  
func main() {  
	// 不初始化結構體  
	var lesson = Lesson{}  
  
	fmt.Println("lesson ", lesson)  
}

程式返回:

lesson  {  0}

假設某個或者某幾個欄位沒有賦值,也會預設賦值為對應基本資料型別的零值:

package main  
  
import "fmt"  
  
type Lesson struct {  
	name, target    string  
	spend             int  
}  
  
func main() {  
	// 為結構體指定欄位賦初值  
	var lesson = Lesson{  
		name: "go lang 1.18",  
	}  
  
    // 上面的結構體變數 lesson 只初始化了 name 欄位, 其他欄位沒有初始化,所以會被初始化為零值  
	fmt.Println("lesson ", lesson)  
}

程式返回:

lesson  {go lang 1.18  0}

結構體struct的屬性與指標

通過點操作符 . 可以訪問結構體的屬性:

package main  
  
import "fmt"  
  
type Lesson struct {  
	name, target string  
	spend        int  
}  
  
func main() {  
  
	var lesson = Lesson{  
		name: "go lang 1.18",  
	}  
  
	fmt.Println("lesson name ", lesson.name)  
}

程式返回:

lesson name  go lang 1.18

也可以使用點操作符 . 對結構體的欄位進行賦值操作:

package main  
  
import "fmt"  
  
type Lesson struct {  
	name, target string  
	spend        int  
}  
  
func main() {  
  
	var lesson = Lesson{  
		name: "go lang 1.18",  
	}  
  
	fmt.Println("lesson name ", lesson.name)  
  
	lesson.name = "Python 3.11"  
  
	fmt.Println("lesson name ", lesson.name)  
  
}

程式返回:

lesson name  go lang 1.18  
lesson name  Python 3.11

需要注意的是,賦值變數和結構體屬性的基本資料型別要一致。

在前一篇: 借問變數何處存,牧童笑稱用指標,Go lang1.18入門精煉教程,由白丁入鴻儒,go lang型別指標(Pointer)的使用EP05 我們使用了指標來操作變數,指標也可以指向結構體:

package main  
  
import "fmt"  
  
type Lesson struct {  
	name, target string  
	spend        int  
}  
  
func main() {  
	lesson := &Lesson{"go lang 1.18", "完成對應web需求", 1}  
	fmt.Println("lesson name: ", (*lesson).name)  
	fmt.Println("lesson name: ", lesson.name)  
}

程式返回:

lesson name:  go lang 1.18  
lesson name:  go lang 1.18

lesson是一個指向結構體 Lesson 的指標,上面用 (*lesson).name 訪問 lesson的 name 欄位,lesson.name 是代替 (*lesson).name 的解引用訪問。

在建立結構體時,屬性可以只有型別沒有屬性名,這種屬性稱為匿名欄位(Anonymous Field) :

package main  
  
import "fmt"  
  
type Lesson struct {  
	string  
	int  
}  
  
func main() {  
	lesson := Lesson{"go lang 1.18", 1}  
	fmt.Println("lesson ", lesson)  
	fmt.Println("lesson string: ", lesson.string)  
	fmt.Println("lesson int: ", lesson.int)  
}

程式返回:

lesson  {go lang 1.18 1}  
lesson string:  go lang 1.18  
lesson int:  1

這裡程式結構體定義了兩個匿名欄位,雖然這兩個欄位沒有欄位名,但匿名欄位的名稱預設就是它的型別。所以上面的結構體 Lesoon 有兩個名為 string 和 int 的欄位,同樣需要注意順序和欄位資料型別的匹配問題。

巢狀結構體

結構體本身也支援複合的巢狀結構:

package main  
  
import "fmt"  
  
type Author struct {  
	name string  
}  
  
type Lesson struct {  
	name, target string  
	spend        int  
	author       Author  
}  
  
func main() {  
	lesson := Lesson{  
		name:  "go lang 1.18",  
		spend: 1,  
	}  
	lesson.author = Author{  
		name: "佚名",  
	}  
	fmt.Println("lesson name:", lesson.name)  
	fmt.Println("lesson spend:", lesson.spend)  
	fmt.Println("lesson author name:", lesson.author.name)  
}

程式返回:

lesson name: go lang 1.18  
lesson spend: 1  
lesson author name: 佚名

這裡結構體Author本身作為結構體Lesson的一個屬性而存在,賦值時,通過父結構體直接呼叫子結構體名稱即可。

如果結構體中有匿名的結構體型別欄位,則該匿名結構體裡的欄位就稱為提升欄位(Promoted Fields) 。這是因為提升欄位就像是屬於外部結構體一樣,可以用外部結構體直接訪問:

package main  
  
import (  
	"fmt"  
)  
  
type Address struct {  
	city, state string  
}  
type Person struct {  
	name string  
	age  int  
	Address  
}  
  
func main() {  
	var p Person  
	p.name = "Naveen"  
	p.age = 50  
	p.Address = Address{  
		city:  "Chicago",  
		state: "Illinois",  
	}  
	fmt.Println("Name:", p.name)  
	fmt.Println("Age:", p.age)  
	fmt.Println("City:", p.city)   //city is promoted field  
	fmt.Println("State:", p.state) //state is promoted field  
}

系統返回:

Name: Naveen  
Age: 50  
City: Chicago  
State: Illinois

如果我們把 Person 結構體中的欄位 address 直接用匿名欄位 Address 代替, Address 結構體的欄位例如 city 就不用像 p.address.city這樣訪問,而是使用 p.address 就能訪問 Address 結構體中的 address欄位。現在結構體 Address 有city欄位,訪問欄位就像在 Person 裡直接宣告的一樣,因此我們稱之為提升欄位,說白了就是把子結構體的欄位提升為父結構體的欄位,但是定義還是在子結構體之中。

假設結構體的全部屬性都是可以比較的,那麼結構體也是可以比較的,那樣的話兩個結構體將可以使用 == 或 != 運算子進行比較。可以通過==運算子或 DeeplyEqual()函式比較兩個結構相同的型別幷包含相同的欄位值:

package main  
  
import (    
    "fmt"  
)  
  
type name struct {    
    firstName string  
    lastName string  
}  
  
  
func main() {    
    name1 := name{"Steve", "Jobs"}  
    name2 := name{"Steve", "Jobs"}  
    if name1 == name2 {  
        fmt.Println("name1 and name2 are equal")  
    } else {  
        fmt.Println("name1 and name2 are not equal")  
    }  
  
    name3 := name{firstName:"Steve", lastName:"Jobs"}  
    name4 := name{}  
    name4.firstName = "Steve"  
    if name3 == name4 {  
        fmt.Println("name3 and name4 are equal")  
    } else {  
        fmt.Println("name3 and name4 are not equal")  
    }  
}

程式返回:

name1 and name2 are equal  
name3 and name4 are not equal

如果結構變數包含的欄位是不可比較的,那麼結構變數是不可比較的:

package main  
  
import (  
	"fmt"  
)  
  
type image struct {  
	data map[int]int  
}  
  
func main() {  
	image1 := image{data: map[int]int{  
		0: 155,  
	}}  
	image2 := image{data: map[int]int{  
		0: 155,  
	}}  
	if image1 == image2 {  
		fmt.Println("image1 and image2 are equal")  
	}  
}

程式報錯:

# command-line-arguments  
.\test.go:18:5: invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)

由此可知,結構體的比較可以理解為其屬性的批量比較。

結構體繫結方法

在 Go lang中無法在結構體內部定義方法,這一點與 C 語言類似:

package main  
  
import "fmt"  
  
// Lesson 定義一個名為 Lesson 的結構體  
type Lesson struct {  
	name, target string  
	spend        int  
}  
  
// ShowLessonInfo 定義一個與 Lesson 的繫結的方法  
func (l Lesson) ShowLessonInfo() {  
	fmt.Println("name:", l.name)  
	fmt.Println("target:", l.target)  
}  
  
func main() {  
	l := Lesson{  
		name: "go lang 1.1 8",  
	}  
	l.ShowLessonInfo()  
}

程式返回:

name: go lang 1.1 8  
target:

這裡定義了一個與結構體 Lesson 繫結的方法 ShowLessonInfo() ,其中 ShowLessonInfo 是方法名, (l Lesson) 表示將此方法與 Lesson 的例項繫結,這在 Go lang中稱為接收者,而 l 表示例項本身,相當於 Python 中的 self ,在方法內可以使用例項本身.屬性名稱來訪問例項屬性。

如果繫結結構體的方法中要改變例項的屬性時,必須使用指標作為方法的接收者:

package main  
  
import "fmt"  
  
// Lesson 定義一個名為 Lesson 的結構體  
type Lesson struct {  
	name, target string  
	spend        int  
}  
  
// ShowLessonInfo 定義一個與 Lesson 的繫結的方法  
func (l Lesson) ShowLessonInfo() {  
	fmt.Println("spend:", l.spend)  
}  
  
// AddTime 定義一個與 Lesson 的繫結的方法,使 spend 值加 n  
func (l *Lesson) AddTime(n int) {  
	l.spend = l.spend + n  
}  
  
func main() {  
	lesson13 := Lesson{  
		spend: 1,  
	}  
	fmt.Println("新增add方法前")  
	lesson13.ShowLessonInfo()  
	lesson13.AddTime(5)  
	fmt.Println("新增add方法後")  
	lesson13.ShowLessonInfo()  
}

程式返回:

新增add方法前  
spend: 1  
新增add方法後  
spend: 6

結語

大抵上,Go lang的結構體就是物件類的變種,雖然並沒有顯性的繼承操作,但是通過巢狀結構體和提升欄位兩種方式,也能達到“繼承”的效果,結構體的最終目的和效果與物件類並無二致,類比的話,有點像電腦散熱的兩種方式:風冷和水冷,我們不能說哪一種方式更好或者不好,只要這種方式可以完成專案中的需求即可,不管黑貓白貓,只要能抓到耗子,就是好貓。