再續前文,在物件導向層面,Python做到了超神:萬物皆為物件,而Ruby,則乾脆就是神:飛花摘葉皆可物件。二者都提供物件類操作以及繼承的方式為物件導向張目,但Go lang顯然有一些特立獨行,因為它沒有傳統的類,也沒有繼承,取而代之的是結構和組合的方式,也就是結構體(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
}
宣告了結構體之後,我們可以根據宣告好的結構體型別來建立結構體,這個過程有點像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}
通過點操作符 . 可以存取結構體的屬性:
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的結構體就是物件類的變種,雖然並沒有顯性的繼承操作,但是通過巢狀結構體和提升欄位兩種方式,也能達到「繼承」的效果,結構體的最終目的和效果與物件類並無二致,類比的話,有點像電腦散熱的兩種方式:風冷和水冷,我們不能說哪一種方式更好或者不好,只要這種方式可以完成專案中的需求即可,不管黑貓白貓,只要能抓到耗子,就是好貓。