鍍金池/ 教程/ GO/ 10.6 方法
4.7 strings 和 strconv 包
13.6 啟動(dòng)外部命令和程序
?# 11.4 類型判斷:type-switch
12.1 讀取用戶的輸入
10.6 方法
12.2 文件讀寫
13 錯(cuò)誤處理與測試
9.3 鎖和 sync 包
12.3 文件拷貝
?# 11.7 第一個(gè)例子:使用 Sorter 接口排序
?# 11.5 測試一個(gè)值是否實(shí)現(xiàn)了某個(gè)接口
6.4 defer 和追蹤
12.10 XML 數(shù)據(jù)格式
13.10 性能調(diào)試:分析并優(yōu)化 Go 程序
?# 11.1 接口是什么
2.2 Go 環(huán)境變量
2.6 安裝目錄清單
2.5 在 Windows 上安裝 Go
11.11 Printf 和反射
1.2 語言的主要特性與發(fā)展的環(huán)境和影響因素
9.0 包(package)
7.4 切片重組(reslice)
13.2 運(yùn)行時(shí)異常和 panic
10.2 使用工廠方法創(chuàng)建結(jié)構(gòu)體實(shí)例
12.8 使用接口的實(shí)際例子:fmt.Fprintf
2.4 在 Mac OS X 上安裝 Go
3.8 Go 性能說明
7.2 切片
8.0 Map
3.1 Go 開發(fā)環(huán)境的基本要求
5.6 標(biāo)簽與 goto
6.10 使用閉包調(diào)試
9.5 自定義包和可見性
4.3 常量
?# 11.2 接口嵌套接口
6.5 內(nèi)置函數(shù)
前言
10.8 垃圾回收和 SetFinalizer
2.8 Go 解釋器
13.7 Go 中的單元測試和基準(zhǔn)測試
6.8 閉包
4.9 指針
13.1 錯(cuò)誤處理
10.1 結(jié)構(gòu)體定義
5.1 if-else 結(jié)構(gòu)
6.6 遞歸函數(shù)
9.9 通過 Git 打包和安裝
2.7 Go 運(yùn)行時(shí)(runtime)
10.7 類型的 String() 方法和格式化描述符
3.7 其它工具
9.6 為自定義包使用 godoc
11.12 接口與動(dòng)態(tài)類型
13.3 從 panic 中恢復(fù)(Recover)
10.3 使用自定義包中的結(jié)構(gòu)體
11.14 結(jié)構(gòu)體、集合和高階函數(shù)
3.6 生成代碼文檔
9.2 regexp 包
4.1 文件名、關(guān)鍵字與標(biāo)識(shí)符
?# 11.6 使用方法集與接口
7.0 數(shù)組與切片
7.1 聲明和初始化
12.11 用 Gob 傳輸數(shù)據(jù)
5.5 Break 與 continue
1.1 起源與發(fā)展
?# 11 接口(Interfaces)與反射(reflection)
6.9 應(yīng)用閉包:將函數(shù)作為返回值
4.2 Go 程序的基本結(jié)構(gòu)和要素
8.6 將 map 的鍵值對調(diào)
6.11 計(jì)算函數(shù)執(zhí)行時(shí)間
5.0 控制結(jié)構(gòu)
10.5 匿名字段和內(nèi)嵌結(jié)構(gòu)體
4.6 字符串
3.0 編輯器、集成開發(fā)環(huán)境與其它工具
13.8 測試的具體例子
7.6 字符串、數(shù)組和切片的應(yīng)用
8.4 map 類型的切片
3.9 與其它語言進(jìn)行交互
7.3 For-range 結(jié)構(gòu)
9.7 使用 go install 安裝自定義包
6.0 函數(shù)
9.8 自定義包的目錄結(jié)構(gòu)、go install 和 go test
6.3 傳遞變長參數(shù)
13.9 用(測試數(shù)據(jù))表驅(qū)動(dòng)測試
11.9 空接口
8.1 聲明、初始化和 make
6.2 函數(shù)參數(shù)與返回值
9.11 在 Go 程序中使用外部庫
3.3 調(diào)試器
4.5 基本類型和運(yùn)算符
?# 11.8 第二個(gè)例子:讀和寫
12.5 用 buffer 讀取文件
總結(jié):Go 中的面向?qū)ο?/span>
11.10 反射包
12.7 用 defer 關(guān)閉文件
9.4 精密計(jì)算和 big 包
4.4 變量
6.1 介紹
13.4 自定義包中的錯(cuò)誤處理和 panicking
12.4 從命令行讀取參數(shù)
9.10 Go 的外部包和項(xiàng)目
8.3 for-range 的配套用法
3.5 格式化代碼
10.4 帶標(biāo)簽的結(jié)構(gòu)體
7.5 切片的復(fù)制與追加
?# 11.3 類型斷言:如何檢測和轉(zhuǎn)換接口變量的類型
5.4 for 結(jié)構(gòu)
4.8 時(shí)間和日期
2.3 在 Linux 上安裝 Go
12 讀寫數(shù)據(jù)
6.12 通過內(nèi)存緩存來提升性能
9.1 標(biāo)準(zhǔn)庫概述
12.6 用切片讀寫文件
10 結(jié)構(gòu)(struct)與方法(method)
8.5 map 的排序
12.9 JSON 數(shù)據(jù)格式
13.5 一種用閉包處理錯(cuò)誤的模式
3.2 編輯器和集成開發(fā)環(huán)境
12.12 Go 中的密碼學(xué)
5.2 測試多返回值函數(shù)的錯(cuò)誤
6.7 將函數(shù)作為參數(shù)
8.2 測試鍵值對是否存在及刪除元素
3.4 構(gòu)建并運(yùn)行 Go 程序
2.1 平臺(tái)與架構(gòu)
5.3 switch 結(jié)構(gòu)

10.6 方法

10.6.1 方法是什么

在 Go 語言中,結(jié)構(gòu)體就像是類的一種簡化形式,那么面向?qū)ο蟪绦騿T可能會(huì)問:類的方法在哪里呢?在 Go 中有一個(gè)概念,它和方法有著同樣的名字,并且大體上意思相同:Go 方法是作用在接收者(receiver)上的一個(gè)函數(shù),接收者是某種類型的變量。因此方法是一種特殊類型的函數(shù)。

接收者類型可以是(幾乎)任何類型,不僅僅是結(jié)構(gòu)體類型:任何類型都可以有方法,甚至可以是函數(shù)類型,可以是 int、bool、string 或數(shù)組的別名類型。但是接收者不能是一個(gè)接口類型(參考 第 11 章),因?yàn)榻涌谑且粋€(gè)抽象定義,但是方法卻是具體實(shí)現(xiàn);如果這樣做會(huì)引發(fā)一個(gè)編譯錯(cuò)誤:invalid receiver type…。

最后接收者不能是一個(gè)指針類型,但是它可以是任何其他允許類型的指針。

一個(gè)類型加上它的方法等價(jià)于面向?qū)ο笾械囊粋€(gè)類。一個(gè)重要的區(qū)別是:在 Go 中,類型的代碼和綁定在它上面的方法的代碼可以不放置在一起,它們可以存在在不同的源文件,唯一的要求是:它們必須是同一個(gè)包的。

類型 T(或 *T)上的所有方法的集合叫做類型 T(或 *T)的方法集。

因?yàn)榉椒ㄊ呛瘮?shù),所以同樣的,不允許方法重載,即對于一個(gè)類型只能有一個(gè)給定名稱的方法。但是如果基于接收者類型,是有重載的:具有同樣名字的方法可以在 2 個(gè)或多個(gè)不同的接收者類型上存在,比如在同一個(gè)包里這么做是允許的:

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)鍵字之后的括號(hào)中指定 receiver。

如果 recv 是 receiver 的實(shí)例,Method1 是它的方法名,那么方法調(diào)用遵循傳統(tǒng)的 object.name 選擇器符號(hào):recv.Method1()。

如果 recv 是一個(gè)指針,Go 會(huì)自動(dòng)解引用。

如果方法不需要使用 recv 的值,可以用 _ 替換它,比如:

func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }

recv 就像是面向?qū)ο笳Z言中的 thisself,但是 Go 中并沒有這兩個(gè)關(guān)鍵字。隨個(gè)人喜好,你可以使用 thisself 作為 receiver 的名字。下面是一個(gè)結(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,它有一個(gè) salary 字段,給這個(gè)結(jié)構(gòu)體定義一個(gè)方法 giveRaise 來按照指定的百分比增加薪水。

練習(xí) 10.7 iteration_list.go

下面這段代碼有什么錯(cuò)?

package main

import "container/list"

func (p *list.List) Iter() {
    // ...
}

func main() {
    lst := new(list.List)
    for _= range lst.Iter() {
    }
}

類型和作用在它上面定義的方法必須在同一個(gè)包里定義,這就是為什么不能在 int、float 或類似這些的類型上定義方法。試圖在 int 類型上定義方法會(huì)得到一個(gè)編譯錯(cuò)誤:

cannot define new methods on non-local type int

比如想在 time.Time 上定義如下方法:

func (t time.Time) first3Chars() string {
    return time.LocalTime().String()[0:3]
}

類型在其他的,或是非本地的包里定義,在它上面定義方法都會(huì)得到和上面同樣的錯(cuò)誤。

但是有一個(gè)間接的方式:可以先定義該類型(比如:int 或 float)的別名類型,然后再為別名類型定義方法?;蛘呦裣旅孢@樣將它作為匿名類型嵌入在一個(gè)新的結(jié)構(gòu)體中。當(dāng)然方法只在這個(gè)別名類型上有效。

示例 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
*/

10.6.2 函數(shù)和方法的區(qū)別

函數(shù)將變量作為參數(shù):Function1(recv)

方法在變量上被調(diào)用:recv.Method1()

在接收者是指針時(shí),方法可以改變接收者的值(或狀態(tài)),這點(diǎn)函數(shù)也可以做到(當(dāng)參數(shù)作為指針傳遞,即通過引用調(diào)用時(shí),函數(shù)也可以改變參數(shù)的狀態(tài))。

不要忘記 Method1 后邊的括號(hào) (),否則會(huì)引發(fā)編譯器錯(cuò)誤:method recv.Method1 is not an expression, must be called

接收者必須有一個(gè)顯式的名字,這個(gè)名字必須在方法中被使用。

receiver_type 叫做 (接收者)基本類型,這個(gè)類型必須在和方法同樣的包中被聲明。

在 Go 中,(接收者)類型關(guān)聯(lián)的方法不寫在類型結(jié)構(gòu)里面,就像類那樣;耦合更加寬松;類型和方法之間的關(guān)聯(lián)由接收者來建立。

方法沒有和數(shù)據(jù)定義(結(jié)構(gòu)體)混在一起:它們是正交的類型;表示(數(shù)據(jù))和行為(方法)是獨(dú)立的。

10.6.3 指針或值作為接收者

鑒于性能的原因,recv 最常見的是一個(gè)指向 receiver_type 的指針(因?yàn)槲覀儾幌胍粋€(gè)實(shí)例的拷貝,如果按值調(diào)用的話就會(huì)是這樣),特別是在 receiver 類型是結(jié)構(gòu)體時(shí),就更是如此了。

如果想要方法改變接收者的數(shù)據(jù),就在接收者的指針類型上定義該方法。否則,就在普通的值類型上定義方法。

下面的例子 pointer_value.go 作了說明:change()接受一個(gè)指向 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的值:將會(huì)看到它可以正常編譯,但是開始的 b 沒有被改變。

我們知道方法將指針作為接收者不是必須的,如下面的例子,我們只是需要 Point3 的值來做計(jì)算:

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)昂貴,因?yàn)?Point3 是作為值傳遞給方法的,因此傳遞的是它的拷貝,這在 Go 中是合法的。也可以在指向這個(gè)類型的指針上調(diào)用此方法(會(huì)自動(dòng)解引用)。

假設(shè) p3 定義為一個(gè)指針:p3 := &Point{ 3, 4, 5}。

可以使用 p3.Abs() 來替代 (*p3).Abs()

像例子 10.10(method1.go)中接收者類型是 *TwoInts 的方法 AddThem(),它能在類型 TwoInts 的值上被調(diào)用,這是自動(dòng)間接發(fā)生的。

因此 two2.AddThem 可以替代 (&two2).AddThem()。

在值和指針上調(diào)用方法:

可以有連接到類型的方法,也可以有連接到類型指針的方法。

但是這沒關(guān)系:對于類型 T,如果在 *T 上存在方法 Meth(),并且 t 是這個(gè)類型的變量,那么 t.Meth() 會(huì)被自動(dòng)轉(zhuǎn)換為 (&t).Meth()。

指針方法和值方法都可以在指針或非指針上被調(diào)用,如下面程序所示,類型 List 在值上有一個(gè)方法 Len(),在指針上有一個(gè)方法 Append(),但是可以看到兩個(gè)方法都可以在兩種類型的變量上被調(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)
}

10.6.4 方法和未導(dǎo)出字段

考慮 person2.go 中的 person 包:類型 Person 被明確的導(dǎo)出了,但是它的字段沒有被導(dǎo)出。例如在 use_person2.gop.firstName 就是錯(cuò)誤的。該如何在另一個(gè)程序中修改或者只是讀取一個(gè) Person 的名字呢?

這可以通過面向?qū)ο笳Z言一個(gè)眾所周知的技術(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 個(gè)或 2 個(gè)以上的不同線程在同一時(shí)間去改變。如果在程序發(fā)生這種情況,為了安全并發(fā)訪問,可以使用包 sync(參考第 9.3 節(jié))中的方法。在第 14.17 節(jié)中我們會(huì)通過 goroutines 和 channels 探索另一種方式。

10.6.5 內(nèi)嵌類型的方法和繼承

當(dāng)一個(gè)匿名類型被內(nèi)嵌在結(jié)構(gòu)體中時(shí),匿名類型的可見方法也同樣被內(nèi)嵌,這在效果上等同于外層類型 繼承 了這些方法:將父類型放在子類型中來實(shí)現(xiàn)亞型。這個(gè)機(jī)制提供了一種簡單的方式來模擬經(jīng)典面向?qū)ο笳Z言中的子類和繼承相關(guān)的效果,也類似 Ruby 中的混入(mixin)。

下面是一個(gè)示例(可以在練習(xí) 10.8 中進(jìn)一步學(xué)習(xí)):假定有一個(gè) Engine 接口類型,一個(gè) Car 結(jié)構(gòu)體類型,它包含一個(gè) 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)體上的方法可以直接在外層類型的實(shí)例上調(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)嵌將一個(gè)已存在類型的字段和方法注入到了另一個(gè)類型里:匿名字段上的方法“晉升”成為了外層類型的方法。當(dāng)然類型可以有只作用于本身實(shí)例而不作用于內(nèi)嵌“父”類型上的方法,

可以覆寫方法(像字段一樣):和內(nèi)嵌類型方法具有同樣名字的外層類型的方法會(huì)覆寫內(nèi)嵌類型對應(yīng)的方法。

在示例 10.18 method4.go 中添加:

func (n *NamedPoint) Abs() float64 {
    return n.Point.Abs() * 100.
}

現(xiàn)在 fmt.Println(n.Abs()) 會(huì)打印 500

因?yàn)橐粋€(gè)結(jié)構(gòu)體可以嵌入多個(gè)匿名類型,所以實(shí)際上我們可以有一個(gè)簡單版本的多重繼承,就像:type Child struct { Father; Mother}。在第 10.6.7 節(jié)中會(huì)進(jìn)一步討論這個(gè)問題。

結(jié)構(gòu)體內(nèi)嵌和自己在同一個(gè)包中的結(jié)構(gòu)體時(shí),可以彼此訪問對方所有的字段和方法。

練習(xí) 10.8 inheritance_car.go

創(chuàng)建一個(gè)上面 CarEngine 可運(yùn)行的例子,并且給 Car 類型一個(gè) wheelCount 字段和一個(gè) numberOfWheels() 方法。

創(chuàng)建一個(gè) Mercedes 類型,它內(nèi)嵌 Car,并新建 Mercedes 的一個(gè)實(shí)例,然后調(diào)用它的方法。

然后僅在 Mercedes 類型上創(chuàng)建方法 sayHiToMerkel() 并調(diào)用它。

10.6.6 如何在類型中嵌入功能

主要有兩種方法來實(shí)現(xiàn)在類型中嵌入功能:

A:聚合(或組合):包含一個(gè)所需功能類型的具名字段。

B:內(nèi)嵌:內(nèi)嵌(匿名地)所需功能類型,像前一節(jié) 10.6.5 所演示的那樣。

為了使這些概念具體化,假設(shè)有一個(gè) Customer 類型,我們想讓它通過 Log 類型來包含日志功能,Log 類型只是簡單地包含一個(gè)累積的消息(當(dāng)然它可以是復(fù)雜的)。如果想讓特定類型都具備日志功能,你可以實(shí)現(xiàn)一個(gè)這樣的 Log 類型,然后將它作為特定類型的一個(gè)字段,并提供 Log(),它返回這個(gè)日志的引用。

方式 A 可以通過如下方法實(shí)現(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 可能會(huì)像這樣:

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 方法,它使用 LogAdd 方法,Customer 有自己的 String 方法,并且在它里面調(diào)用了 LogString 方法。

如果內(nèi)嵌類型嵌入了其他類型,也是可以的,那些類型的方法可以直接在外層類型中使用。

因此一個(gè)好的策略是創(chuàng)建一些小的、可復(fù)用的類型作為一個(gè)工具箱,用于組成域類型。

10.6.7 多重繼承

多重繼承指的是類型獲得多個(gè)父類型行為的能力,它在傳統(tǒng)的面向?qū)ο笳Z言中通常是不被實(shí)現(xiàn)的(C++ 和 Python 例外)。因?yàn)樵陬惱^承層次中,多重繼承會(huì)給編譯器引入額外的復(fù)雜度。但是在 Go 語言中,通過在類型中嵌入所有必要的父類型,可以很簡單的實(shí)現(xiàn)多重繼承。

作為一個(gè)例子,假設(shè)有一個(gè)類型 CameraPhone,通過它可以 Call(),也可以 TakeAPicture(),但是第一個(gè)方法屬于類型 Phone,第二個(gè)方法屬于類型 Camera。

只要嵌入這兩個(gè)類型就可以解決這個(gè)問題,如下所示:

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í)):使用方法來實(shí)現(xiàn) Abs()Scale()函數(shù),Point 作為方法的接收者類型。也為 Point3Polar 實(shí)現(xiàn) Abs() 方法。完成了 point.go 中同樣的事情,只是這次通過方法。

練習(xí) 10.10 inherit_methods.go:

定義一個(gè)結(jié)構(gòu)體類型 Base,它包含一個(gè)字段 id,方法 Id() 返回 id,方法 SetId() 修改 id。結(jié)構(gòu)體類型 Person 包含 Base,及 FirstNameLastName 字段。結(jié)構(gòu)體類型 Employee 包含一個(gè) Personsalary 字段。

創(chuàng)建一個(gè) employee 實(shí)例,然后顯示它的 id。

練習(xí) 10.11 magic.go:

首先預(yù)測一下下面程序的結(jié)果,然后動(dòng)手實(shí)驗(yàn)下:

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()
}

10.6.8 通用方法和方法命名

在編程中一些基本操作會(huì)一遍又一遍的出現(xiàn),比如打開(Open)、關(guān)閉(Close)、讀(Read)、寫(Write)、排序(Sort)等等,并且它們都有一個(gè)大致的意思:打開(Open)可以作用于一個(gè)文件、一個(gè)網(wǎng)絡(luò)連接、一個(gè)數(shù)據(jù)庫連接等等。具體的實(shí)現(xiàn)可能千差萬別,但是基本的概念是一致的。在 Go 語言中,通過使用接口(參考 第 11 章),標(biāo)準(zhǔn)庫廣泛的應(yīng)用了這些規(guī)則,在標(biāo)準(zhǔn)庫中這些通用方法都有一致的名字,比如 Open()Read()、Write()等。想寫規(guī)范的 Go 程序,就應(yīng)該遵守這些約定,給方法合適的名字和簽名,就像那些通用方法那樣。這樣做會(huì)使 Go 開發(fā)的軟件更加具有一致性和可讀性。比如:如果需要一個(gè) convert-to-string 方法,應(yīng)該命名為 String(),而不是 ToString()(參考第 10.7 節(jié))。

10.6.9 和其他面向?qū)ο笳Z言比較 Go 的類型和方法

在如 C++、Java、C# 和 Ruby 這樣的面向?qū)ο笳Z言中,方法在類的上下文中被定義和繼承:在一個(gè)對象上調(diào)用方法時(shí),運(yùn)行時(shí)會(huì)檢測類以及它的超類中是否有此方法的定義,如果沒有會(huì)導(dǎo)致異常發(fā)生。

在 Go 語言中,這樣的繼承層次是完全沒必要的:如果方法在此類型定義了,就可以調(diào)用它,和其他類型上是否存在這個(gè)方法沒有關(guān)系。在這個(gè)意義上,Go 具有更大的靈活性。

下面的模式就很好的說明了這個(gè)問題:

Go 不需要一個(gè)顯式的類定義,如同 Java、C++、C# 等那樣,相反地,“類”是通過提供一組作用于一個(gè)共同類型的方法集來隱式定義的。類型可以是結(jié)構(gòu)體或者任何用戶自定義類型。

比如:我們想定義自己的 Integer 類型,并添加一些類似轉(zhuǎn)換成字符串的方法,在 Go 中可以如下定義:

type Integer int
func (i *Integer) String() string {
    return strconv.Itoa(int(*i))
}

在 Java 或 C# 中,這個(gè)方法需要和類 Integer 的定義放在一起,在 Ruby 中可以直接在基本類型 int 上定義這個(gè)方法。

總結(jié)

在 Go 中,類型就是類(數(shù)據(jù)和關(guān)聯(lián)的方法)。Go 不知道類似面向?qū)ο笳Z言的類繼承的概念。繼承有兩個(gè)好處:代碼復(fù)用和多態(tài)。

在 Go 中,代碼復(fù)用通過組合和委托實(shí)現(xiàn),多態(tài)通過接口的使用來實(shí)現(xiàn):有時(shí)這也叫 組件編程(Component Programming)。

許多開發(fā)者說相比于類繼承,Go 的接口提供了更強(qiáng)大、卻更簡單的多態(tài)行為。

備注

如果真的需要更多面向?qū)ο蟮哪芰?,看一?goop 包(Go Object-Oriented Programming),它由 Scott Pakin 編寫: 它給 Go 提供了 JavaScript 風(fēng)格的對象(基于原型的對象),并且支持多重繼承和類型獨(dú)立分派,通過它可以實(shí)現(xiàn)你喜歡的其他編程語言里的一些結(jié)構(gòu)。

問題 10.1

我們在某個(gè)類型的變量上使用點(diǎn)號(hào)調(diào)用一個(gè)方法:variable.method(),在使用 Go 以前,在哪兒碰到過面向?qū)ο蟮狞c(diǎn)號(hào)?

問題 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)中同樣的問題。

鏈接