go中的interface

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

用了9年的C++,1年的C#,最近改用go語言開發,深感go語言的設計簡單,其設計宗旨也是less is more,它極大的加快了開發速度。

go語言吸取了很多現代語言的優點,一個比較重要的特性就是基於介面編程,函數是程式世界的第一公民,這個有點像scalar語言。實現這個介面的語言原型是interface。

基於介面編程

C++中不支援介面,介面的實現方式是用純虛函數來實現的,C#具有介面,但是它認為介面是對象的一個能力,這是一個很大的進步,但是它不大靈活,比如

public Interface IFile{    public int Read(string filePath, int len)    public int Write(string filePath, int len)}這個介面實現從一個檔案中讀取len個位元組資料,返回實際的資料,以及寫入len個位元組資料,返回實際寫入的資料個數public class CDataNode:IFile{    public int Read(string filePath, int len){...}    public int Write(string filePath, int len){...}}這樣CDataNode就可以在支援IFile介面的地方直接使用

這樣的代碼寫了很久,但是如果發現IFile這個粒度太大了,有些地方只需要Read,有些地方需要Write,在C#中只能是

public Interface IReadpublic Interface IWritepublic Interface IFile:IRead,IWrite

在go中不需要這樣,只需要再聲明IRead和IWrite介面,IFile可以直接轉換到IRead和IWrite,它更加靈活。

package mainimport (    "fmt")type iFile interface {    Read(int) int    Write(int) int}type rwFile struct {}func (rw rwFile) Read(int) int {    fmt.Println("rwFile read")    return 0}func (rw rwFile) Write(int) int {    fmt.Println("rwFile write")    return 0}type iRead interface {    Read(int) int}type iWrite interface {    Write(int) int}func TestInterface() {    var rwInterface iFile = rwFile{}    rwInterface.Read(0)    rwInterface.Write(0)    var rInterface iRead = rwInterface    rInterface.Read(0)    var wInterface iWrite = rwInterface    wInterface.Write(0)}

模組之間依賴介面,並且go是不支援繼承,只支援組合,繼承是一種強關係,一旦定下來就很難改動,或者說改動成本非常之高,組合是一種弱關係,更加鬆散一點。

繼承和多態特性

繼承和多態是物件導向設計一個非常好的特性,它可以更好的抽象架構,讓模組之間依賴於介面,而不是依賴於具體實現。

package mainimport (    "fmt")type iAnimal interface {    speak() string}type dog struct {}func (d dog) speak() string {    return "Woof!"}type cat struct {}func (c cat) speak() string {    return "Meow!"}type coder struct {}func (c coder) speak() string {    return "Hello world!"}type superCoder struct {    coder//如果沒有給出變數名字,編譯器產生一個coder coder的變數,然後它的文法糖是針對這種變數的訪問,直接可以訪問其內部成員變數,而不需要經過.coder來訪問。}func (s superCoder) sayHi() {    fmt.Println("super coder say hi")}func Polymorphic() {    animals := []iAnimal{dog{}, cat{}, coder{}, superCoder{}}    for _, animal := range animals {        fmt.Println(animal.speak())    }}

依賴於介面來實現,只要實現了這個介面就可以認為賦值給這個口,實現動態綁定。superCoder在這裡可以認為是繼承與coder,但是它增加了新的方法SayHi,原有的speak方法沒有變化,所以它也不需要重寫。
go中是沒有繼承的,匿名成員變數也只是一個文法糖而已,並不是繼承

type cat struct{    age int}type dog struct{    cat}var c cat = dog{}//編譯錯誤var pc *cat = &dog{}//編譯錯誤var pc *cat = (*cat)(&dog{})//編譯錯誤

在C++中是可以用基類指標指向子類的對象的,但是go中它認為是兩種類型,不能進行轉換。
從根本上講,struct在go中是沒有虛表指標,這種特性只有interface才有,所以它是兩種獨立的類型,不能相互轉換

interface的使用

interface特性是:
* 是一組函數簽名的集合
* 是一種類型

interface{}

interface{}是一類特殊的介面,因為它沒有實現任何類型,所以所有類型變數都可以賦值給它,有點像C#的object,它是所有對象的基類。

package mainimport (    "fmt")func PrintAll(vals []interface{}) {    for _, val := range vals {        fmt.Println(val)    }}func main() {    names := []string{"stanley", "david", "oscar"}    PrintAll(names)}

最開始我很奇怪為什麼不能編譯通過,後來仔細想想就是應該編譯不過,這裡PrintAll接收一個“[]interface{}”類型,輸入的參數是一個”[]string”類型,兩個是不同的類型,不能進行賦值。所以正確的做法是

package mainimport (    "fmt")func PrintAll(vals []interface{}) {    for _, val := range vals {        fmt.Println(val)    }}func main() {    names := []string{"stanley", "david", "oscar"}    //PrintAll(names)    vals := make([]interface{}, len(names))    for i, v := range names {        vals[i] = v    }    PrintAll(vals)}

因為[]interface{}也是一種類型,所以它是可以賦值給interface{}的.

interface{}用法

有一個需求,將一個json解析稱為一個map,因為資料格式多變,採用interface{},調用庫函數UnmarshalJSON。

package mainimport (    "encoding/json"    "fmt"    "reflect")// start with a string representation of our JSON datavar input = `{    "created_at": "Thu May 31 00:00:01 +0000 2012"}`func main() {    // our target will be of type map[string]interface{}, which is a    // pretty generic type that will give us a hashtable whose keys    // are strings, and whose values are of type interface{}    var val map[string]interface{}    if err := json.Unmarshal([]byte(input), &val); err != nil {        panic(err)    }    fmt.Println(val)    for k, v := range val {        fmt.Println(k, reflect.TypeOf(v))    }}返回結果map[created_at:Thu May 31 00:00:01 +0000 2012]created_at string

注意到這裡的時間是一種特殊格式的time,和普通的不一樣,這樣返回的結果是string類型而不是time類型.及時將map的資料改成time也會有問題,因為它不是一種標準的time格式,會拋出異常。
可以考慮實現json的介面

type Unmarshaler interface {    UnmarshalJSON([]byte) error}func (t *Timestamp) UnmarshalJSON(b []byte) error {    v, err := time.Parse(time.RubyDate, string(b[1:len(b)-1]))    if err != nil {        return err    }    *t = Timestamp(v)    return nil}

上面的map就能夠正常,返回的類型也是time類型。

## interface{}一般可以作為函數的入口參數,但是最好不要作為一個函數的傳回值。
有一個需求,要設計一個API,將http request的請求轉化為某中資料類型格式,格式有可能有很多種。
因為返回的資料格式沒有定,採用interface{}格式最好。

GetEntity(*http.Request) (interface{}, error)caller:switch v:Get(r).type(){    int:        ...    string:        ...}

這樣做非常不好,因為每次增加一個新類型,調用方就需要改變,不滿足開閉原則,正確的做法是

type Entity interface {    UnmarshalHTTP(*http.Request) error}func GetEntity(r *http.Request, v Entity) error {    return v.UnmarshalHTTP(r)}caller:var u Userif err := GetEntity(req, &u); err != nil {    // ...}其中User需要實現這個介面func (u *User) UnmarshalHTTP(r *http.Request) error {   // ...}

將interface{}作為參數,而不是傳回值,這樣做的好處增加新的類型只需要實現Entity介面就可以了,再增加一個對這種類型的調用,而不需要修改GetEntity的內部實現。

interface的底層實現

我們先看一個例子,從這個例子來看怎麼實現的

type Stringer interface {    String() string}type Binary uint64func (i Binary) String() string {    return strconv.Uitob64(i.Get(), 2)}func (i Binary) Get() uint64 {    return uint64(i)}

建立一個uint64,初始值為200

b := Binary(200)

其記憶體模型如下:

此時將b賦值給一個介面:

s := Stringer(b)//如果不能轉換會拋出exception


interface s由兩個成員,
* 第一個是itable,有點類似C++的vtable,儲存了一系列的函數對象,第一個是type,通過它可以找到它所屬於哪個類型,這個是所有interface的itable第一個值,其它的就是這個interface類型需要的函數對應的實際實現,我們可以看到String()的Binary實現。Binary中實現了Get函數,因為它不是Stringer的介面,所以就沒有放在這裡。
itable是如何產生?它是在運行時動態產生的,首先每種類型,編譯器都會為其產生meta,這個meta裡包含了所有它支援的函數以及成員變數,當一個對象要賦值給interface時,runtime會遍曆整個對象的函數簽名找到匹配這個interface的函數簽名,如果全部能夠匹配則繼續執行,否則就會拋出異常(如果不檢查成功失敗的話),同理將一個interface賦值給另外一個interface.
假設變數的類型有m個函數,interface有n個需要支援的簽名,則採用演算法每個都匹配一遍,複雜度為O(mn),go中它會將每個函數簽名集合進行簽名,這樣做匹配的時候覆雜度只為O(m+n).
* 另外一個是實際的資料data,這裡它會建立一個新的值,所以b的修改是不會影響介面的

func (i *Binary) String() string {    return strconv.Uitob64(i.Get(), 2)}ps := Stringer(&b)

這種情況ps中的data部分儲存的就是指向b的指標,因為在做轉換的時候傳進來的也是指標。

總結

  • go語言所有的賦值都是值拷貝
  • interface是一種函數集合的類型
  • interface的itable中儲存了這個interface的具體實現,idata中儲存了賦值資料的拷貝
  • interface作為參數的入口函數,而不應該作為傳回值。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.