方法和接口
第四篇包含了方法和接口,可以用它們來定義對象和其行為;以及如何將所有內(nèi)容貫通起來。
方法
Go 沒有類。然而,可以在結(jié)構(gòu)體類型上定義方法。
方法是一個帶有特殊接收者參數(shù)的函數(shù)。方法接收者?出現(xiàn)在?func?關(guān)鍵字和方法名之間的參數(shù)中。
Ocean覺得GO語言中更真實(shí)的還原了方法的本質(zhì),同樣的功能,我們用普通函數(shù)也可以實(shí)現(xiàn)。
在非結(jié)構(gòu)體類型上也可以定義方法,下面的例子中給數(shù)字類型Myfloat定義了一個Abs方法。
方法接受者聲明的類型,必須在定義同一個package中。方法接收者的類型不能夸包存在。
指針接收者
可以在方法定義中使用指針接收者,就是說可以使用 *T的語法來指向某個類型T,注意T本身不能是指針類型。比如下面例子中,Scale方法使用*Vertex語法來定義。
使用指針接收者的方法,可以改變指針接收者指向的值。因?yàn)榉椒ㄍǔ薷乃麄兊慕邮照叩闹担灾羔樈邮照咄ǔ1戎到邮照吒R姟?/p>
使用值接收者時,Scale方法在原來Vertex類型的拷貝上進(jìn)行操作。例如Scale方法想要修改main方法中聲明的Vertex值,就必須要使用指針接收者。
如果例子中第16行,*Vertex的結(jié)果是50,如果是Vertex則結(jié)果是5
指針和函數(shù)
同樣把上面例子改寫成使用指針和函數(shù),就更好理解在方法中使用指針接收者。
方法和間接指針
對比上面的兩個程序,帶有指針參數(shù)的函數(shù),必須接收一個指針傳遞。
var?v?Vertex ScaleFunc(v)??//?Compile?error! ScaleFunc(&v)?//?OK
然而,帶有指針接收者的方法,當(dāng)方法被調(diào)用時,值傳遞和指針傳遞都會按指針傳遞處理
var?v?Vertex v.Scale(5)??//?OK p?:=?&v p.Scale(10)?//?OK
比如語句 v.Scale(5),即使v是一個值不是指針,帶有指針接收者的方法會被自動調(diào)用。其原理是,GO語言會自動的把v.Scale(5)?解釋成?(&v).Scale(5),?因?yàn)镾cale方法有一個指針接收者。
同樣的事情也可以反向進(jìn)行。接收一個值參數(shù)的函數(shù),必須接收一個值傳遞。
var?v?Vertex fmt.Println(AbsFunc(v))??//?OK fmt.Println(AbsFunc(&v))?//?Compile?error!
然而帶有值接收者的方法,當(dāng)被調(diào)用時,不管是按值傳遞還是指針傳遞,最終都會按值傳遞處理
var?v?Vertex fmt.Println(v.Abs())?//?OK p?:=?&v fmt.Println(p.Abs())?//?OK
上面的例子中,p.Abs()?被GO解釋成?(*p).Abs()。
選擇值接收者還是指針接收者
使用指針接收者有2個原因,第一,方法能夠修改其指向的值。第二,能夠避免每次調(diào)用方法時,都觸發(fā)目標(biāo)值的拷貝,在目標(biāo)值是大型結(jié)構(gòu)體時,對資源利用和效率的提高更明顯。
在圖中的例子中,Scale和Abs方法都是用了*Vertex,即使Abs并不需要修改目標(biāo)值。一般來說,值接收者和引用接收者,只會二者選其一,不會同時存在。
接口
接口類型是由一組方法定義的集合。接口類型的值可以是存放實(shí)現(xiàn)這些方法的任何值。
注意: 示例代碼的 18行存在一個錯誤。 由于?Abs?只定義在?*Vertex(指針類型)上, 所以?Vertex(值類型)不滿足?Abser。
接口是隱式實(shí)現(xiàn)的
一個類型通過實(shí)現(xiàn)接口的方法來實(shí)現(xiàn)一個接口,這句聽起來有點(diǎn)拗口,意思就是不需要明確的關(guān)鍵字 implements 這種來表明實(shí)現(xiàn)關(guān)系。
隱式的接口把接口的定義和接口的實(shí)現(xiàn)做了徹底分離,接口可以出現(xiàn)在任何包中,不需要任何準(zhǔn)備。
接口值的構(gòu)成
在接口表皮下面,一個接口類型的值可以被看成一個元組,包含了接口值本身和其對應(yīng)的具體類型:
(value,?type)
就是說接口 = 值+類型。調(diào)用接口上的一個方法時,實(shí)際上是調(diào)用了,接口中對應(yīng)類型中同名的方法。
接口值中含有Nil
如果接口值值中有具體類型,但是類型對象未被初始化為nil。調(diào)用接口方法時,方法實(shí)際arg參數(shù)值會是nil。有些語言中,方法調(diào)用作用在nil對象時,會觸發(fā)空指針異常。但GO中可以很優(yōu)雅處理nil的情況,直接進(jìn)行透傳。注意一個接口值中類型對象是nil時,這個接口值本身不是nil。
Nil接口
一個nil接口是指既不包含值也不包含類型的接口,在調(diào)用nil接口時,會觸發(fā)運(yùn)行時異常,因?yàn)樵诮涌跇?gòu)成的tuple元組中沒有具體的類型。
空接口
在一個接口中沒定義任何方法時,被稱為空接口:
interface{}
一個空接口可以包含任何類型的值,空接口是用來處理未知類型的。比如,fmt.Print接收任意多個空接口類型的參數(shù)。
類型斷言
類型斷言提供了一個機(jī)制找出接口底層對應(yīng)的實(shí)際類型
t?:=?i.(T)
這個語句在斷言接口i中實(shí)際包含了類型T,然后把底層類型T的值賦值給變量t。
如果斷言失敗,i中沒有包含T,這條語句會觸發(fā)panic。
為了測試接口是否包含指定的類型,類型斷言會返回2個值,底層類型實(shí)際對應(yīng)的值和一個bool值,來報告斷言是否成功。
t,?ok?:=?i.(T)
如果i中包含T,則t是底層類型的實(shí)際值,變量ok是真。
如果斷言失敗,ok變量是假,t是一個零值類型T,不會觸發(fā)panic,這個語法和對map操作類似。
類型Switch
類型switch是一個構(gòu)建,可以連續(xù)進(jìn)行多個類型斷言。
類型switch和普通的switch語句類似,但是case表達(dá)式中是類型,不是具體的值,是接口值對應(yīng)的類型和case的類型相比。
switch?v?:=?i.(type)?{ case?T: ????//?here?v?has?type?T case?S: ????//?here?v?has?type?S default: ????//?no?match;?here?v?has?the?same?type?as?i }
在類型switch的聲明中的語法,和類型斷言的語法 i.(T) 相似,但是具體的類型T被替換成了關(guān)鍵字 type.
這個switch語句判斷接口值i是否包含了類型T或者S。在每一個T或者S的case中,v的值就是i中的值。在默認(rèn)case中(沒有匹配),v的類型就是接口中tuple的值和類型。
Stringers
一個普遍存在的接口是?fmt?包中定義的?Stringer。
type?Stringer?interface?{ ????String()?string }
Stringer?是一個可以用字符串描述自己的類型。`fmt`包 (還有許多其他包)使用這個來進(jìn)行打印值。
讓?IPAddr?類型實(shí)現(xiàn)?fmt.Stringer?以便用點(diǎn)分格式輸出地址。
例如,IPAddr{1, 2, 3, 4}?應(yīng)當(dāng)輸出?"1.2.3.4"。
錯誤
Go 程序使用?error?值來表示錯誤狀態(tài)。
與?fmt.Stringer?類似,?error?類型是一個內(nèi)建接口:
type?error?interface?{ ????Error()?string }
(與?fmt.Stringer?類似,fmt?包在輸出時也會試圖匹配?error。)
通常函數(shù)會返回一個?error?值,調(diào)用的它的代碼應(yīng)當(dāng)判斷這個錯誤是否等于?nil, 來進(jìn)行錯誤處理。
i,?err?:=?strconv.Atoi("42") if?err?!=?nil?{ ????fmt.Printf("couldn't?convert?number:?%v\n",?err) ????return } fmt.Println("Converted?integer:",?i)
error?為 nil 時表示成功;非 nil 的?error?表示錯誤。
從先前的練習(xí)中復(fù)制?Sqrt?函數(shù),并修改使其返回?error?值。
由于不支持復(fù)數(shù),當(dāng)?Sqrt?接收到一個負(fù)數(shù)時,應(yīng)當(dāng)返回一個非 nil 的錯誤值。
創(chuàng)建一個新類型
type?ErrNegativeSqrt?float64
為其實(shí)現(xiàn)
func?(e?ErrNegativeSqrt)?Error()?string
使其成為一個?error, 該方法就可以讓?ErrNegativeSqrt(-2).Error()?返回 `"cannot Sqrt negative number: -2"`。
*注意:* 在?Error?方法內(nèi)調(diào)用?fmt.Sprint(e)?將會讓程序陷入死循環(huán)。可以通過先轉(zhuǎn)換?e?來避免這個問題:fmt.Sprint(float64(e))。請思考這是為什么呢?
修改?Sqrt?函數(shù),使其接受一個負(fù)數(shù)時,返回?ErrNegativeSqrt?值。
Readers
io?包指定了?io.Reader?接口, 它表示從數(shù)據(jù)流結(jié)尾讀取。
Go 標(biāo)準(zhǔn)庫包含了這個接口的許多實(shí)現(xiàn), 包括文件、網(wǎng)絡(luò)連接、壓縮、加密等等。
io.Reader?接口有一個?Read?方法:
func?(T)?Read(b?[]byte)?(n?int,?err?error)
Read?用數(shù)據(jù)填充指定的字節(jié) slice,并且返回填充的字節(jié)數(shù)和錯誤信息。 在遇到數(shù)據(jù)流結(jié)尾時,返回?io.EOF?錯誤。
例子代碼創(chuàng)建了一個?strings.Reader。 并且以每次 8 字節(jié)的速度讀取它的輸出。
實(shí)現(xiàn)一個?Reader?類型,它不斷生成 ASCII 字符?'A'?的流。
練習(xí):rot13Reader
一個常見模式是?io.Reader?包含另一個?io.Reader,然后通過某種形式修改數(shù)據(jù)流。
例如,gzip.NewReader?函數(shù)接受?io.Reader(壓縮的數(shù)據(jù)流)并且返回同樣實(shí)現(xiàn)了?io.Reader的?*gzip.Reader(解壓縮后的數(shù)據(jù)流)。
編寫一個實(shí)現(xiàn)了?io.Reader?的?rot13Reader, 并從一個?io.Reader?讀取, 利用?rot13?代換密碼對數(shù)據(jù)流進(jìn)行修改。
圖片
Package image?定義了?Image?接口:
package?image type?Image?interface?{ ????ColorModel()?color.Model ????Bounds()?Rectangle ????At(x,?y?int)?color.Color }
注意:Bounds?方法的?Rectangle?返回值實(shí)際上是一個?image.Rectangle, 其定義在?image?包中。
color.Color?和?color.Model?也是接口,但是通常因?yàn)橹苯邮褂妙A(yù)定義的實(shí)現(xiàn)?image.RGBA?和?image.RGBAModel?而被忽視了。這些接口和類型由image/color?包定義。
◆??◆??◆ ?◆??◆??
來源:
作者介紹:張洋銘,投資人中最懂動漫的程序員,負(fù)責(zé)PlugandPlay早期科技類項(xiàng)目投資,個人關(guān)注動漫智能助理。
微信公眾號:張洋銘Ocean(ocean_anidata)
BP請投遞至:ocean.zhang@plugandplaychina.com