官术网_书友最值得收藏!

1.4.3 接口

Go語言之父Rob Pike曾說過一句名言:“試圖禁止白癡行為的編程語言,本身會變得白癡。”(Languages that try to disallow idiocy become themselves idiotic.)一般靜態(tài)編程語言都有嚴(yán)格的類型系統(tǒng),這使編譯器可以深入檢查程序員有沒有做出什么出格的舉動。但是,過于嚴(yán)格的類型系統(tǒng)卻會使得編程太過煩瑣,讓程序員把時間都浪費在和編譯器的斗爭中。Go語言試圖讓程序員能在安全和靈活的編程之間取得一種平衡。它在提供嚴(yán)格的類型檢查的同時,通過接口類型實現(xiàn)了對鴨子型的類型的支持,使得安全動態(tài)的編程變得相對容?易。

Go的接口類型是對其他類型行為的抽象和概括,因為接口類型不會和特定的實現(xiàn)細(xì)節(jié)綁定在一起,通過這種抽象的方式,我們可以讓對象更加靈活和更具有適應(yīng)能力。很多面向?qū)ο蟮恼Z言都有相似的接口概念,但Go語言中接口類型的獨特之處在于它是滿足隱式實現(xiàn)的鴨子型。所謂鴨子型說的是:只要走起路來像鴨子、叫起來也像鴨子,那么就可以把它當(dāng)作鴨子。Go語言中的面向?qū)ο缶褪侨绱耍绻粋€對象只要看起來像是某種接口類型的實現(xiàn),那么它就可以作為該接口類型使用。這種設(shè)計使程序員可以創(chuàng)建一個新的接口類型滿足已經(jīng)存在的具體類型卻不用去破壞這些類型原有的定義。當(dāng)使用的類型來自不受我們控制的包時這種設(shè)計尤其靈活有用。Go語言的接口類型是延遲綁定,可以實現(xiàn)類似于虛函數(shù)的多態(tài)功?能。

接口在Go語言中無處不在,在“Hello, World”的例子中,fmt.Printf()函數(shù)的設(shè)計就是完全基于接口的,它的真正功能由fmt.Fprintf()函數(shù)完成。用于表示錯誤的error類型更是內(nèi)置的接口類型。在C語言中,printf只能將幾種有限的基礎(chǔ)數(shù)據(jù)類型打印到文件對象中。但是Go語言由于靈活的接口特性,fmt.Fprintf可以向任何自定義的輸出流對象打印,可以打印到文件或標(biāo)準(zhǔn)輸出,也可以打印到網(wǎng)絡(luò),甚至可以打印到一個壓縮文件;同時,打印的數(shù)據(jù)也不僅局限于語言內(nèi)置的基礎(chǔ)類型,任意隱式滿足fmt.Stringer接口的對象都可以打印,不滿足fmt.Stringer接口的依然可以通過反射的技術(shù)打印。fmt.Fprintf()函數(shù)的簽名如下:

func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)

其中io.Writer用于輸出的接口,error是內(nèi)置的錯誤接口,它們的定義如下:

type io.Writer interface {
    Write(p []byte) (n int, err error)
}
type error interface {
    Error() string
}

我們可以通過定制自己的輸出對象,將每個字符轉(zhuǎn)換為大寫字符后輸出:

type UpperWriter struct {
    io.Writer
}
func (p *UpperWriter) Write(data []byte) (n int, err error) {
    return p.Writer.Write(bytes.ToUpper(data))
}
func main() {
    fmt.Fprintln(&UpperWriter{os.Stdout}, "hello, world")
}

當(dāng)然,我們也可以定義自己的打印格式來實現(xiàn)將每個字符轉(zhuǎn)換為大寫字符后輸出的效果。對于每個要打印的對象,如果滿足了fmt.Stringer接口,則默認(rèn)使用對象的String()方法返回的結(jié)果打印:

type UpperString string
func (s UpperString) String() string {
    return strings.ToUpper(string(s))
}
type fmt.Stringer interface {
    String() string
}
func main() {
    fmt.Fprintln(os.Stdout, UpperString("hello, world"))
}

Go語言中,對于基礎(chǔ)類型(非接口類型)不支持隱式的轉(zhuǎn)換,我們無法將一個int類型的值直接賦值給int64類型的變量,也無法將int類型的值賦值給底層是int類型的新定義具名變量。Go語言對基礎(chǔ)類型的類型一致性要求可謂是非常嚴(yán)格,但是Go語言對于接口類型的轉(zhuǎn)換則非常靈活。對象和接口之間的轉(zhuǎn)換、接口和接口之間的轉(zhuǎn)換都可能是隱式的轉(zhuǎn)換。可以看下面的例子:

var (
    a io.ReadCloser = (*os.File)(f)     // 隱式轉(zhuǎn)換,*os.File滿足io.ReadCloser接口
    b io.Reader      = a                // 隱式轉(zhuǎn)換,io.ReadCloser滿足io.Reader接口
    c io.Closer      = a                // 隱式轉(zhuǎn)換,io.ReadCloser滿足io.Closer接口
    d io.Reader      = c.(io.Reader)    // 顯式轉(zhuǎn)換,io.Closer不滿足io.Reader接口
)

有時候?qū)ο蠛徒涌谥g太靈活了,需要人為地限制這種無意之間的適配。常見的做法是定義一個特殊方法來區(qū)分接口。例如,runtime包中的Error接口就定義了一個特有的RuntimeError()方法,用于避免其他類型無意中適配該接口:

type runtime.Error interface {
    error
    // RuntimeError is a no-op function but
    // serves to distinguish types that are run time
    // errors from ordinary errors: a type is a
    // run time error if it has a RuntimeError method.
    RuntimeError()
}

在Protobuf中,Message接口也采用了類似的方法,定義了一個特有的ProtoMessage()方法,用于避免其他類型無意中適配該接口:

type proto.Message interface {
    Reset()
    String() string
    ProtoMessage()
}

不過這種做法只是“君子協(xié)定”,如果有人故意偽造一個proto.Message接口也是很容易的。再嚴(yán)格一點的做法是給接口定義一個私有方法。只有滿足了這個私有方法的對象才可能滿足這個接口,而私有方法的名字是包含包的絕對路徑名的,因此只有在包內(nèi)部實現(xiàn)這個私有方法才能滿足這個接口。測試包中的testing.TB接口就是采用類似的技術(shù):

type testing.TB interface {
    Error(args ...interface{})
    Errorf(format string, args ...interface{})
    ...
    // A private method to prevent users implementing the
    // interface and so future additions to it will not
    // violate Go 1 compatibility.
    private()
}

不過這種通過私有方法禁止外部對象實現(xiàn)接口的做法也是有代價的:首先,這個接口只能在包內(nèi)部使用,外部包在正常情況下是無法直接創(chuàng)建滿足該接口的對象的;其次,這種防護(hù)措施也不是絕對的,惡意用戶依然可以繞過這種保護(hù)機(jī)?制。

1.4.2節(jié)中講到,通過在結(jié)構(gòu)體中嵌入匿名類型成員,可以繼承匿名類型的方法。其實這個被嵌入的匿名成員不一定是普通類型,也可以是接口類型。我們可以通過嵌入匿名的testing.TB接口來偽造私有方法,因為接口方法是延遲綁定,所以編譯時私有方法是否真的存在并不重?要。

package main
import (
    "fmt"
    "testing"
)
type TB struct {
    testing.TB
}
func (p *TB) Fatal(args ...interface{}) {
    fmt.Println("TB.Fatal disabled!")
}
func main() {
    var tb testing.TB = new(TB)
    tb.Fatal("Hello, playground")
}

我們在自己的TB結(jié)構(gòu)體類型中重新實現(xiàn)了Fatal()方法,然后通過將對象隱式轉(zhuǎn)換為testing.TB接口類型(因為內(nèi)嵌了匿名的testing.TB對象,所以是滿足testing.TB接口的),再通過testing.TB接口來調(diào)用自己的Fatal()方?法。

這種通過嵌入匿名接口或嵌入匿名指針對象來實現(xiàn)繼承的做法其實是一種純虛繼承,繼承的只是接口指定的規(guī)范,真正的實現(xiàn)在運行時才被注入。例如,可以模擬實現(xiàn)一個gRPC的插件:

type grpcPlugin struct {
    *generator.Generator
}
func (p *grpcPlugin) Name() string { return "grpc" }
func (p *grpcPlugin) Init(g *generator.Generator) {
    p.Generator = g
}
func (p *grpcPlugin) GenerateImports(file *generator.FileDescriptor) {
    if len(file.Service) == 0 {
        return
    }
    p.P(`import "google.golang.org/grpc"`)
    // ...
}

構(gòu)造的grpcPlugin類型對象必須滿足generate.Plugin接口:

type Plugin interface {
    // Name identifies the plugin.
    Name() string
    // Init is called once after data structures are built but before
    // code generation begins.
    Init(g *Generator)
    // Generate produces the code generated by the plugin for this file,
    // except for the imports, by calling the generator's methods
    // P, In, and Out.
    Generate(file *FileDescriptor)
    // GenerateImports produces the import declarations for this file.
    // It is called after Generate.
    GenerateImports(file *FileDescriptor)
}

generate.Plugin接口對應(yīng)的grpcPlugin類型的GenerateImports()方法中使用的p.P(...)函數(shù)卻是通過Init()函數(shù)注入的generator.Generator對象實現(xiàn)的。這里的generator.Generator對應(yīng)一個具體類型,如果generator.Generator是接口類型,我們甚至可以傳入真正的實?現(xiàn)。

Go語言通過幾種簡單特性的組合,就輕易實現(xiàn)了鴨子型的面向?qū)ο蠛吞摂M繼承等高級特性,真的是不可思?議。

主站蜘蛛池模板: 宕昌县| 高尔夫| 富顺县| 高邑县| 包头市| 共和县| 会理县| 定安县| 都江堰市| 贵阳市| 榕江县| 镇远县| 清远市| 安陆市| 富民县| 南投市| 梨树县| 龙岩市| 南和县| 襄汾县| 永泰县| 高清| 桃江县| 上林县| 开远市| 光泽县| 灵丘县| 浪卡子县| 马关县| 乌海市| 卢氏县| 乐业县| 额尔古纳市| 防城港市| 邢台县| 广东省| 武冈市| 阳江市| 桑日县| 陇南市| 托克逊县|