【Free Style】深入Go语言模型(1):interface的底层详解
Interface简介
Go语言的Interface和Java里面的Interface与Scala的Traits类似,都是定义一组行为,也就是定义了一个契约:
上面的代码就是定义了一个Writer的interface,如果要实现这个Interface很简答,只需要实现Write方法就可以了:
type File struct { *file // os specific } func (f *File) Write(b []byte) (n int, err error) { ...... return n, err }
同样可以让其他的struct或者类型来实现
type Buffer struct { buf []byte // contents are the bytes buf[off : len(buf)] off int // read at &buf[off], write at &buf[len(buf)] bootstrap [64]byte // memory to hold first slice; helps small buffers avoid allocation. lastRead readOp // last read operation, so that Unread*
can
work correctly.
}
func (b *Buffer) Write(p []byte) (n int, err error) {
......
return copy(b.buf[m:], p), nil
}
这样File和Buffer都实现了Writer接口,需要注意的是,这种实现是DuckType,只要某个类型实现了Write方法都行,而不需要像Java或者Scala那样必须受Writer类型的影响。(
类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口
)
然后在需要调用Write的地方,只需要实现这个这样的方法,就可以实现多态的特性,也就是面向接口编程:
func myWrite(w Writer, p []byte) (n int, err error){ return w.Write(p) }这个函数的第一个参数是Writer类型,那么只需要传入File和Buffer的指针,那么就可以正常工作。
空interface
在Go语言中,有一种interface{}类型,这就就比较像Scala中的Any:
type Any interface {}
对空接口类型实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。
var any interface{} any = true any = 12.34 any = "hello" any = map[string]int{"one": 1} any = new(bytes.Buffer)我们可以通过类型断言来进行接口实际类型判断和转换。
Interface的内部实现
非空interface的实现
所有Interface,包括有方法和空接口,在内存中都是占据两个字长。那么在32位机器上就是8个字节,在64位机器上就是16个字节。
##EmptyInterface的底层实现
在Go语言的源码位置: src\runtime\runtime2.go中
type eface struct { _type *_type //类型指针 data unsafe.Pointer //数据区域指针 }
可以看到对于空的interface,其实就是两个指针。,先看第一个rtype类型, 这个就表示类型基本信息,包括类型的大小,对齐信息,类型的编号type _type struct { size uintptr ptrdata uintptr // size of memory prefix holding all pointers hash uint32 tflag tflag align uint8 fieldalign uint8 kind uint8 alg *typeAlg // gcdata stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, gcdata is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. gcdata *byte str nameOff ptrToThis typeOff }
非空接口的底层实现(带有方法)
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
link *itab
bad int32
inhash int32 // has this itab been added to hash?
fun [1]uintptr // variable sized
}
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod //接口带有的函数名
}
对于有方法的interface来说,也是两个指针
第一个itab中存放了类型信息,还有一个fun表示方法表。
我们来举一个例子来看看非空接口的是如何表示的:
import (
"strconv"
"fmt"
)
type Stringer interface {
String() string
}
type Binary uint64
func (i Binary) String() string {
return strconv.FormatUint(i.Get(), 2)
}
func (i Binary) Get() uint64 {
return uint64(i)
}
func main() {
b := Binary(200)
s := Stringer(b)
fmt.Println(s.String())
}
对于Binary,作为一个64位整数,可以这么表示:
对于s := Stringer(b),可以如下表示:
那么对于s来说
itab中的ityp表示的是Stringer这个接口,typ表示的是Binary这个动态类型,fun函数表中存放的就是Binary中实现了String而接口的方法地址。
对于接口的type-switch,返回的就是静态类型
对于反射里面的TypeOf,返回的是动态类型,也就是数据真实的类型对于调用s.String()方法,其实就是 s.itab->fun[0]。
Interface的赋值过程
var sum uintptr = 0
func sub(inter interface{}) {
t := reflect.TypeOf(inter)
sum += t.Size()
}
func main() {
var inter int = 1
sub(inter)
fmt.Println(sum)
}
然后生成汇编代码,查看main函数相关的:
MOVQ $1, “”..autotmp_6+48(SP)这个是var inter int = 1
LEAQ type.int(SB), AX 这个是取到了int类型的地址
CALL runtime.convT2E(SB) 这个调用创建了一个interface{}
当我们将一个值(字符串,整数,自定义类型等等Anything)赋给interface{}的时候,Go语言会调用runtime.convT2E去创建Emtpy interface的数据结构,也就是我们前面讲的emptyInterface,这个里面就会有内存申请的动作。(如果要创建是的有方法的interface那么调用的是convT2I方法)
在src/cmd/compile/internal/gc/walk.go中:
会根据interface的源类型和目标类型进行选择对应的函数func convFunc N a me(from, to *Type) string { tkind := to.iet() switch from.iet() { case 'I': switch tkind { case 'I': return "convI2I" } case 'T': switch tkind { case 'E': return "convT2E" case 'I': return "convT2I" } } Fatalf("unknown conv func %c2%c", from.iet(), to.iet()) panic("unreachable") } func walkexpr(n *Node, init *Nodes) *Node { case OCONVIFACE: //完成Interface的转换,包括T2I, T2E, I2I, 这里有一大堆代码,读不太懂,就是需要生成itab或者type信息, 然后调用conv函数生成interface if isdirectiface(n.Left.Type) { var t *Node if n.Type.IsEmptyInterface() { t = typename(n.Left.Type) } else { t = itabname(n.Left.Type, n.Type) } l := nod(OEFACE, t, n.Left) l.Type = n.Type l.Typecheck = n.Typecheck n = l break } ...... fn := syslook(convFun
***
me(n.Left.Type, n.Type)) fn = substArgTypes(fn, n.Left.Type, n.Type) dowidth(fn.Type) n = nod(OCALL, fn, nil) }
我们看一下convT2E的代码实现,就是根据参数中type和data生成一个eface:func convT2E(t *_type, elem unsafe.Pointer) (e eface) { if raceenabled { raceReadObjectPC(t, elem, getcallerpc(unsafe.Pointer(&t)), funcPC(convT2E)) } if msanenabled { msanread(elem, t.size) } if isDirectIface(t) { // This case is implemented directly by the compiler. throw("direct convT2E") } x := newobject(t) // TODO: We allocate a zeroed object only to overwrite it with // actual data. Figure out how to avoid zeroing. Also below in convT2I. typedmemmove(t, x, elem) e._type = t //类型赋值 e.data = x //数据赋值 return }
interface赋值的优化
如果是直接将一个指针类型赋值给interface,那么go语言会比较简单地完成这一次赋值
在go中有这么一个函数,如果是将一个指针那么可以更简单地生成Interface//
can
this type be stored directly in an interface word?
// Yes, if the representation is a single pointer.
func isdirectiface(t *Type) bool {
switch t.Etype {
case TPTR32,
TPTR64,
TCHAN,
TMAP,
TFUNC,
TUNSAFEPTR:
return true
case TARRAY:
// Array of 1 direct iface type
can
be direct. return t.NumElem() == 1 && isdirectiface(t.Elem()) case TSTRUCT: // Struct with 1 field of direct iface type
can
be direct.
return t.NumFields() == 1 && isdirectiface(t.Field(0).Type)
}
return false
}
如果赋值给interface的值可以用一个指针的空间来存储,会进行优化吗
我们从前面看到了interface是由一个type和data指针组成的,如果刚好值可以用一个指针空间存储,还需要一个二级指针来存储吗,在网上也有这样的描述:
网上说是会优化的,我实际测试了一下,并没有优化:
输出的结果是类似于:0xc042008240 : 10
所以并没有像网上说的有优化,不知道是不是Go语言就没有实现过这样的优化
Interface的类型断言
我们经常使用type-switch或者v.(T)来判断一个interface是否满足一个另一个interface的要求
b := Binary(200) var a interface{} = b if v, ok := a.(Stringer); ok { fmt.Println(v) }
从生成的汇编代码可以看到:
CALL runtime.assertE2I2(SB)
上面这个assertE2I2是在runtime\iface.go下面,我们找一个相关的函数看一下
func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {
t := e._type
if t == nil {
return
}
tab := getitab(inter, t, true)
if tab == nil {
return
}
r.tab = tab
r.data = e.data
b = true
return
}
上面这个函数式用来判断eface是否是满足一个interfacetype中的接口函数要求。
interface中一个坑的解释
如果Stringer是一个interface,那么
var a interface{} 这个地方用if a == nil是可以判断的
但是如果使用其他类型的nil指针赋给interface
var b *Binary = nil var a interface{} = b p(a)
if a == nil这个时候就不会成立的,即使b为nil,这是因为这个时候a的结构如下:
所以这个时候你把一个其他类型的nil指针赋值给interface{}的时候,interface并不是空,而是对interface进行了初始化,只不过data是nil而已
我们可以从生成的汇编代码来看LEAQ type.*"".Binary(SB), AX MOVQ AX, (SP) MOVQ $0, 8(SP) PCDATA $0, $0 CALL "".p(SB)上面并没有出现ConvT2E之类的函数调用,但是有一个取type的地址,从上面Interface赋值的过程来看,对于这种nil指针是有优化的。
所有如果一个参数的返回值是interface{}或者其他有方法的interface,比如error等,所以一旦函数返回的是某种interface的时候,就需要注意,不要直接返回某种类型的空指针,需要转换成直接的nil进行赋值。
- 点赞
- 收藏
- 关注作者
评论(0)