[go]go接口装箱-其它类型转换为interface类型

Interface实现原理分析

具体类型转换成接口类型

到此已经知道什么是接口以及接口的底层结构,那么当具体类型赋值给接口类型时,是如何进行转换的?再来看下2.3中的示例


package main

import "fmt"

type Runner interface {
  Run()
}

type Person struct {
  Name string
}

func (p Person) Run() {
  fmt.Printf("%s is running\n", p.Name)
}

func main() {
  var r Runner
  r = Person{Name: "song_chh"}
  r.Run()
}

通过Go提供的工具生成汇编代码

go tool compile -S interface.go

只截取与第19行相关的代码

0x001d 00029 (interface.go:19)  PCDATA  $2, $0
0x001d 00029 (interface.go:19)  PCDATA  $0, $1
0x001d 00029 (interface.go:19)  XORPS  X0, X0
0x0020 00032 (interface.go:19)  MOVUPS  X0, ""..autotmp_1+32(SP)
0x0025 00037 (interface.go:19)  PCDATA  $2, $1
0x0025 00037 (interface.go:19)  LEAQ  go.string."song_chh"(SB), AX
0x002c 00044 (interface.go:19)  PCDATA  $2, $0
0x002c 00044 (interface.go:19)  MOVQ  AX, ""..autotmp_1+32(SP)
0x0031 00049 (interface.go:19)  MOVQ  $8, ""..autotmp_1+40(SP)
0x003a 00058 (interface.go:19)  PCDATA  $2, $1
0x003a 00058 (interface.go:19)  LEAQ  go.itab."".Person,"".Runner(SB), AX
0x0041 00065 (interface.go:19)  PCDATA  $2, $0
0x0041 00065 (interface.go:19)  MOVQ  AX, (SP)
0x0045 00069 (interface.go:19)  PCDATA  $2, $1
0x0045 00069 (interface.go:19)  PCDATA  $0, $0
0x0045 00069 (interface.go:19)  LEAQ  ""..autotmp_1+32(SP), AX
0x004a 00074 (interface.go:19)  PCDATA  $2, $0
0x004a 00074 (interface.go:19)  MOVQ  AX, 8(SP)
0x004f 00079 (interface.go:19)  CALL  runtime.convT2I(SB)
0x0054 00084 (interface.go:19)  MOVQ  16(SP), AX
0x0059 00089 (interface.go:19)  PCDATA  $2, $2
0x0059 00089 (interface.go:19)  MOVQ  24(SP), CX

可以看到,编译器在构造itab后调用runtime.convT2I(SB)转换函数,看下函数的实现

//runtime/iface.go
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
  t := tab._type
  if raceenabled {
    raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
  }
  if msanenabled {
    msanread(elem, t.size)
  }
  x := mallocgc(t.size, t, true)
  typedmemmove(t, x, elem)
  i.tab = tab
  i.data = x
  return
}

首先根据类型大小调用mallocgc申请一块内存空间,将elem指针的内容拷贝到新空间,将tab赋值给iface的tab,将新内存指针赋值给iface的data,这样一个iface就创建完成

将示例代码稍作更改,使用结构体指针类型的变量赋值给接口变量

r = &Person{Name: "song_chh"}

再次通过工具生成汇编代码

go tool compile -S interface.go

0x001d 00029 (interface.go:19)  PCDATA  $2, $1
0x001d 00029 (interface.go:19)  PCDATA  $0, $0
0x001d 00029 (interface.go:19)  LEAQ  type."".Person(SB), AX
0x0024 00036 (interface.go:19)  PCDATA  $2, $0
0x0024 00036 (interface.go:19)  MOVQ  AX, (SP)
0x0028 00040 (interface.go:19)  CALL  runtime.newobject(SB)
0x002d 00045 (interface.go:19)  PCDATA  $2, $2
0x002d 00045 (interface.go:19)  MOVQ  8(SP), DI
0x0032 00050 (interface.go:19)  MOVQ  $8, 8(DI)
0x003a 00058 (interface.go:19)  PCDATA  $2, $-2
0x003a 00058 (interface.go:19)  PCDATA  $0, $-2
0x003a 00058 (interface.go:19)  CMPL  runtime.writeBarrier(SB), $0
0x0041 00065 (interface.go:19)  JNE  105
0x0043 00067 (interface.go:19)  LEAQ  go.string."song_chh"(SB), AX
0x004a 00074 (interface.go:19)  MOVQ  AX, (DI)

首先编译器通过type."".Person(SB)获取Person结构体类型,作为参数调用runtime.newobject()函数,同样的在源码中查看函数定义

import "unsafe"

// runtime/malloc.go

// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
  return mallocgc(typ.size, typ, true)
}

newobject以*Person作为入参,创建新的Person结构体指针,之后由编译器设置值,iface由编译器直接生成
除了convT2I函数外,其实在runtime/runtime.go文件中,还有很多转换函数的定义

// Non-empty-interface to non-empty-interface conversion.
func convI2I(typ *byte, elem any) (ret any)

// Specialized type-to-interface conversion.
// These return only a data pointer.
func convT16(val any) unsafe.Pointer     // val must be uint16-like (same size and alignment as a uint16)
func convT32(val any) unsafe.Pointer     // val must be uint32-like (same size and alignment as a uint32)
func convT64(val any) unsafe.Pointer     // val must be uint64-like (same size and alignment as a uint64 and contains no pointers)
func convTstring(val any) unsafe.Pointer // val must be a string
func convTslice(val any) unsafe.Pointer  // val must be a slice

// Type to empty-interface conversion.
func convT2E(typ *byte, elem *any) (ret any)
func convT2Enoptr(typ *byte, elem *any) (ret any)

// Type to non-empty-interface conversion.   
func convT2I(tab *byte, elem *any) (ret any)        //for the general case
func convT2Inoptr(tab *byte, elem *any) (ret any)   //for structs that do not contain pointers

convI2I用于接口转换成另一个接口时调用,在3.4会进行讲解

convT2Inoptr用于变量内部不含指针的转换,noptr可以理解为no pointer,转换过程与convT2I类似

convT16、convT32、convT64、convTstring 和 convTslice是针对简单类型转接口的特例优化,有兴趣的可以看下函数实现的源码,因为这几个函数内容相似,这里就简单介绍下convT64

//runtime/iface.go
func convT64(val uint64) (x unsafe.Pointer) {
  if val == 0 {
    x = unsafe.Pointer(&zeroVal[0])
  } else {
    x = mallocgc(8, uint64Type, false)
    *(*uint64)(x) = val
  }
  return
}

相较于convT2系列函数,缺少typedmemmove和memmove函数的调用,减少内存拷贝。另外如果变量值为该类型的零值,则不会调用 mallocgc 去申请一块新内存,而是直接返回指向zeroVal[0]的指针

再来看下,空接口转换函数convT2E

func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
  if raceenabled {
    raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
  }
  if msanenabled {
    msanread(elem, t.size)
  }
  x := mallocgc(t.size, t, true)
  // TODO: We allocate a zeroed object only to overwrite it with actual data.
  // Figure out how to avoid zeroing. Also below in convT2Eslice, convT2I, convT2Islice.
  typedmemmove(t, x, elem)
  e._type = t
  e.data = x
  return
}

接口与接口的转换

如果某个类型实现多个接口,那接口直接是如何进行转换的,还是先看一段示例:


package main

import "fmt"

type Runner interface {
  Run()
  Say()
}

type Sayer interface {
  Say()
}

type Person struct {
  Name string
}

func (p Person) Run() {
  fmt.Printf("%s is running\n", p.Name)
}

func (p Person) Say() {
  fmt.Printf("hello, %s", p.Name)
}

func main() {
  var r Runner
  r = Person{Name: "song_chh"}

  var s Sayer
  s = r
  s.Say()
}

增加Sayer接口定义,包含Say()方法,在main函数中声明一个Sayer变量,并将Runner接口变量赋值给Sayer变量。因为Person实现了Say()方法,所以说Person既实现了是Runner接口,又实现了Sayer接口

执行命令

go tool compile -S interface.go

截取32行汇编代码

0x0062 00098 (interface.go:32)  PCDATA  $2, $3
0x0062 00098 (interface.go:32)  LEAQ    type."".Sayer(SB), DX
0x0069 00105 (interface.go:32)  PCDATA  $2, $2
0x0069 00105 (interface.go:32)  MOVQ    DX, (SP) 
0x006d 00109 (interface.go:32)  MOVQ    AX, 8(SP)
0x0072 00114 (interface.go:32)  PCDATA  $2, $0
0x0072 00114 (interface.go:32)  MOVQ    CX, 16(SP)
0x0077 00119 (interface.go:32)  CALL    runtime.convI2I(SB)
0x007c 00124 (interface.go:32)  MOVQ    24(SP), AX
0x0081 00129 (interface.go:32)  PCDATA  $2, $2
0x0081 00129 (interface.go:32)  MOVQ    32(SP), CX

可以看到在执行期间,调用runtime.convI2I进行接口转换,接下来看下源代码

func convI2I(inter *interfacetype, i iface) (r iface) {
  tab := i.tab
  if tab == nil {
    return
  }
  if tab.inter == inter {
    r.tab = tab
    r.data = i.data
    return
  }
  r.tab = (inter, tab._type, false)
  r.data = i.data
  return
}

函数参数inter表示接口的类型,由编译器生成,即type."".Sayer(SB),i 是绑定实体的接口, r 是转换后新的接口,如果要转换的接口是同一类型,则直接把 i 的tab和data给新接口 r ,将 r 返回。如果要转换的接口不是同一类型,则通过getitab生成一个新的tab复制给r.tab,然后将 r 返回

那么具体来看一下getitab这个函数,还是先看源码


func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
  if len(inter.mhdr) == 0 {
    throw("internal error - misuse of itab")
  }

  // easy case
  if typ.tflag&tflagUncommon == 0 {
    if canfail {
      return nil
    }
    name := inter.typ.nameOff(inter.mhdr[0].name)
    panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()})
  }

  var m *itab

  // First, look in the existing table to see if we can find the itab we need.
  // This is by far the most common case, so do it without locks.
  // Use atomic to ensure we see any previous writes done by the thread
  // that updates the itabTable field (with atomic.Storep in itabAdd).
  t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
  if m = t.find(inter, typ); m != nil {
    goto finish
  }

  // Not found.  Grab the lock and try again.
  lock(&itabLock)
  if m = itabTable.find(inter, typ); m != nil {
    unlock(&itabLock)
    goto finish
  }

  // Entry doesn't exist yet. Make a new entry & add it.
  m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
  m.inter = inter
  m._type = typ
  m.init()
  itabAdd(m)
  unlock(&itabLock)
finish:
  if m.fun[0] != 0 {
    return m
  }
  if canfail {
    return nil
  }
  // this can only happen if the conversion
  // was already done once using the , ok form
  // and we have a cached negative result.
  // The cached result doesn't record which
  // interface function was missing, so initialize
  // the itab again to get the missing function name.
  panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}

首先,用 t 保存全局itabTable的地址,使用t.find进行查找,这个是通用的查找

如果没有查找到,就会上锁,重新使用itabTable.find进行查找

再没有找到,就会根据具体类型typ和接口类型inter生成一个itab,并将这个新生成的itab添加到全局的itabTable中。如果具体类型并没有实现接口,根据canfail值返回nil或者painc

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章