閱讀579 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Go語言與數據庫開發:01-06

Go語言包含了對OOP語言的支持,接下來我們來看看Go語言中的方法。

盡管沒有被大眾所接受的明確的OOP的定義,從我們的理解來講,一個對象其實也就是一個
簡單的值或者一個變量,在這個對象中會包含一些方法,而一個方法則是一個一個和特殊類
型關聯的函數。一個麵向對象的程序會用方法來表達其屬性和對應的操作,這樣使用這個對
象的用戶就不需要直接去操作對象,而是借助方法來做這些事情。

在函數聲明時,在其名字之前放上一個變量,即是一個方法。
這個附加的參數會將該函數附加到這種類型上,即相當於為這種類型定義了一個獨占的方法。

package geometry
import "math"
type Point struct{ X, Y float64 }
// traditional function
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}

上麵的代碼裏那個附加的參數p,叫做方法的接收器(receiver),早期的麵向對象語言留下的遺
產將調用一個方法稱為“向一個對象發送消息”。

在Go語言中,我們並不會像其它語言那樣用this或者self作為接收器;我們可以任意的選擇接
收器的名字。由於接收器的名字經常會被使用到,所以保持其在方法間傳遞時的一致性和簡
短性是不錯的主意。這裏的建議是可以使用其類型的第一個字母,比如這裏使用了Point的首
字母p。

在方法調用過程中,接收器參數一般會在方法名之前出現。這和方法聲明是一樣的,都是接
收器參數在方法名字之前。

p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", function call
fmt.Println(p.Distance(q)) // "5", method call

可以看到,上麵的兩個函數調用都是Distance,但是卻沒有發生衝突。第一個Distance的調用
實際上用的是包級別的函數geometry.Distance,而第二個則是使用剛剛聲明的Point,調用的
是Point類下聲明的Point.Distance方法。

這種p.Distance的表達式叫做選擇器,因為他會選擇合適的對應p這個對象的Distance方法來
執行。選擇器也會被用來選擇一個struct類型的字段,比如p.X。由於方法和字段都是在同一
命名空間,所以如果我們在這裏聲明一個X方法的話,編譯器會報錯,因為在調用p.X時會有
歧義。

因為每種類型都有其方法的命名空間,我們在用Distance這個名字的時候,不同的Distance調
用指向了不同類型裏的Distance方法。

在能夠給任意類型定義方法這一點上,Go和很多其它的麵向對象的語言不太一樣。因此
在Go語言裏,我們為一些簡單的數值、字符串、slice、map來定義一些附加行為很方便。方
法可以被聲明到任意類型,隻要不是一個指針或者一個interface。


基於指針對象的方法

當調用一個函數時,會對其每一個參數值進行拷貝,如果一個函數需要更新一個變量,或者
函數的其中一個參數實在太大我們希望能夠避免進行這種默認的拷貝,這種情況下我們就需
要用到指針了。

對應到我們這裏用來更新接收器的對象的方法,當這個接受者變量本身比較
大時,我們就可以用其指針而不是對象來聲明方法,如下:

func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}

這個方法的名字是 (*Point).ScaleBy 。這裏的括號是必須的;沒有括號的話這個表達式可能
會被理解為 *(Point.ScaleBy)。

實際上注意兩點:
1. 不管你的method的receiver是指針類型還是非指針類型,都是可以通過指針/非指針類型
進行調用的,編譯器會幫你做類型轉換。
2. 在聲明一個method的receiver該是指針還是非指針類型時,你需要考慮兩方麵的內部,第
一方麵是這個對象本身是不是特別大,如果聲明為非指針變量時,調用會產生一次拷
貝;第二方麵是如果你用指針類型作為receiver,那麼你一定要注意,這種指針類型指向
的始終是一塊內存地址,就算你對其進行了拷貝。熟悉C或者C艸的人這裏應該很快能明白。

Nil也是一個合法的接收器類型
就像一些函數允許nil指針作為參數一樣,方法理論上也可以用nil指針作為其接收器,尤其當
nil對於對象來說是合法的零值時,比如map或者slice。


方法值和方法表達式

我們經常選擇一個方法,並且在同一個表達式裏執行,比如常見的p.Distance()形式,實際上
將其分成兩步來執行也是可能的。p.Distance叫作“選擇器”,選擇器會返回一個方法"值"->一
個將方法(Point.Distance)綁定到特定接收器變量的函數。這個函數可以不通過指定其接收器
即可被調用;即調用時不需要指定接收器(譯注:因為已經在前文中指定過了),隻要傳入函數
的參數即可:

p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // "5"
var origin Point // {0, 0}
fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5)
scaleP := p.ScaleBy // method value
scaleP(2) // p becomes (2, 4)
scaleP(3) // then (6, 12)
scaleP(10) // then (60, 120)


封裝

一個對象的變量或者方法如果對調用方是不可見的話,一般就被定義為“封裝”。封裝有時候也
被叫做信息隱藏,同時也是麵向對象編程最關鍵的一個方麵。

Go語言隻有一種控製可見性的手段:大寫首字母的標識符會從定義它們的包中被導出,小寫
字母的則不會。這種限製包內成員的方式同樣適用於struct或者一個類型的方法。因而如果我
們想要封裝一個對象,我們必須將其定義為一個struct。

例如:
type IntSet struct {
words []uint64
}

這種基於名字的手段使得在語言中最小的封裝單元是package,而不是像其它語言一樣的類
型。一個struct類型的字段對同一個包的所有代碼都有可見性,無論你的代碼是寫在一個函數
還是一個方法裏。

封裝提供了三方麵的優點。首先,因為調用方不能直接修改對象的變量值,其隻需要關注少
量的語句並且隻要弄懂少量變量的可能的值即可。
第二,隱藏實現的細節,可以防止調用方依賴那些可能變化的具體實現,這樣使設計包的程
序員在不破壞對外的api情況下能得到更大的自由。
把bytes.Buffer這個類型作為例子來考慮。這個類型在做短字符串疊加的時候很常用,所以在
設計的時候可以做一些預先的優化,比如提前預留一部分空間,來避免反複的內存分配。又
因為Buffer是一個struct類型,這些額外的空間可以用附加的字節數組來保存,且放在一個小
寫字母開頭的字段中。這樣在外部的調用方隻能看到性能的提升,但並不會得到這個附加變量。


接口

接口類型是對其它類型行為的抽象和概括;因為接口類型不會和特定的實現細節綁定在一
起,通過這種抽象的方式我們可以讓我們的函數更加靈活和更具有適應能力。

很多麵向對象的語言都有相似的接口概念,但Go語言中接口類型的獨特之處在於它是滿足隱
式實現的。也就是說,我們沒有必要對於給定的具體類型定義所有滿足的接口類型;簡單地
擁有一些必需的方法就足夠了。

這種設計可以讓你創建一個新的接口類型滿足已經存在的具
體類型卻不會去改變這些類型的定義;當我們使用的類型來自於不受我們控製的包時這種設
計尤其有用。

最後更新:2017-09-27 09:33:32

  上一篇:go  阿裏雲發布異構計算產品家族,你可以在上麵模擬核爆炸
  下一篇:go  2017杭州·雲棲大會---大數據workshop:《雲數據·大計算:海量日誌數據分析與應用》之《數據采集:日誌數據上傳》篇