Golang的Interface是個什麼鬼

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

Golang的Interface是個什麼鬼

問題概述

Golang的interface,和別的語言是不同的。它不需要顯式的implements,只要某個struct實現了interface裡的所有函數,編譯器會自動認為它實現了這個interface。第一次看到這種設計的時候,我的第一反應是:What the fuck?這種奇葩的設計方式,和主流OO語言顯式implement或繼承的區別在哪兒呢?

直到看了SICP以後,我的觀點發生了變化:Golang的這種方式和Java、C++之流並無本質區別,都是實現多態的具體方式。而所謂多態,就是“一個介面,多種實現”。

SICP裡詳細解釋了為什麼同一個介面,需要根據不同的資料類型,有不同的實現;以及如何做到這一點。在這裡沒有OO的概念,先把OO放到一邊,從原理上看一下這是怎麼做到的。

先把大概原理放在這裡,然後再舉例子。為了實現多態,需要維護一張全域的尋找表,它的功能是根據類型名和方法名,返回對應的函數入口。當我增加了一種類型,需要把新類型的名字、相應的方法名和實際函數入口添加到表裡。這基本上就是所謂的動態綁定了,類似於C++裡的vtable。對於SICP中使用的lisp語言來說,這些工作需要手動完成。而對於java,則通過implements完成了這項工作。而golang則用了更加激進的方式,連implements都省了,編譯器自動探索自動綁定。

一個複數包的例子

SICP裡以複數為例,我用clojure、java和golang分別實現了一下,代碼放在https://github.com/nanoix9/golang-interface。這裡的目的是實現一個複數包,它支援直角座標(rectangular)和極座標(polar)兩種實現方式,但是兩者以相同的形式提供對外的介面,包括擷取實部、虛部、模、輻角四個操作,文中簡單起見,僅以擷取實部為例。代碼中有完整的內容。

Clojure版

對於直角座標,用一個兩個元素的列表表示它,分別是實部和虛部。

(defn make-rect [r i] (list r i))

對於極座標,也是含有兩個元素的列表,分別表示模和輻角

(defn make-polar [abs arg] (list abs arg))

現在要加一個“取實部”的函數get-real。問題來了,我希望這個函數能同時處理兩種座標,而且對於使用者來說,無論使用哪種座標表示,get-real函數的行為是一致的。最簡單的想法是,增加一個tag欄位用於區分兩種類型,然後get-real根據類型資訊執行不同的操作。

為此,定義attach-tagget-tagget-content函數用於關聯標籤、提取標籤和提取內容:

(defn attach-tag [tag data] (list tag data))(defn get-tag [data-with-tag] (first data-with-tag))(defn get-content [data-with-tag] (second data-with-tag))

在構造複數的函數中加入tag

(defn make-rect [r i] (attach-tag 'rect (list r i)))(defn make-polar [abs arg] (attach-tag 'polar (list abs arg)))

get-real函數首先擷取tag,根據直角座標或極座標執行不同的操作

(defn get-real [c]  (let [tag (get-tag c)        num (get-content c)]    (cond (= tag 'rect) (first num)          (= tag 'polar) (* (first num) (Math/cos (second num)))          :else (println "Unknown complex type:" tag))))

但是這樣有個問題,如果要加第三種類型怎麼辦?必須修改get-real函數。也就是說,要增加一種實現,必須改動函數主入口。有沒有方法避免呢?答案就是採用前面的尋找表(當然這不是唯一方法,SICP中還介紹了訊息傳遞方法,這裡就不介紹了)。這個尋找表提供get-opput-op兩個方法

 (defn get-op [tag op-name] ... (defn put-op [tag op-name func] ...)

這裡只給出原型,get-op根據類型名和方法名,擷取對應的函數入口。而put-op向表中增加類型名、方法名和函數入口。這張表的內容直觀上可以這麼理解

tag\op-name 'get-real 'get-image ...
'rect get-real-rect get-image-rect ...
'polar get-real-polar get-image-polar ...

於是get-real函數可以這樣實現:首先每種類型各自將自己的函數入口添加到尋找表

(defn install-rect []  (letfn [(get-real [c] (first c))]    put-op 'rect 'get-real get-real))(defn install-polar []  (letfn [(get-real [c] (* (first c) (Math/cos (second c))))]    put-op 'polar 'get-real get-real))(install-rect)(install-polar)

注意這裡用了局部函數letfn,所以兩種類型都用get-real作為函數名並不衝突。

定義apply-generic函數,用來從尋找表中擷取函數入口,並把tag去掉,將內容和剩餘參數送給擷取到的函數

(defn apply-generic [op-name tagged-data & args]  (let [tag (get-tag tagged-data)        content (get-content tagged-data)        func (get-op tag op-name)]    (if (null? func)        (println "No entry for data type" tag "and method" op-name))        (apply func (cons content args))))

get-real函數可以實現了

(defn get-real [c]    (apply-generic 'get-real c))

Java版

Java實現複數包就不需要這麼麻煩了,編譯器完成了大部分工作。當然Java是靜態語言,還有類型檢查。

public interface Complex {    public double getReal();    ...}public class ComplexRect implements Complex {    private double real;    private double image;    public double getReal() {        return real;    }    ...}public class ComplexPolar implements Complex {    private double abs;    private double arg;    public double getReal() {        return abs * Math.cos(arg);    }    ...}

Golang版

Golang和Java的差別就是省去了implements

type Complex interface {    GetReal() float64    ...}type ComplexRect struct {    real, image float64}func (c ComplexRect) GetReal() float64 {    return c.real}...type ComplexPolar struct {    abs, arg float64}func (c ComplexPolar) GetReal() float64 {    return c.abs * math.Cos(c.arg)}...

乍一看看不出ComplexRectComplex之間有什麼關係,它是隱含的,編譯器自動探索。這樣的做法更靈活,比如增加一個新的介面類型,編譯器會自動探索那些struct實現了該介面,而無需修改struct的代碼。如果是java,就必須修改原始碼,顯式的implements

總結

通過這個問題,我意識到,OO只不過是一種方法,其實本沒有什麼對象。至於為什麼要OO,最根本的,是要實現“一個介面,多種實現”,這就要求介面是穩定的,而實現有可能是多變的。如果介面也是經常變的,那就沒必要把介面抽象出來了。至於代碼結構是否反映了世界的繼承/組合等關係,這並不重要,也不是根本的。重要的是,將穩定的介面和不穩定的實現分離,使得改動某個模組的時候,不至於影響到其他部分。這是軟體本質上的複雜性提出的要求,對於大型軟體來說,模組的分解和隔離尤為重要。

為了達到這個目的,C++實現了vtable,Java提供了interface,Golang則自動探索這種關係。可以用OO,也可以不用OO。無論語言提供了哪種方式,背後的思想是統一的。甚至我們可以在語言特性滿足不了需求的時候,自己實現相關的機制,例如spring,通過xml完成依賴注入,這使得可以在不改動原始碼的情況下,用一種實現替換另一種實現。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.