Go 面向?qū)ο缶幊唐ǘ侯惖亩x、初始化和成員方法

上篇教程學院君簡單給大家介紹了 Go 語言的類型系統(tǒng),關(guān)于基礎類型、復合類型以及值語義和引用語義我們前面在數(shù)據(jù)類型篇里面已經(jīng)基本都介紹過了,接下來,我們就 Go 語言面向?qū)ο缶幊滔嚓P(guān)的特性展開介紹。
一、類的定義和初始化
Go 語言的面向?qū)ο缶幊膛c我們之前所熟悉的 PHP、Java 那一套完全不同,沒有 class、extends、implements 之類的關(guān)鍵字和相應的概念,而是借助結(jié)構(gòu)體來實現(xiàn)類的聲明,比如要定義一個學生類,可以這么做:
type Student struct {
id uint
name string
male bool
score float64
}
類名為 Student,并且包含了 id、name、male、score 四個屬性,Go 語言中也不支持構(gòu)造函數(shù)、析構(gòu)函數(shù),取而代之地,可以通過定義形如 NewXXX 這樣的全局函數(shù)(首字母大寫)作為類的初始化函數(shù):
func NewStudent(id uint, name string, male bool, score float64) *Student {
return &Student{id, name, male, score}
}
在這個函數(shù)中,我們通過傳入的屬性字段對 Student 類進行初始化并返回一個指向該類的指針,除此之外,還可以初始化指定字段:
func NewStudent(id uint, name string, score float64) *Student {
return &Student{id: id, name:name, score:score}
}
在 Go 語言中,未進行顯式初始化的變量都會被初始化為該類型的零值,例如
bool類型的零值為false,int類型的零值為 0,string類型的零值為空字符串,float類型的零值為0.0。
然后我們可以在 main() 函數(shù)中調(diào)用這個 NewStudent 函數(shù)對 Student 類進行初始化:
student := NewStudent(1, "學院君", 100)
fmt.Println(student)
上述代碼的打印結(jié)果如下:

二、定義類的成員方法
值方法
由于 Go 語言不支持 class 這樣的代碼塊,要為 Go 類定義成員方法,需要在 func 和方法名之間聲明方法所屬的類型(有的地方將其稱之為接收者聲明),以 Student 類為例,要為其定義獲取 name 值的方法,可以這么做:
func (s Student) GetName() string {
return s.name
}
這樣一來,我們就可以在初始化 Student 類后,通過 GetName() 方法獲取 name 值:
student := NewStudent(1, "學院君", 100)
fmt.Println("Name:", student.GetName())
可以看到,我們通過在函數(shù)簽名中增加接收者聲明的方式定義了函數(shù)所歸屬的類型,這個時候,函數(shù)就不再是普通的函數(shù),而是類的成員方法了。
指針方法
在類的成員方法中,可以通過聲明的類型變量來訪問類的屬性和其他方法(Go 語言不支持隱藏的 this 指針,所有的東西都是顯式聲明)。GetName 是一個只讀方法,如果我們要在外部通過 Student 類暴露的方法設置 name 值,可以這么做:
func (s *Student) SetName(name string) {
s.name = name
}
你可能已經(jīng)注意到,這里的方法聲明和前面 GetXXX 方法聲明不太一樣,Student 類型設置成了指針類型:
s *Student
這是因為 Go 語言面向?qū)ο缶幊滩幌?PHP、Java 那樣支持隱式的 this 指針,所有的東西都是顯式聲明的,在 GetXXX 方法中,由于不需要對類的成員變量進行修改,所以不需要傳入指針,而 SetXXX 方法需要在函數(shù)內(nèi)部修改成員變量的值,并且該修改要作用到該函數(shù)作用域以外,所以需要傳入指針類型(結(jié)構(gòu)體是值類型,不是引用類型,所以需要顯式傳入指針)。
我們可以把接收者類型為指針的成員方法叫做指針方法,把接收者類型為非指針的成員方法叫做值方法,二者的區(qū)別在于值方法傳入的結(jié)構(gòu)體變量是值類型(類型本身為指針類型除外),因此傳入函數(shù)內(nèi)部的是外部傳入結(jié)構(gòu)體實例的值拷貝,修改不會作用到外部傳入的結(jié)構(gòu)體實例。
接下來,我們可以在 main 函數(shù)中初始化 Student 類之后,通過 SetName 方法修改 name 值,然后再通過 GetName 將其打印出來:
student := NewStudent(1, "學院君", 100)
student.SetName("學院君小號")
fmt.Println("Name:", student.GetName())
打印結(jié)果是:

值方法和指針方法的區(qū)別
另外,需要聲明的是,在 Go 語言中,當我們將成員方法 SetName 所屬的類型聲明為指針類型時,嚴格來說,該方法并不屬于 Student 類,而是屬于指向 Student 的指針類型,所以,歸屬于 Student 的成員方法只是 Student 類型下所有可用成員方法的子集,歸屬于 *Student 的成員方法才是 Student 類完整可用方法的集合。
我們在調(diào)用方法時,之所以可以直接在 student 實例上調(diào)用 SetName 方法,是因為 Go 語言底層會自動將 student 轉(zhuǎn)化為對應的指針類型 &student,所以真正調(diào)用的代碼是 (&student).SetName("學院君小號"),這一點需要大家知曉。
總結(jié)下來,就是一個自定義數(shù)據(jù)類型的方法集合中僅會包含它的所有「值方法」,而該類型對應的指針類型包含的方法集合才囊括了該類型的所有方法,包括所有「值方法」和「指針方法」,指針方法可以修改所屬類型的屬性值,而值方法則不能。
Go 版 toString 方法實現(xiàn)
PHP、Java 支持默認調(diào)用類的 toString 方法以字符串格式打印類的實例,Go 語言也有類似的機制,只不過這個方法名是 String,以上面這個 Student 類型為例,我們?yōu)槠渚帉?String 方法如下:
func (s Student) String() string {
return fmt.Sprintf("{id: %d, name: %s, male: %t, score: %f}",
s.id, s.name, s.male, s.score)
}
然后我們可以在 main 方法中這樣調(diào)用來打印 Student 類實例:
student := NewStudent(1, "學院君", 100)
fmt.Println(student)
無需顯式調(diào)用 String 方法,Go 語言會自動調(diào)用該方法來打印,結(jié)果如下:

三、小結(jié)
我們來簡單總結(jié)下,在 Go 語言中,有意弱化了傳統(tǒng)面向?qū)ο缶幊讨械念惛拍睿@也符合 Go 語言的簡單設計哲學,基于結(jié)構(gòu)體定義的「類」就是和內(nèi)置的數(shù)據(jù)類型一樣的普通數(shù)據(jù)類型而已,內(nèi)置的數(shù)據(jù)類型也可以通過 type 關(guān)鍵字轉(zhuǎn)化為可以包含自定義成員方法的「類」。
一個數(shù)據(jù)類型關(guān)聯(lián)的所有方法,共同組成了該類型的方法集合,和其他支持面向?qū)ο缶幊痰恼Z言一樣,同一個方法集合中的方法也不能出現(xiàn)重名,并且,如果它們所屬的是一個結(jié)構(gòu)體類型,那么它們的名稱與該類型中任何字段的名稱也不能重復。
(本文完)
學習過程中有任何問題,可以通過下面的評論功能或加入「Go 語言研習社」與學院君討論:
本系列教程首發(fā)在 geekr.dev,你可以點擊頁面左下角閱讀原文鏈接查看最新更新的教程。
