Go 方法調用與介面

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

在比較C++和Go的時候,通常會說到Go不支援繼承和多態,但通過組合和介面實現了類似的語言特性。總結一下Go不支援的原因:(1) 首先struct是實值型別,賦值和傳參都會複製全部內容。struct的記憶體布局跟C幾乎一致,沒有任何附加的object資訊,比如指向虛函數表的指標。(2)其次Go不支援隱式的類型轉換,因此用基類的指標指向子類會編譯錯誤。

Go程式抽象的基本原則依賴於介面而不是實現,優先使用組合而不是繼承。

struct的方法調用

對象的方法調用相當於普通函數調用的文法糖。Value方法的調用m.Value()等價於func Value(m M) 即把對象執行個體m作為函數調用的第一個實參壓棧,這時m稱為receiver。通過執行個體或執行個體的指標其實都可以調用所有方法,區別是複製給函數的receiver不同。

通過執行個體m調用Value時,以及通過指標p調用Value時,receiver是m和*p,即複製的是m執行個體本身。因此receiver是m執行個體的副本,他們地址不同。通過執行個體m調用Pointer時,以及通過指標p調用Pointer時,複製的是都是&m和p,即複製的都是指向m的指標,返回的都是m執行個體的地址。

type M struct {    a int}func (m M) Value() string {return fmt.Sprintf("Value: %p\n", &m)}func (m *M) Pointer() string {return fmt.Sprintf("Pointer: %p\n", m)}var m Mp := &m      // p is address of m 0x2101ef018m.Value()    // value(m) return 0x2101ef028m.Pointer()  // value(&m) return 0x2101ef018p.Value()    // value(*p) return 0x2101ef030p.Pointer()  // value(p) return 0x2101ef018
如果想在方法中修改對象的值只能用pointer receiver,對象較大時避免拷貝也要用pointer receiver。

方法集理解

上面例子中通過執行個體m和p都可以調用全部方法,由編譯器自動轉換。在很多go的文法書裡有方法集的概念。類型T方法集包含全部receiver T方法,類型*T包含全部receiver T和*T的方法。這句話一直不理解,既然通過執行個體和指標可以訪問T和*T的所有方法,那方法集的意義是什麼。

定義在M類型的方法除了通過執行個體和執行個體指標訪問,還可以通過method expression的方式調用。這時Pointer對M類型就是不可見的。

(M).Value(m)       // valid(M).Pointer(m)     // invalid M does not have Pointer(*M).Value(&m)     // valid(*M).Pointer(&m)   // valid

再解釋一下method value的receiver複製的問題。這裡u.Test返回的類型類似於閉包返回的FuncVal類型,也就是FuncVal{method_address, receiver_copy}對象。因此下面例子中mValue中已經包含了執行個體u的副本。當然如果Test方法的receiver是*User,結果將不一樣。

u := User{1}     // User{Id int}mValue := u.Test // func(s User) Test() {fmt.Println(s.Id)}u.Id = 2u.Test()  // output: 2mValue()  // output: 1

匿名欄位與組合

Go沒有繼承,但是有結構體嵌入。當一個類型T被匿名的嵌入另一類型M時,T的方法也就會拷貝到M的方法表當中。這時根據方法集的規則,如果M包含的是*T,則M包含T與*T上所有的方法。

通過匿名欄位,Go實現了類似繼承的複用能力,並且可以在M上定義相同的方法名實現 override

interface介面實現

Go的interface是一種內建類型,屬於動態風格的duck-typing類型。介面作為方法簽名的集合,任何類型的方法集中只要擁有與之對應的全部方法,就表示它實現了該介面。

interface底層結構

interface是一個結構體,包含兩個成員。根據interface是否包含方法,底層又分為兩個結構體。eface主要是儲存了類型資訊,以後總結反射時具體講,這裡先總結帶方法的iface。結構體定義在runtime2.go顯然iface由兩部分組成,data域儲存中繼資料,tab描述介面。

type eface struct {    _type *_type    data unsafe.Pointer}type iface struct {    tab *itab    data unsafe.Pointer}
type itab struct {  inter *interfacetype // 儲存該介面的方法簽名  _type *_type // 儲存動態類型的type類型資訊  link *itab // 可能有嵌套的itab  bad int32  unused int32  fun [1]uintptr  // 儲存動態類型對應的實現}type interfacetype struct {  type _type  mhdr []imethod}

為了理解iface的資料結構,找到一個唐老鴨介面介面的例子,通過gdb看看iface的資料到底是什麼。首先dd=&DonalDuck{}這個類型的方法集包括MakeFun Walking Speaking 它實現了DuckActor兩個介面。

type Duck interface {GaGaSpeaking()OfficialWalking()}type Actor interface {MakeFun()}type DonaldDuck struct {height uintname   string}func (dd *DonaldDuck) GaGaSpeaking() { fmt.Println("DonaldDuck gaga") }func (dd *DonaldDuck) OfficialWalking() { fmt.Println("DonaldDuck walk") }func (dd *DonaldDuck) MakeFun() { fmt.Println("DonaldDuck make fun") }func main() {dd := &DonaldDuck{10, "tang lao ya"}var duck Duck = ddvar actor Actor = ddduck.GaGaSpeaking()actor.MakeFun()dd.OfficialWalking()}

可以看出來當dd賦值給介面Duck後,介面duck的data域儲存的地址就是dd對象指向的地址。tab域的inter欄位裡儲存了實現這個介面的兩個方法聲明,其中name儲存了方法的名字。tab域的func指標指向了具體實現,即這個符號對應的程式碼片段.text地址。

具體T類型到Iface的轉換涉及到3個內容的複製 (1) iface的tab域的func欄位儲存T類型的方法集,即對tab域inter聲明的方法的實現。(2) iface的data域指標指向用於賦值的對象的副本。(3) iface的tab域的_type欄位儲存T類型的_type。

編譯期檢測

當T類型沒有實現I介面中所有方法時,從T到I的賦值將拋出TypeAssertionError編譯錯誤。檢查的方法在函數additab當中,即查看T類型的_type方法表uncommentType是否包含了I介面interfacetype中所有的imethod,同時將T類型對方法的實現拷貝到tab的func指向的表中。

type _type struct {size       uintptrptrdata    uintptr // size of memory prefix holding all pointershash       uint32_unused    uint8align      uint8fieldalign uint8kind       uint8alg        *typeAlggcdata  *byte_string *stringx       *uncommontypeptrto   *_typezero    *byte // ptr to the zero value for this type}type uncommontype struct {name    *stringpkgpath *stringmhdr    []method}

三張方法表的區別

1) 每個具體T類型type結構對應的方法表是uncommontype,類型的方法集都在這裡。reflect包中的Method和MethodByName方法都是通過查詢這張表實現的。表中每一項都是method

type method struct {name    *stringpkgpath *stringmtyp    *_typetyp     *_typeifn     unsafe.Pointertfn     unsafe.Pointer}

2) itab的interfacetype域是一張方法表,它聲明了介面所有的方法,每一項都是imethod,可見它沒有實現只有聲明

type imethod struct {  name *string  pkgpath *string  _type *type}

3) itab的func域也是一張方法表,表中每一項是一個函數指標,也就是只有實現沒有聲明。即賦值的時候只是把具體類型的實現,即函數指標拷貝給了itab的func域。

運行時 ConvT2I

Go-internals分析了go編譯器在編譯期產生的文法樹節點。在T2I的轉換時,通過getitab產生了中間狀態itab。並且調用convT2I完成了運行時資料data域的記憶體拷貝,以及中間狀態itab到tab域的賦值。

可以看到getitab完成了T類型的方法表的實現地址到tab的fnc[0]的賦值。完成getiab需要T類型的_type資訊,以及I介面類型的interfacetype方法表,這些都是編譯期提供的。因此介面的動態性和反射的實現都是以編譯期為運行時提供的類型資訊為基礎的。

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {..  m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+      uintptr(len(inter.mhdr)-1)*ptrSize, 0, &memstats.other_sys))  m.inter = inter  m._type = typ...  for k := 0; k < ni; k++ {    for ; j < nt; j++ {     *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*ptrSize)) = t.ifn    }    goto nextimethod   }  // didn't find method  if !canfail {    panic(&TypeAssertionError{"", *typ._string, *inter.typ._string, *iname})  }  return m}

最後的convT2I存在資料的記憶體拷貝,可見data域是T類型對象的一個拷貝。

func convT2I(t *_type, inter *interfacetype, cache **itab, elem unsafe.Pointer, x unsafe.Pointer) (i fInterface) {  tab := (*itab)(atomicloadp(unsafe.Pointer(cache)))...  if x == nil {    x = newobject(t)  }  typedmemmove(t, x, elem)  pi.tab = tab  pi.data = x  return}

總結 將對象賦值給介面時,編譯期檢查對象是否實現了介面所有的方法。運行時將對象的資料、類型、實現拷貝到iface介面當中。

聯繫我們

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