go語言介面避免無意被適配

浮塵丶若夢發表於2020-10-17

學習記錄(Go語言高階程式設計)

物件和介面之間太靈活了,導致我們需要人為地限制這種無意之間的適配。常見的做法是定義一 個含特殊方法來區分介面。比如 runtime 包中的 Error 介面就定義了一個特有 的 RuntimeError 方法,用於避免其它型別無意中適配了該介面:

type runtime.Error interface { 
    error
    RuntimeError()
}

在protobuf中, Message 介面也採用了類似的方法,也定義了一個特有的 ProtoMessage ,用於 避免其它型別無意中適配了該介面:

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

不過這種做法只是君子協定,如果有人刻意偽造一個 proto.Message 介面也是很容易的。再嚴格一 點的做法是給介面定義一個私有方法。只有滿足了這個私有方法的物件才可能滿足這個介面,而私有方法的名字是包含包的絕對路徑名的,因此只能在包內部實現這個私有方法才能滿足這個介面。測試包中 的 testing.TB 介面就是採用類似的技術:

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() 
}

不過這種通過私有方法禁止外部物件實現介面的做法也是有代價的:首先是這個介面只能包內部使用, 外部包正常情況下是無法直接建立滿足該介面物件的;其次,這種防護措施也不是絕對的,惡意的使用者 依然可以繞過這種保護機制。
在前面的方法一節中我們講到,通過在結構體中嵌入匿名型別成員,可以繼承匿名型別的方法。其實這 個被嵌入的匿名成員不一定是普通型別,也可以是介面型別。我們可以通過嵌入匿名 的 testing.TB 介面來偽造私有的 private 方法,因為介面方法是延遲繫結,編譯 時 private 方法是否真的存在並不重要。

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 結構體型別中重新實現了 Fatal 方法,然後通過將物件隱式轉換 為 testing.TB 介面型別(因為內嵌了匿名的 testing.TB 物件,因此是滿足 testing.TB 介面 的),然後通過 testing.TB 介面來呼叫我們自己的 Fatal 方法

這種通過嵌入匿名介面或嵌入匿名指標物件來實現繼承的做法其實是一種純虛繼承,我們繼承的只是接 口指定的規範,真正的實現在執行的時候才被注入。比如,我們可以模擬實現一個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"`)
    // ...
}

構造的 grpcPlugin 型別物件必須滿足 generate.Plugin 介面 (在”github.com/golang/protobuf/protoc-gen-go/generator”包中)

type Plugin interface {
    Name() string
    Init(g *Generator)
    Generate(file *FileDescriptor)
    GenerateImports(file *FileDescriptor)
}

generate.Plugin 介面對應的 grpcPlugin 型別的 GenerateImports 方法中使用 的 p.P(…) 函式卻是通過 Init 函式注入的 generator.Generator 物件實現。這裡 的 generator.Generator 對應一個具體型別,但是如果 generator.Generator 是介面型別的話 我們甚至可以傳入直接的實現。
Go語言通過幾種簡單特性的組合,就輕易就實現了鴨子物件導向和虛擬繼承等高階特性,真的是不可思議

相關文章