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

  • 2022 年 8 月 10 日
  • 筆記

再續前文,在面向對象層面,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的結構體就是對象類的變種,雖然並沒有顯性的繼承操作,但是通過嵌套結構體和提升字段兩種方式,也能達到「繼承」的效果,結構體的最終目的和效果與對象類並無二致,類比的話,有點像電腦散熱的兩種方式:風冷和水冷,我們不能說哪一種方式更好或者不好,只要這種方式可以完成項目中的需求即可,不管黑貓白貓,只要能抓到耗子,就是好貓。