在 Go 語言中,結(jié)構(gòu)體就像是類的一種簡化形式,那么面向?qū)ο蟪绦騿T可能會問:類的方法在哪里呢?在 Go 中有一個概念,它和方法有著同樣的名字,并且大體上意思相同:Go 方法是作用在接收者(receiver)上的一個函數(shù),接收者是某種類型的變量。因此方法是一種特殊類型的函數(shù)。
接收者類型可以是(幾乎)任何類型,不僅僅是結(jié)構(gòu)體類型:任何類型都可以有方法,甚至可以是函數(shù)類型,可以是 int、bool、string 或數(shù)組的別名類型。但是接收者不能是一個接口類型(參考 第 11 章),因為接口是一個抽象定義,但是方法卻是具體實現(xiàn);如果這樣做會引發(fā)一個編譯錯誤:invalid receiver type…。
最后接收者不能是一個指針類型,但是它可以是任何其他允許類型的指針。
一個類型加上它的方法等價于面向?qū)ο笾械囊粋€類。一個重要的區(qū)別是:在 Go 中,類型的代碼和綁定在它上面的方法的代碼可以不放置在一起,它們可以存在在不同的源文件,唯一的要求是:它們必須是同一個包的。
類型 T(或 *T)上的所有方法的集合叫做類型 T(或 *T)的方法集。
因為方法是函數(shù),所以同樣的,不允許方法重載,即對于一個類型只能有一個給定名稱的方法。但是如果基于接收者類型,是有重載的:具有同樣名字的方法可以在 2 個或多個不同的接收者類型上存在,比如在同一個包里這么做是允許的:
func (a *denseMatrix) Add(b Matrix) Matrix
func (a *sparseMatrix) Add(b Matrix) Matrix
別名類型不能有它原始類型上已經(jīng)定義過的方法。
定義方法的一般格式如下:
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
在方法名之前,func 關(guān)鍵字之后的括號中指定 receiver。
如果 recv 是 receiver 的實例,Method1 是它的方法名,那么方法調(diào)用遵循傳統(tǒng)的 object.name 選擇器符號:recv.Method1()。
如果 recv 是一個指針,Go 會自動解引用。
如果方法不需要使用 recv 的值,可以用 _ 替換它,比如:
func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }
recv 就像是面向?qū)ο笳Z言中的 this 或 self,但是 Go 中并沒有這兩個關(guān)鍵字。隨個人喜好,你可以使用 this 或 self 作為 receiver 的名字。下面是一個結(jié)構(gòu)體上的簡單方法的例子:
示例 10.10 method .go:
package main
import "fmt"
type TwoInts struct {
a int
b int
}
func main() {
two1 := new(TwoInts)
two1.a = 12
two1.b = 10
fmt.Printf("The sum is: %d\n", two1.AddThem())
fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20))
two2 := TwoInts{3, 4}
fmt.Printf("The sum is: %d\n", two2.AddThem())
}
func (tn *TwoInts) AddThem() int {
return tn.a + tn.b
}
func (tn *TwoInts) AddToParam(param int) int {
return tn.a + tn.b + param
}
輸出:
The sum is: 22
Add them to the param: 42
The sum is: 7
下面是非結(jié)構(gòu)體類型上方法的例子:
示例 10.11 method2.go:
package main
import "fmt"
type IntVector []int
func (v IntVector) Sum() (s int) {
for _, x := range v {
s += x
}
return
}
func main() {
fmt.Println(IntVector{1, 2, 3}.Sum()) // 輸出是6
}
練習(xí) 10.6 employee_salary.go
定義結(jié)構(gòu)體 employee,它有一個 salary 字段,給這個結(jié)構(gòu)體定義一個方法 giveRaise 來按照指定的百分比增加薪水。
練習(xí) 10.7 iteration_list.go
下面這段代碼有什么錯?
package main
import "container/list"
func (p *list.List) Iter() {
// ...
}
func main() {
lst := new(list.List)
for _= range lst.Iter() {
}
}
類型和作用在它上面定義的方法必須在同一個包里定義,這就是為什么不能在 int、float 或類似這些的類型上定義方法。試圖在 int 類型上定義方法會得到一個編譯錯誤:
cannot define new methods on non-local type int
比如想在 time.Time 上定義如下方法:
func (t time.Time) first3Chars() string {
return time.LocalTime().String()[0:3]
}
類型在其他的,或是非本地的包里定義,在它上面定義方法都會得到和上面同樣的錯誤。
但是有一個間接的方式:可以先定義該類型(比如:int 或 float)的別名類型,然后再為別名類型定義方法?;蛘呦裣旅孢@樣將它作為匿名類型嵌入在一個新的結(jié)構(gòu)體中。當(dāng)然方法只在這個別名類型上有效。
示例 10.12 method_on_time.go:
package main
import (
"fmt"
"time"
)
type myTime struct {
time.Time //anonymous field
}
func (t myTime) first3Chars() string {
return t.Time.String()[0:3]
}
func main() {
m := myTime{time.Now()}
// 調(diào)用匿名Time上的String方法
fmt.Println("Full time now:", m.String())
// 調(diào)用myTime.first3Chars
fmt.Println("First 3 chars:", m.first3Chars())
}
/* Output:
Full time now: Mon Oct 24 15:34:54 Romance Daylight Time 2011
First 3 chars: Mon
*/
函數(shù)將變量作為參數(shù):Function1(recv)
方法在變量上被調(diào)用:recv.Method1()
在接收者是指針時,方法可以改變接收者的值(或狀態(tài)),這點(diǎn)函數(shù)也可以做到(當(dāng)參數(shù)作為指針傳遞,即通過引用調(diào)用時,函數(shù)也可以改變參數(shù)的狀態(tài))。
不要忘記 Method1 后邊的括號 (),否則會引發(fā)編譯器錯誤:method recv.Method1 is not an expression, must be called
接收者必須有一個顯式的名字,這個名字必須在方法中被使用。
receiver_type 叫做 (接收者)基本類型,這個類型必須在和方法同樣的包中被聲明。
在 Go 中,(接收者)類型關(guān)聯(lián)的方法不寫在類型結(jié)構(gòu)里面,就像類那樣;耦合更加寬松;類型和方法之間的關(guān)聯(lián)由接收者來建立。
方法沒有和數(shù)據(jù)定義(結(jié)構(gòu)體)混在一起:它們是正交的類型;表示(數(shù)據(jù))和行為(方法)是獨(dú)立的。
鑒于性能的原因,recv 最常見的是一個指向 receiver_type 的指針(因為我們不想要一個實例的拷貝,如果按值調(diào)用的話就會是這樣),特別是在 receiver 類型是結(jié)構(gòu)體時,就更是如此了。
如果想要方法改變接收者的數(shù)據(jù),就在接收者的指針類型上定義該方法。否則,就在普通的值類型上定義方法。
下面的例子 pointer_value.go 作了說明:change()接受一個指向 B 的指針,并改變它內(nèi)部的成員;write() 通過拷貝接受 B 的值并只輸出B的內(nèi)容。注意 Go 為我們做了探測工作,我們自己并沒有指出是否在指針上調(diào)用方法,Go 替我們做了這些事情。b1 是值而 b2 是指針,方法都支持運(yùn)行了。
示例 10.13 pointer_value.go:
package main
import (
"fmt"
)
type B struct {
thing int
}
func (b *B) change() { b.thing = 1 }
func (b B) write() string { return fmt.Sprint(b) }
func main() {
var b1 B // b1是值
b1.change()
fmt.Println(b1.write())
b2 := new(B) // b2是指針
b2.change()
fmt.Println(b2.write())
}
/* 輸出:
{1}
{1}
*/
試著在 write() 中改變接收者b的值:將會看到它可以正常編譯,但是開始的 b 沒有被改變。
我們知道方法將指針作為接收者不是必須的,如下面的例子,我們只是需要 Point3 的值來做計算:
type Point3 struct { x, y, z float64 }
// A method on Point3
func (p Point3) Abs() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y + p.z*p.z)
}
這樣做稍微有點(diǎn)昂貴,因為 Point3 是作為值傳遞給方法的,因此傳遞的是它的拷貝,這在 Go 中是合法的。也可以在指向這個類型的指針上調(diào)用此方法(會自動解引用)。
假設(shè) p3 定義為一個指針:p3 := &Point{ 3, 4, 5}。
可以使用 p3.Abs() 來替代 (*p3).Abs()。
像例子 10.10(method1.go)中接收者類型是 *TwoInts 的方法 AddThem(),它能在類型 TwoInts 的值上被調(diào)用,這是自動間接發(fā)生的。
因此 two2.AddThem 可以替代 (&two2).AddThem()。
在值和指針上調(diào)用方法:
可以有連接到類型的方法,也可以有連接到類型指針的方法。
但是這沒關(guān)系:對于類型 T,如果在 *T 上存在方法 Meth(),并且 t 是這個類型的變量,那么 t.Meth() 會被自動轉(zhuǎn)換為 (&t).Meth()。
指針方法和值方法都可以在指針或非指針上被調(diào)用,如下面程序所示,類型 List 在值上有一個方法 Len(),在指針上有一個方法 Append(),但是可以看到兩個方法都可以在兩種類型的變量上被調(diào)用。
示例 10.14 methodset1.go:
package main
import (
"fmt"
)
type List []int
func (l List) Len() int { return len(l) }
func (l *List) Append(val int) { *l = append(*l, val) }
func main() {
// 值
var lst List
lst.Append(1)
fmt.Printf("%v (len: %d)", lst, lst.Len()) // [1] (len: 1)
// 指針
plst := new(List)
plst.Append(2)
fmt.Printf("%v (len: %d)", plst, plst.Len()) // &[2] (len: 1)
}
考慮 person2.go 中的 person 包:類型 Person 被明確的導(dǎo)出了,但是它的字段沒有被導(dǎo)出。例如在 use_person2.go 中 p.firstName 就是錯誤的。該如何在另一個程序中修改或者只是讀取一個 Person 的名字呢?
這可以通過面向?qū)ο笳Z言一個眾所周知的技術(shù)來完成:提供 getter 和 setter 方法。對于 setter 方法使用 Set 前綴,對于 getter 方法只使用成員名。
示例 10.15 person2.go:
package person
type Person struct {
firstName string
lastName string
}
func (p *Person) FirstName() string {
return p.firstName
}
func (p *Person) SetFirstName(newName string) {
p.firstName = newName
}
示例 10.16—use_person2.go:
package main
import (
"./person"
"fmt"
)
func main() {
p := new(person.Person)
// p.firstName undefined
// (cannot refer to unexported field or method firstName)
// p.firstName = "Eric"
p.SetFirstName("Eric")
fmt.Println(p.FirstName()) // Output: Eric
}
并發(fā)訪問對象
對象的字段(屬性)不應(yīng)該由 2 個或 2 個以上的不同線程在同一時間去改變。如果在程序發(fā)生這種情況,為了安全并發(fā)訪問,可以使用包 sync(參考第 9.3 節(jié))中的方法。在第 14.17 節(jié)中我們會通過 goroutines 和 channels 探索另一種方式。
當(dāng)一個匿名類型被內(nèi)嵌在結(jié)構(gòu)體中時,匿名類型的可見方法也同樣被內(nèi)嵌,這在效果上等同于外層類型 繼承 了這些方法:將父類型放在子類型中來實現(xiàn)亞型。這個機(jī)制提供了一種簡單的方式來模擬經(jīng)典面向?qū)ο笳Z言中的子類和繼承相關(guān)的效果,也類似 Ruby 中的混入(mixin)。
下面是一個示例(可以在練習(xí) 10.8 中進(jìn)一步學(xué)習(xí)):假定有一個 Engine 接口類型,一個 Car 結(jié)構(gòu)體類型,它包含一個 Engine 類型的匿名字段:
type Engine interface {
Start()
Stop()
}
type Car struct {
Engine
}
我們可以構(gòu)建如下的代碼:
func (c *Car) GoToWorkIn() {
// get in car
c.Start()
// drive to work
c.Stop()
// get out of car
}
下面是 method3.go 的完整例子,它展示了內(nèi)嵌結(jié)構(gòu)體上的方法可以直接在外層類型的實例上調(diào)用:
package main
import (
"fmt"
"math"
)
type Point struct {
x, y float64
}
func (p *Point) Abs() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
type NamedPoint struct {
Point
name string
}
func main() {
n := &NamedPoint{Point{3, 4}, "Pythagoras"}
fmt.Println(n.Abs()) // 打印5
}
內(nèi)嵌將一個已存在類型的字段和方法注入到了另一個類型里:匿名字段上的方法“晉升”成為了外層類型的方法。當(dāng)然類型可以有只作用于本身實例而不作用于內(nèi)嵌“父”類型上的方法,
可以覆寫方法(像字段一樣):和內(nèi)嵌類型方法具有同樣名字的外層類型的方法會覆寫內(nèi)嵌類型對應(yīng)的方法。
在示例 10.18 method4.go 中添加:
func (n *NamedPoint) Abs() float64 {
return n.Point.Abs() * 100.
}
現(xiàn)在 fmt.Println(n.Abs()) 會打印 500。
因為一個結(jié)構(gòu)體可以嵌入多個匿名類型,所以實際上我們可以有一個簡單版本的多重繼承,就像:type Child struct { Father; Mother}。在第 10.6.7 節(jié)中會進(jìn)一步討論這個問題。
結(jié)構(gòu)體內(nèi)嵌和自己在同一個包中的結(jié)構(gòu)體時,可以彼此訪問對方所有的字段和方法。
練習(xí) 10.8 inheritance_car.go
創(chuàng)建一個上面 Car 和 Engine 可運(yùn)行的例子,并且給 Car 類型一個 wheelCount 字段和一個 numberOfWheels() 方法。
創(chuàng)建一個 Mercedes 類型,它內(nèi)嵌 Car,并新建 Mercedes 的一個實例,然后調(diào)用它的方法。
然后僅在 Mercedes 類型上創(chuàng)建方法 sayHiToMerkel() 并調(diào)用它。
主要有兩種方法來實現(xiàn)在類型中嵌入功能:
A:聚合(或組合):包含一個所需功能類型的具名字段。
B:內(nèi)嵌:內(nèi)嵌(匿名地)所需功能類型,像前一節(jié) 10.6.5 所演示的那樣。
為了使這些概念具體化,假設(shè)有一個 Customer 類型,我們想讓它通過 Log 類型來包含日志功能,Log 類型只是簡單地包含一個累積的消息(當(dāng)然它可以是復(fù)雜的)。如果想讓特定類型都具備日志功能,你可以實現(xiàn)一個這樣的 Log 類型,然后將它作為特定類型的一個字段,并提供 Log(),它返回這個日志的引用。
方式 A 可以通過如下方法實現(xiàn)(使用了第 10.7 節(jié)中的 String() 功能):
示例 10.19 embed_func1.go:
package main
import (
"fmt"
)
type Log struct {
msg string
}
type Customer struct {
Name string
log *Log
}
func main() {
c := new(Customer)
c.Name = "Barak Obama"
c.log = new(Log)
c.log.msg = "1 - Yes we can!"
// shorter
c = &Customer{"Barak Obama", &Log{"1 - Yes we can!"}}
// fmt.Println(c) &{Barak Obama 1 - Yes we can!}
c.Log().Add("2 - After me the world will be a better place!")
//fmt.Println(c.log)
fmt.Println(c.Log())
}
func (l *Log) Add(s string) {
l.msg += "\n" + s
}
func (l *Log) String() string {
return l.msg
}
func (c *Customer) Log() *Log {
return c.log
}
輸出:
1 - Yes we can!
2 - After me the world will be a better place!
相對的方式 B 可能會像這樣:
package main
import (
"fmt"
)
type Log struct {
msg string
}
type Customer struct {
Name string
Log
}
func main() {
c := &Customer{"Barak Obama", Log{"1 - Yes we can!"}}
c.Add("2 - After me the world will be a better place!")
fmt.Println(c)
}
func (l *Log) Add(s string) {
l.msg += "\n" + s
}
func (l *Log) String() string {
return l.msg
}
func (c *Customer) String() string {
return c.Name + "\nLog:" + fmt.Sprintln(c.Log)
}
輸出:
Barak Obama
Log:{1 - Yes we can!
2 - After me the world will be a better place!}
內(nèi)嵌的類型不需要指針,Customer 也不需要 Add 方法,它使用 Log 的 Add 方法,Customer 有自己的 String 方法,并且在它里面調(diào)用了 Log 的 String 方法。
如果內(nèi)嵌類型嵌入了其他類型,也是可以的,那些類型的方法可以直接在外層類型中使用。
因此一個好的策略是創(chuàng)建一些小的、可復(fù)用的類型作為一個工具箱,用于組成域類型。
多重繼承指的是類型獲得多個父類型行為的能力,它在傳統(tǒng)的面向?qū)ο笳Z言中通常是不被實現(xiàn)的(C++ 和 Python 例外)。因為在類繼承層次中,多重繼承會給編譯器引入額外的復(fù)雜度。但是在 Go 語言中,通過在類型中嵌入所有必要的父類型,可以很簡單的實現(xiàn)多重繼承。
作為一個例子,假設(shè)有一個類型 CameraPhone,通過它可以 Call(),也可以 TakeAPicture(),但是第一個方法屬于類型 Phone,第二個方法屬于類型 Camera。
只要嵌入這兩個類型就可以解決這個問題,如下所示:
package main
import (
"fmt"
)
type Camera struct{}
func (c *Camera) TakeAPicture() string {
return "Click"
}
type Phone struct{}
func (p *Phone) Call() string {
return "Ring Ring"
}
type CameraPhone struct {
Camera
Phone
}
func main() {
cp := new(CameraPhone)
fmt.Println("Our new CameraPhone exhibits multiple behaviors...")
fmt.Println("It exhibits behavior of a Camera: ", cp.TakeAPicture())
fmt.Println("It works like a Phone too: ", cp.Call())
}
輸出:
Our new CameraPhone exhibits multiple behaviors...
It exhibits behavior of a Camera: Click
It works like a Phone too: Ring Ring
練習(xí) 10.9 point_methods.go:
從 point.go 開始(第 10.1 節(jié)的練習(xí)):使用方法來實現(xiàn) Abs() 和 Scale()函數(shù),Point 作為方法的接收者類型。也為 Point3 和 Polar 實現(xiàn) Abs() 方法。完成了 point.go 中同樣的事情,只是這次通過方法。
練習(xí) 10.10 inherit_methods.go:
定義一個結(jié)構(gòu)體類型 Base,它包含一個字段 id,方法 Id() 返回 id,方法 SetId() 修改 id。結(jié)構(gòu)體類型 Person 包含 Base,及 FirstName 和 LastName 字段。結(jié)構(gòu)體類型 Employee 包含一個 Person 和 salary 字段。
創(chuàng)建一個 employee 實例,然后顯示它的 id。
練習(xí) 10.11 magic.go:
首先預(yù)測一下下面程序的結(jié)果,然后動手實驗下:
package main
import (
"fmt"
)
type Base struct{}
func (Base) Magic() {
fmt.Println("base magic")
}
func (self Base) MoreMagic() {
self.Magic()
self.Magic()
}
type Voodoo struct {
Base
}
func (Voodoo) Magic() {
fmt.Println("voodoo magic")
}
func main() {
v := new(Voodoo)
v.Magic()
v.MoreMagic()
}
在編程中一些基本操作會一遍又一遍的出現(xiàn),比如打開(Open)、關(guān)閉(Close)、讀(Read)、寫(Write)、排序(Sort)等等,并且它們都有一個大致的意思:打開(Open)可以作用于一個文件、一個網(wǎng)絡(luò)連接、一個數(shù)據(jù)庫連接等等。具體的實現(xiàn)可能千差萬別,但是基本的概念是一致的。在 Go 語言中,通過使用接口(參考 第 11 章),標(biāo)準(zhǔn)庫廣泛的應(yīng)用了這些規(guī)則,在標(biāo)準(zhǔn)庫中這些通用方法都有一致的名字,比如 Open()、Read()、Write()等。想寫規(guī)范的 Go 程序,就應(yīng)該遵守這些約定,給方法合適的名字和簽名,就像那些通用方法那樣。這樣做會使 Go 開發(fā)的軟件更加具有一致性和可讀性。比如:如果需要一個 convert-to-string 方法,應(yīng)該命名為 String(),而不是 ToString()(參考第 10.7 節(jié))。
在如 C++、Java、C# 和 Ruby 這樣的面向?qū)ο笳Z言中,方法在類的上下文中被定義和繼承:在一個對象上調(diào)用方法時,運(yùn)行時會檢測類以及它的超類中是否有此方法的定義,如果沒有會導(dǎo)致異常發(fā)生。
在 Go 語言中,這樣的繼承層次是完全沒必要的:如果方法在此類型定義了,就可以調(diào)用它,和其他類型上是否存在這個方法沒有關(guān)系。在這個意義上,Go 具有更大的靈活性。
下面的模式就很好的說明了這個問題:

Go 不需要一個顯式的類定義,如同 Java、C++、C# 等那樣,相反地,“類”是通過提供一組作用于一個共同類型的方法集來隱式定義的。類型可以是結(jié)構(gòu)體或者任何用戶自定義類型。
比如:我們想定義自己的 Integer 類型,并添加一些類似轉(zhuǎn)換成字符串的方法,在 Go 中可以如下定義:
type Integer int
func (i *Integer) String() string {
return strconv.Itoa(int(*i))
}
在 Java 或 C# 中,這個方法需要和類 Integer 的定義放在一起,在 Ruby 中可以直接在基本類型 int 上定義這個方法。
總結(jié)
在 Go 中,類型就是類(數(shù)據(jù)和關(guān)聯(lián)的方法)。Go 不知道類似面向?qū)ο笳Z言的類繼承的概念。繼承有兩個好處:代碼復(fù)用和多態(tài)。
在 Go 中,代碼復(fù)用通過組合和委托實現(xiàn),多態(tài)通過接口的使用來實現(xiàn):有時這也叫 組件編程(Component Programming)。
許多開發(fā)者說相比于類繼承,Go 的接口提供了更強(qiáng)大、卻更簡單的多態(tài)行為。
備注
如果真的需要更多面向?qū)ο蟮哪芰Γ匆幌?goop 包(Go Object-Oriented Programming),它由 Scott Pakin 編寫: 它給 Go 提供了 JavaScript 風(fēng)格的對象(基于原型的對象),并且支持多重繼承和類型獨(dú)立分派,通過它可以實現(xiàn)你喜歡的其他編程語言里的一些結(jié)構(gòu)。
問題 10.1
我們在某個類型的變量上使用點(diǎn)號調(diào)用一個方法:variable.method(),在使用 Go 以前,在哪兒碰到過面向?qū)ο蟮狞c(diǎn)號?
問題 10.2
a)假設(shè)定義: type Integer int,完成 get() 方法的方法體: func (p Integer) get() int { ... }。
b)定義: func f(i int) {}; var v Integer ,如何就 v 作為參數(shù)調(diào)用f?
c)假設(shè) Integer 定義為 type Integer struct {n int},完成 get() 方法的方法體:func (p Integer) get() int { ... }。
d)對于新定義的 Integer,和 b)中同樣的問題。