- Go語言高級編程(第2版)
- 柴樹杉 曹春暉
- 2277字
- 2025-08-07 17:56:14
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繼承等高級特性,真的是不可思?議。
- Mastering OpenLayers 3
- Big Data Analytics
- QTP自動化測試進(jìn)階
- 3D少兒游戲編程(原書第2版)
- Webpack實戰(zhàn):入門、進(jìn)階與調(diào)優(yōu)
- Android移動開發(fā)案例教程:基于Android Studio開發(fā)環(huán)境
- Web編程基礎(chǔ):HTML5、CSS3、JavaScript(第2版)
- SQL Server on Linux
- 微信小程序開發(fā)邊做邊學(xué)(微課視頻版)
- Swift 2 Design Patterns
- Opa Application Development
- 開發(fā)者測試
- React Router Quick Start Guide
- Hands-On Machine Learning with ML.NET
- Lucene 4 Cookbook