溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

gopl 方法和接口

發(fā)布時(shí)間:2020-07-18 20:16:54 來(lái)源:網(wǎng)絡(luò) 閱讀:334 作者:騎士救兵 欄目:編程語(yǔ)言

方法聲明

寫(xiě)一個(gè)簡(jiǎn)單的方法:

type Point struct{X, Y float64}

// 普通的函數(shù)
func Distance(p, q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// 同樣的作用,用方法實(shí)現(xiàn)
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

接收者:附加的參數(shù) p 稱(chēng)為方法的接收者。
調(diào)用方法的時(shí)候,接收者在方法名的前面。這樣就和聲明保持一致:

p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // 函數(shù)調(diào)用
fmt.Println(p.Distance(q))  // 方法調(diào)用

選擇子:表達(dá)是 p.Distance 稱(chēng)作選擇子(selector),因?yàn)樗鼮榻邮照?p 選擇合適的 Distance 方法。

指針接收者的方法

對(duì)于函數(shù),它會(huì)復(fù)制每一只實(shí)參變量。如果函數(shù)需要更新一個(gè)變量,或者是因?yàn)閷?shí)參太大而需要避免復(fù)制整個(gè)實(shí)參,就需要使用指針來(lái)傳遞變量的地址。
對(duì)于方法的接受者,也可以將方法綁定到指針類(lèi)型。習(xí)慣上遵循如果一個(gè)類(lèi)型的任何一個(gè)方法使用指針接收者,那么所有該類(lèi)型的方法都應(yīng)該使用指針接收者,即使有些方法不一定需要。
另外,為了防止混淆,不允許本身是指針的類(lèi)型進(jìn)行方法聲明,會(huì)有編譯錯(cuò)誤:

type p *int
func (p) f() { /*...*/ } // 編譯錯(cuò)誤:非法的接收者類(lèi)型

方法變量與表達(dá)式

方法變量(method value)

通常是在相同的表達(dá)式里使用和調(diào)用方法,但是把兩個(gè)操作分開(kāi)也是可以的。選擇子 p.Distance 可以賦予一個(gè)方法變量,它是一個(gè)函數(shù),把方法(Point.Distance)綁定到一個(gè)接收者 p 上。函數(shù)只需要提供實(shí)參而不需要提供接收者就能夠調(diào)用:

p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // 方法變量
fmt.Println(distanceFromP(q))

這里 p.Distance 是選擇子,把它賦值給變量 distanceFromP,這個(gè)變量就是方法變量,并且這個(gè)變量是一個(gè)函數(shù)。
如果包內(nèi)的 API 調(diào)用一個(gè)函數(shù)值,并且使用者期望這個(gè)函數(shù)的行為是調(diào)用一個(gè)特定接收者的方法,方法變量就非常有用。使用方法變量還可以是代碼更加簡(jiǎn)潔:

type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }

r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() }) // 如果沒(méi)有方法變量,那么要把執(zhí)行一個(gè)方法包在一個(gè)函數(shù)里,等到函數(shù)被調(diào)用后執(zhí)行
time.AfterFunc(10 * time.Second, r.Launch)  // 使用方法變量,這里 r.Launch 就是一個(gè)函數(shù),只是沒(méi)有賦值給某個(gè)變量,沒(méi)有函數(shù)名

函數(shù) time.AfterFunc 的作用是在指定的延遲后調(diào)用一個(gè)函數(shù)。上面說(shuō)了,方法變量也是函數(shù)。

方法表達(dá)式(method expression)

調(diào)用方法的時(shí)候必須提供接收者,并且按照選擇子的語(yǔ)法進(jìn)行調(diào)用。
方法表達(dá)式,寫(xiě)成 T.f 或者 (*T.f)。
其中 T 是類(lèi)型,是一種函數(shù)變量,把原來(lái)方法的接收者替換成函數(shù)的第一個(gè)形參,因此它可以像平常的函數(shù)一樣調(diào)用:

p := Point{1, 2}
q := Point{4, 6}
distance :=  Point.Distance  // 方法表達(dá)式
fmt.Println(distance(p, q))
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"

如果需要一個(gè)值來(lái)代表多個(gè)方法中的一個(gè),而方法都屬于同一個(gè)類(lèi)型,方法表達(dá)式可以實(shí)現(xiàn)讓這個(gè)值所對(duì)應(yīng)的方法來(lái)處理不同的接收者。就是可以把一個(gè)方法變成一個(gè)函數(shù),函數(shù)的變量會(huì)增加一個(gè),第一個(gè)變量就是原來(lái)方法中的接收者。其實(shí)各個(gè)參數(shù)的順序還是一樣的,原本第一個(gè)參數(shù)在 func 前,現(xiàn)在移動(dòng)到了 func 后面。 p.Distance(q) 變成了 distance(p, q)。

接口類(lèi)型

io包定義了很多有用的接口:

  • io.Writer : 抽象了所有寫(xiě)入字節(jié)的類(lèi)型,下面會(huì)列舉
  • io.Reader : 抽象了所有可以讀取字節(jié)的類(lèi)型
  • io.Closer : 抽象了所有可以關(guān)閉的類(lèi)型,比如文件或者網(wǎng)絡(luò)連接

io.Writer 是一個(gè)廣泛使用的接口,它負(fù)責(zé)所有可以寫(xiě)入字節(jié)的抽象,包括但不限于下面列舉的這些:

  • 文件
  • 內(nèi)存緩沖區(qū)
  • 網(wǎng)絡(luò)連接
  • HTTP客戶(hù)端
  • 打包器(archiver)
  • 散列器(hasher)

接口值

接口值,就是一個(gè)接口類(lèi)型的值。分兩個(gè)部分:

  • 動(dòng)態(tài)類(lèi)型 : 該接口的具體類(lèi)型
  • 動(dòng)態(tài)值 : 該具體類(lèi)型的一個(gè)值
var w io.Writer  // 聲明接口,動(dòng)態(tài)類(lèi)型和動(dòng)態(tài)值都是nil
w = os.Stdout  // 有動(dòng)態(tài)類(lèi)型,也有動(dòng)態(tài)值
w = io.Writer(os.Stdout)  // 和上面這句等價(jià),把一個(gè)具體類(lèi)型顯式轉(zhuǎn)換為接口類(lèi)型
w = new(bytes.Buffer)  // 有動(dòng)態(tài)類(lèi)型,也有動(dòng)態(tài)值
w = nil  // 把動(dòng)態(tài)類(lèi)型和動(dòng)態(tài)值都設(shè)置為nil,恢復(fù)到聲明時(shí)的狀態(tài)

比較接口值

接口值可以用 == 和 != 來(lái)比較。動(dòng)態(tài)類(lèi)型一致,然后動(dòng)態(tài)值相等(使用動(dòng)態(tài)類(lèi)型的 == 來(lái)比較),那么接口值相等。接口值都是nil也是相等的。
可以作為map的key,也可以作為switch語(yǔ)句的操作數(shù),因?yàn)榭梢员容^。
動(dòng)態(tài)值可能是不可比較的類(lèi)型,比如切片。對(duì)這樣的接口進(jìn)行比較,就會(huì)Panic。把這樣的接口用作map的key或者switch語(yǔ)句的操作數(shù)時(shí)也同樣會(huì)Panic。所以,僅在能確認(rèn)接口值包含的動(dòng)態(tài)值可以比較時(shí),才比較接口值。
fmt 包的 %T 打印出來(lái)的就是動(dòng)態(tài)類(lèi)型。在內(nèi)部實(shí)現(xiàn)中,fmt 用反射來(lái)拿到接口動(dòng)態(tài)類(lèi)型的名字。

注意:含有空指針的非空接口

空的接口值(動(dòng)態(tài)類(lèi)型和動(dòng)態(tài)值都為空)和僅僅動(dòng)態(tài)值為nil的接口值是不一樣的。

const debug = true

func main() {
    var buf *bytes.Buffer
    if debug {
        buf = new(bytes.Buffer)
    }
    f(buf)
    if debug {
        // ...使用 buf...
    }
}

// 如果 out 不是 nil,那么會(huì)向其寫(xiě)入輸出的數(shù)據(jù)
func f(out io.Writer) {
    // ...其他代碼...
    if out != nil {
        out.Write([]byte("done\n"))
    }
}

這里,把一個(gè)類(lèi)型為 *bytes.Buffer 的空指針賦給了 out 參數(shù),此時(shí) out 的動(dòng)態(tài)值為空。但它的動(dòng)態(tài)類(lèi)型是 *bytes.Buffer。就是說(shuō) out 是一個(gè)包含空指針的非空接口,所以這里的檢查 out != nil 是 true,防御不了這種情況。
對(duì)于某些類(lèi)型,比如 *os.File,空接收值是合法的。但是對(duì)于這里的 *buyes.Buffer,要求接收者不能為空,于是運(yùn)行時(shí)會(huì)Panic。
這里的解決方案是,把 main 函數(shù)中的 buf 類(lèi)型修改為 io.Writer,從而避免在最開(kāi)始就把一個(gè)功能不完整的值賦給一個(gè)接口:

var buf io.Writer
if debug {
    buf = new(bytes.Buffer)
}
f(buf)

類(lèi)型斷言

類(lèi)型斷言是一個(gè)作用在接口值上的操作,代碼類(lèi)似于x(T),x是一個(gè)接口類(lèi)型的表達(dá)式,而T是一個(gè)類(lèi)型(稱(chēng)為斷言類(lèi)型)。類(lèi)型斷言會(huì)檢查操作數(shù)的動(dòng)態(tài)類(lèi)型是否滿(mǎn)足指定的斷言類(lèi)型。
這里有兩種可能:

  • 斷言類(lèi)型T是一個(gè)具體類(lèi)型
  • 斷言類(lèi)型T是一個(gè)接口類(lèi)型

具體類(lèi)型
如果斷言類(lèi)型T是一個(gè)具體類(lèi)型,斷言類(lèi)型會(huì)檢查x的動(dòng)態(tài)類(lèi)型是否就是T。如果檢查成功,返回x的動(dòng)態(tài)值,返回的類(lèi)型就是T。如果檢查失敗,那么操作崩潰。

接口類(lèi)型
如果斷言類(lèi)型T是一個(gè)接口類(lèi)型,斷言類(lèi)型會(huì)檢查x的動(dòng)態(tài)類(lèi)型是否滿(mǎn)足T。如果檢查成功,動(dòng)態(tài)值并沒(méi)有提取出來(lái),仍然是一個(gè)接口值,接口值的類(lèi)型和值部分也不會(huì)變,只是結(jié)果類(lèi)型為接口類(lèi)型T。就是說(shuō),這里類(lèi)型斷言就是一個(gè)接口值表達(dá)式,從一個(gè)接口類(lèi)型變?yōu)閾碛辛硗庖惶追椒ǖ慕涌陬?lèi)型,但保留了接口值中動(dòng)態(tài)類(lèi)型和動(dòng)態(tài)值部分。如果檢查失敗還是會(huì)崩潰。

類(lèi)型斷言可以返回兩個(gè)結(jié)果,此時(shí)操作不會(huì)因?yàn)闄z查失敗而崩潰。多出來(lái)的返回值是一個(gè)布爾型,用來(lái)指示斷言是否成功。按照慣例,一般變量名用ok。如果操作失敗,ok為false,而第一個(gè)返回值會(huì)是斷言類(lèi)型的零值。

類(lèi)型分支

接口有兩種不同的風(fēng)格。
第一種風(fēng)格下,典型的比如:io.Reader、io.Writer、fmt.Stringer、sort.Interface、http.Handler 和 error。接口上的各種方法突出了滿(mǎn)足這個(gè)接口的具體類(lèi)型之間的相似性,但隱藏了各個(gè)具體類(lèi)型的布局和各自特有的功能。這種風(fēng)格強(qiáng)調(diào)了方法,而不是具體類(lèi)型。
第二種風(fēng)格則充分利用了接口值能夠容納各種具體類(lèi)型的能力,它把接口作為這些類(lèi)型的聯(lián)合(union)來(lái)使用。類(lèi)型斷言用來(lái)在運(yùn)行時(shí)區(qū)分這些類(lèi)型并分別處理。這這種風(fēng)格中,強(qiáng)調(diào)的是滿(mǎn)足這個(gè)接口的具體類(lèi)型,而不是這個(gè)接口的方法(經(jīng)常是沒(méi)變方法的空接口),也不注重信息隱藏。這種風(fēng)格的接口使用方式稱(chēng)為可識(shí)別聯(lián)合(discriminated union)。
如果對(duì)面向?qū)ο笫煜?,這兩種風(fēng)格分別對(duì)應(yīng):

  • 子類(lèi)型多態(tài)(subtype polymorphism)
  • 特設(shè)多態(tài)(ad hoc polymorphism)

使用接口的一些建議

不要一開(kāi)始就定義接口,每個(gè)接口卻只是一個(gè)單獨(dú)的實(shí)現(xiàn)。這種接口是不必要的抽象,還會(huì)有運(yùn)行時(shí)的成本。僅在有兩個(gè)或多個(gè)具體類(lèi)型需要按統(tǒng)一的方式處理時(shí)才需要接口。
上面的建議也有特例,如果接口和類(lèi)型實(shí)現(xiàn)出于依賴(lài)的原因不能放在同一個(gè)包里邊,那么一個(gè)接口只有一個(gè)具體類(lèi)型實(shí)現(xiàn)也是可以的。在這種情況下,接口是一種解耦兩個(gè)包的好方式。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI