概述
GoGoGoGogomockinterfaceGoGo

隐身接口

JavaJavaJava
public interface MyInterface {
    public String hello = "Hello";
    public void sayHello();
}
sayHellohelloMyInterfaceImplMyInterface
public class MyInterfaceImpl implements MyInterface {
    public void sayHello() {
        System.out.println(MyInterface.hello);
    }
}
JavaGo
GointerfaceGo
type Animal interface {
	Say() string
	Name() string
}
AnimalSay() stringName() stringDuck
type Duck struct {
	Name  string
	Sound string
}

func (a *Duck) MySay() string {
	return fmt.Sprintf("My Sound is: %s", a.Sound)
}

func (a *Duck) MyName() string {
	return fmt.Sprintf("My Name is: %s", a.Name)
}
AnimalMySay() stringName() stringAnimal

指针和结构体接收者

我们经常能看到两种实现接口的接收方式:指针和结构体,看下面缩略代码:

type Animal interface {
	MySay() string
	MyName() string
}

type Duck struct {...}

//指针方式
func (a *Duck) MySay() string {...}
func (a *Duck) MyName() string {...}

//结构体方式
func (a Duck) MySay() string {...}
func (a Duck) MyName() string {...}
Gomethod redeclared

实现接口的类型和初始化返回的类型两个维度共组成了四种情况,然而这四种情况不是都能通过编译器的检查:

结构体接收者- func (a Duck) MySay()指针接收者 - func (a *Duck) MySay()
结构体指针方式初始化
var Duck Animal = &Duck{}
通过检查通过检查
结构体方式初始化
var Duck Animal = Duck{}
通过检查不通过
Duck{}&Duck{}MySay()
&Duck{}&Duck{}dereferenceDuck{}MySayDuck{}*Cat

总结起来:当我们使用指针实现接口时,只有指针类型的变量才会实现该接口;当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口。

接口嵌套

Go

一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用。

GoioWriterCloserWriteCloser
type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

type WriteCloser interface {
    Writer
    Closer
}
io.Writerio.Closerio.WriteCloserio.Writerio.Closerio.WriteCloserio.Writerio.Closer
package main
import (
    "io"
)
// 声明一个设备结构
type device struct {
}
// 实现io.Writer的Write()方法
func (d *device) Write(p []byte) (n int, err error) {
    return 0, nil
}
// 实现io.Closer的Close()方法
func (d *device) Close() error {
    return nil
}
func main() {
    // 声明写入关闭器, 并赋予device的实例
    var wc io.WriteCloser = new(device)
    // 写入数据
    wc.Write(nil)
    // 关闭设备
    wc.Close()
    // 声明写入器, 并赋予device的新实例
    var writeOnly io.Writer = new(device)
    // 写入数据
    writeOnly.Write(nil)
}

nil 与 non-nil

Go 语言的接口类型不是任意类型

main*NilStructniltnil
package main

import "fmt"

type NilStruct struct {
}

func NilOrNot(v interface{}) bool {
	if v == nil {
		return true
	}
	return false
}

func main() {
	var t *NilStruct
	fmt.Println(t == nil)
	fmt.Println(NilOrNot(t))
}

$ go run main.go
true
false

我们简单总结一下上述代码执行的结果:

niltrueNilOrNotnilfalse
NilOrNot*NilStructinterface{}NilStructnil
数据结构

类型

GoType
Gointfloat64boolstringstructinterfacetype
src/runtime/type.go_typeHeader
//go 1.20.3 path: /src/runtime/type.go
type _type struct {
	size       uintptr                                   //表示类型的大小,即占用内存的字节数
	ptrdata    uintptr                                   //类型中指针数据的大小,以字节为单位。这个字段用于垃圾回收器识别类型中哪些部分是指针,哪些部分是非指针
	hash       uint32                                    //类型的哈希值,用于在运行时比较两个类型是否相同
	tflag      tflag                                     //类型的标志位,用于存储一些额外的信息,如是否有名称、是否有不可比较的字段等
	align      uint8                                     //类型的对齐方式,以字节为单位。这个字段决定了类型在内存中的布局和对齐方式
	fieldAlign uint8                                     //类型的字段对齐方式,以字节为单位。这个字段决定了类型中的字段在内存中的布局和对齐方式
	kind       uint8                                     //类型的种类,用于区分不同的基本类型,如 int、string、struct 等
	equal      func(unsafe.Pointer, unsafe.Pointer) bool //类型的相等性函数,用于在运行时比较两个值是否相等。如果为 nil,则表示该类型没有定义相等性函数,或者该类型是不可比较的
	gcdata     *byte                                     //类型的垃圾回收数据,用于存储一些与垃圾回收相关的信息,如指针位图等
	str        nameOff                                   //类型的名称偏移量,用于在运行时获取类型的名称。这个字段是一个相对于 _type 结构体起始地址的偏移量,可以通过它找到一个 nameOff 结构体,进而找到一个 name 结构体,其中存储了类型的名称
	ptrToThis  typeOff                                   //类型的指针偏移量,用于在运行时获取指向该类型的指针类型。这个字段也是一个相对于 _type 结构体起始地址的偏移量,可以通过它找到一个 typeOff 结构体,进而找到一个 _type 结构体,其中存储了指向该类型的指针类型
}
_typeGo

有个别字段需要详细说明下,其他字段直接备注:

const (
	kindBool = 1 + iota
	kindInt
	kindInt8
	kindInt16
	kindInt32
	kindInt64
	kindUint
	kindUint8
	kindUint16
	kindUint32
	kindUint64
	kindUintptr
	kindFloat32
	kindFloat64
	kindComplex64
	kindComplex128
	kindArray
	kindChan
	kindFunc
	kindInterface
	kindMap
	kindPtr
	kindSlice
	kindString
	kindStruct
	kindUnsafePointer
	kindDirectIface = 1 << 5
	kindGCProg      = 1 << 6
	kindMask        = (1 << 5) - 1
)
intstringbool_typearraychanslicefuncgolang
runtime.type_type
//go 1.20.3 path: /src/runtime/type.go

//用于表示数组类型
type arraytype struct {
	typ   _type     // 类型描述符
	elem  *_type    // 数组元素类型的指针
	slice *_type    // 切片类型的指针
	len   uintptr   // 数组长度
}

//用于表示通道类型
type chantype struct {
	typ  _type    // 类型描述符
	elem *_type   // 通道元素类型的指针
	dir  uintptr  // 通道的方向
}

//表示切片类型
type slicetype struct {
	typ  _type    // 类型描述符
	elem *_type   // 切片元素类型的指针
}

//用于表示函数类型
type functype struct {
	typ      _type    // 类型描述符
	inCount  uint16   // 输入参数数量
	outCount uint16   // 输出参数数量
}

//用于表示指针类型
type ptrtype struct {
	typ  _type    // 类型描述符
	elem *_type   // 指针指向的类型
}

//用于表示结构体类型
type structtype struct {
	typ     _type
	pkgPath name          // 结构体所属包的路径
	fields  []structfield //结构体的字段列表
}
go
uncommontypeuncommontype
//自定义类型元数据
type uncommontype struct {
	pkgpath nameOff
	mcount  uint16 // number of methods
	xcount  uint16 // number of exported methods
	moff    uint32 // offset from this uncommontype to [mcount]method
	_       uint32 // unused
}
uncommontype
[]stringmysliceLenCap
mysliceslicetypeuncommontypeuncommontypemyslice

接口实现

golang
runtime.ifaceruntime.efaceinterface{}

eface — 空Interface

eface
//go1.20.1  path: src/runtime/runtime2.go
type eface struct {
	_type *_type         //类型元数据
	data  unsafe.Pointer //数据信息,指向数据指针
}
eface
 _type.text.rodatadata

举个例子:

package main

import "fmt"

//Student结构体
type Student struct {
	name string
}

//Student方法setName
func (s *Student) setName(name string) {
	s.name = name
}

//Student方法getName
func (s *Student) getName() string {
	return s.name
}

func main() {
	//声明一个空接口变量a
	var a interface{}
	s := &Student{"Jack"}
	a = s
	fmt.Println(a)
}
Studentsaa
gdb
(gdb) info locals
a = {_type = 0x10c0980 <type:*+46208>, data = 0xc000014270}
s = 0xc000014270
interface

iface — 非空Interface

runtime.iface
//go1.20.1  path: src/runtime/runtime2.go
type iface struct {
	tab  *itab
	data unsafe.Pointer  //指向原始数据指针
}

同样包含两个字段:

//go1.20.1  path: src/runtime/runtime2.go
type itab struct {
	inter *interfacetype // 接口自身定义的类型信息,用于定位到具体interface类型
	_type *_type         // 接口的具体类型,指向实际对象类型
	hash  uint32         //_type.hash的拷贝,用于快速查询和判断目标类型和接口中类型是一致
	_     [4]byte        // 填充字段,保证对齐用
	fun   [1]uintptr     //动态数组,接口方法实现列表(方法集),即函数地址列表,按字典序排序,如果数组中的内容为空表示 _type 没有实现 inter 接口
}
type interfacetype struct {
	typ     _type	     //接口类型
	pkgpath name       //包路径
	mhdr    []imethod  //接口中的方法表
}
//接口类型的方法
type imethod struct {
	name nameOff  // 方法名称在名称表中的偏移量
	ityp typeOff  // 方法类型在类型表中的偏移量
}

//非接口类型的方法,它是一个压缩格式的结构,每个字段的值都是一个相对偏移量
type method struct {
	name nameOff  // 方法名称在名称表中的偏移量
	mtyp typeOff  // 方法类型在类型表中的偏移量
	ifn  textOff  // 接口方法的实现函数在代码段中的偏移量
	tfn  textOff  // 普通方法的实现函数在代码段中的偏移量
}

下面我们看一个例子,代码如下:

package main

import (
	"fmt"
)

type Square interface {
	Area() float64
	Perimeter() float64
}

type Sdata struct {
	x, y float64
}

func (s *Sdata) Area() float64 {
	return s.x * s.y
}

func (s *Sdata) Perimeter() float64 {
	return (s.x + s.y) * 2
}

func NewSdata(x, y float64) *Sdata {
	return &Sdata{
		x: x,
		y: y,
	}
}

func main() {
	var s Square
	Object := NewSdata(1, 2)
	s = Object
	fmt.Println(s)
}
gdbs
-----------  赋值前 ---------------
(gdb) p s
$2 = {tab = 0x0, data = 0x0}

-----------  赋值后 ---------------
(gdb) p s
$1 = {tab = 0x10dd9b8 <go:itab.*main.Sdata,main.Square>, data = 0xc0000b4010}
(gdb) ptype s
type = struct runtime.iface {
    runtime.itab *tab;
    void *data;
}
(gdb) p Object
$2 = (main.Sdata *) 0xc0000b4010
s{tab = 0x0, data = 0x0}{tab = 0x10dd9b8 , data = 0xc0000b4010}
itab

itab结构

itab缓存(itabTable)

itabitab.interitab._typeitabitabitabTableitabTable
const itabInitSize = 512
type itabTableType struct {
	size    uintptr             // length of entries array. Always a power of 2.
	count   uintptr             // current number of filled entries.
	entries [itabInitSize]*itab // really [size] large
}
entriesentries*itabitabInitSizeitab

itab初始化

schedinititabsinit
func schedinit() {
    ......
    itabsinit()     // uses activeModules
    ......
}

func itabsinit() {
	lockInit(&itabLock, lockRankItab)
	lock(&itabLock)
	for _, md := range activeModules() {
		for _, i := range md.itablinks {
			itabAdd(i)
		}
	}
	unlock(&itabLock)
}
itabsinit()itabTable

itabAdd

itabAdd()itabitabTable
const itabInitSize = 512

var (
  ......
	itabTable     = &itabTableInit                    
	itabTableInit = itabTableType{size: itabInitSize}
)


//itabAdd将给定的itab添加到itab哈希表中
func itabAdd(m *itab) {

	// 检查是否存在 malloc 死锁
	if getg().m.mallocing != 0 {
		throw("malloc deadlock")
	}

	t := itabTable
	if t.count >= 3*(t.size/4) { //当itab哈希表中使用率超过75%时,需要扩容处理

		//以原来2倍的空间去申请新的itab哈希表空间
		t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true))
		t2.size = t.size * 2

		//将旧的itab哈希表数据复制到新的itab哈希表中
		iterate_itabs(t2.add)
		if t2.count != t.count {
			throw("mismatched count during itab table copy")
		}

		//发布新的哈希表。使用原子写入
		atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
		//采用新表作为作为当前的itab表
		t = itabTable
	}
	//将m添加进itab表
	t.add(m)
}

// add将给定的itab添加到itab表t中
func (t *itabTableType) add(m *itab) {

	/**
	通过与运算法来确定新itab在itab表中的index
	与运算法公式:index = hash&(m-1) 其中m-1为数组长度减1,具体参考hashmap章节
	*/
	mask := t.size - 1 //掩码,用于计算哈希值对应的索引
	h := itabHashFunc(m.inter, m._type) & mask // 获取当前索引位置的条目

	for i := uintptr(1); ; i++ {
		//获取h位置地址
		p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
		//取值
		m2 := *p
		//如果m信息已经存在,直接返回
		if m2 == m {
			return
		}
		//如果找到的位置为空位置,则使用原子插入
		if m2 == nil {
			atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m))
			//使用值+1
			t.count++
			return
		}
		/**
		如果查找的位置已经存在非m值的其余itab信息,则使用开放寻址法,进行重新寻找位置
		开放寻址法公式为: h(i) = h0 + i*(i+1)/2 mod 2^k
		*/
		h += i
		h &= mask
	}
}
itabAdd() 
func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
	return uintptr(inter.typ.hash ^ typ.hash)
}

getitab & find

getitab()intertypitabTableitab
//根据非空的接口类型和动态类型获取itab内容
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {

	//如果inter.mhdr为空,则表示该接口没有方法和元数据,为空接口,不存在itab
	if len(inter.mhdr) == 0 {
		throw("internal error - misuse of itab")
	}

	/**
	简单情况
	tflagUncommon 标识是否有uncommon内容,即记录pkgpath和方法的内容
	目前看来只有匿名结构体或者reflect动态创建的struct没有methods时,该值为0
	大致猜测意思就是当前查询的动态类型没有methods时,则直接返回nil
	*/
	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
	/**
	首先,查看现有表以查看是否可以找到所需的itab。
	这是迄今为止最常见的情况,因此请不要使用锁。
	使用atomic确保我们看到该线程完成的所有先前写入更新itabTable字段(在itabAdd中使用atomic.Storep)。
	*/
	t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
	if m = t.find(inter, typ); m != nil {
		goto finish
	}

	// 如果任然没有找到数据,则加锁重试
	lock(&itabLock)
	if m = itabTable.find(inter, typ); m != nil {
		unlock(&itabLock)
		goto finish
	}

	// 如果条目不存在,则进行输入并添加
	m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
	m.inter = inter
	m._type = typ
	m.hash = 0
	m.init()
	itabAdd(m)
	unlock(&itabLock)
finish:
	//如果非m.fun[0] == 0,则表明有方法被实现,返回m
	if m.fun[0] != 0 {
		return m
	}
	if canfail {
		return nil
	}
	//如果数据类型并没有实现接口,那么根据调用方式,该报错报错,该panic panic。
	panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}


//根据信息查找itab信息
func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab {
	/**
	通过与运算法来确定在itabTable中的index
	与运算法公式:index = hash&(m-1) 其中m-1为数组长度减1,具体参考hashmap章节
	hash函数为:itabHashFunc
	*/
	mask := t.size - 1
	h := itabHashFunc(inter, typ) & mask
	for i := uintptr(1); ; i++ {
		p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
		// 在这里使用atomic read,所以如果我们看到m!= nil,我们也会看到m字段的初始化
		// m := *p
		m := (*itab)(atomic.Loadp(unsafe.Pointer(p)))
		if m == nil {
			return nil
		}
		if m.inter == inter && m._type == typ {
			return m
		}
		/**
		使用二次探测
		探测公式为h(i)= h0 + i *(i + 1)/ 2 mod 2 ^ k
		*/
		h += i
		h &= mask
	}
}

总结下流程:

titabTablet.finditabTableitabTable.finditabTableitaTableitabitabTablehashpanicpanicitabunsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSizelen(inter.mhdr)fun1len(inter.mhdr)-1fun

init

itab.init()itabgetitab()
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    ......
    m.inter = inter
	  m._type = typ
	  m.hash = 0
	  m.init()
    itabAdd(m)
    ......
}
itabitabTableitabAddinit 
//go 1.20.3 path: /src/runtime/iface.go

//初始化itab,填充itab.fun数组
func (m *itab) init() string {
	//赋值接口类型
	inter := m.inter
	//赋值动态类型
	typ := m._type
	//返回组合的uncommontype类型
	x := typ.uncommon()

	ni := len(inter.mhdr) //接口类型的方法数量
	nt := int(x.mcount)   //具体类型的方法数量
	//指向具体类型的方法数组
	xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
	j := 0
	//指向具体itab.fun
	methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni]
	var fun0 unsafe.Pointer
imethods:
	// 这里看似双重遍历,由于方法数组都是有序的,所以其实时间复杂度为ni+nt, 而不是ni*nt
	for k := 0; k < ni; k++ { //遍历接口方法
		i := &inter.mhdr[k]                //获取接口方法i
		itype := inter.typ.typeOff(i.ityp) //i方法type
		name := inter.typ.nameOff(i.name)  //i方法name字段
		iname := name.name()               // name转化为string
		ipkg := name.pkgPath()             // 路径转为string
		if ipkg == "" {
			ipkg = inter.pkgpath.name()
		}
		for ; j < nt; j++ { // 遍历具体类型的方法
			t := &xmhdr[j]
			tname := typ.nameOff(t.name)
			if typ.typeOff(t.mtyp) == itype && tname.name() == iname { //判等
				pkgPath := tname.pkgPath()
				if pkgPath == "" {
					pkgPath = typ.nameOff(x.pkgpath).name()
				}
				if tname.isExported() || pkgPath == ipkg {
					if m != nil {
						ifn := typ.textOff(t.ifn)
						if k == 0 {
							fun0 = ifn // we'll set m.fun[0] at the end
						} else {
							methods[k] = ifn
						}
					}
					continue imethods // 存在i方法就continue到外层for
				}
			}
		}
		m.fun[0] = 0 // 没有找到i方法, 将m.fun[0] 置为0
		return iname
	}
	m.fun[0] = uintptr(fun0)
	return ""
}
interfacetypetypeinterface

interface、type的方法都按字典序排,所以O(n+m)的时间复杂度可以匹配完

funtypetypeinterface

类型转换

指针类型

先直接上代码:

package main

type People interface {
	GetName() string
}

//go:noinline
func (s *Student) GetName() string {
	return s.name
}

type Student struct {
	name string
	age  int
}

func main() {
	var p People
	var s *Student = &Student{
		name: "XJX",
		age:  15,
	}
	p = s
	p.GetName()
}
go tool compile
go tool compile -N -l -S main.go

我们开始分析汇编代码:

0x0030 00048 (main.go:32)       LEAQ    type."".Student(SB), AX					;AX = &type."".Student
0x0037 00055 (main.go:32)       MOVQ    AX, (SP)								;SP = &type."".Student
0x003b 00059 (main.go:32)       PCDATA  $1, $0
0x003b 00059 (main.go:32)       NOP
0x0040 00064 (main.go:32)       CALL    runtime.newobject(SB)					;SP + 8 = &Student{}
0x0045 00069 (main.go:32)       MOVQ    8(SP), DI                               ;DI = &Student{}
0x004a 00074 (main.go:32)       MOVQ    DI, ""..autotmp_3+32(SP)                ;autotmp_3(32SP) = &Student{}
0x004f 00079 (main.go:33)       MOVQ    $3, 8(DI)                               ;StringHeader(DI.Name).Len = 3
0x0057 00087 (main.go:33)       PCDATA  $0, $-2
0x0057 00087 (main.go:33)       CMPL    runtime.writeBarrier(SB), $0
0x005e 00094 (main.go:33)       NOP
0x0060 00096 (main.go:33)       JEQ     100
0x0062 00098 (main.go:33)       JMP     191
0x0064 00100 (main.go:33)       LEAQ    go.string."XJX"(SB), AX                 ;AX = &"XJX"
0x006b 00107 (main.go:33)       MOVQ    AX, (DI)                                ;StringHeader(DI.Name).Data = &"XJX"
0x006e 00110 (main.go:33)       JMP     112
0x0070 00112 (main.go:32)       PCDATA  $0, $-1
0x0070 00112 (main.go:32)       MOVQ    ""..autotmp_3+32(SP), AX                ;AX = &Student{}
0x0075 00117 (main.go:32)       TESTB   AL, (AX)
0x0077 00119 (main.go:34)       MOVQ    $15, 16(AX)                             ;DI.age = 15
0x007f 00127 (main.go:32)       MOVQ    ""..autotmp_3+32(SP), AX 				;AX = &Student{}
0x008e 00142 (main.go:36)       LEAQ    go.itab.*"".Student,"".People(SB), CX;AX = *itab(go.itab.*"".Student,"".People)
0x0095 00149 (main.go:36)       MOVQ    CX, "".p+48(SP)						;p(48SP) = *itab(go.itab.*"".Student,"".People)
0x009a 00154 (main.go:36)       MOVQ    AX, "".p+56(SP)						;p(56SP) = &Student{}
0x009f 00159 (main.go:37)       MOVQ    "".p+48(SP), AX					; AX = *itab(go.itab.*"".Student,"".People)
0x00a4 00164 (main.go:37)       TESTB   AL, (AX)
0x00a6 00166 (main.go:37)       MOVQ    24(AX), AX						; AX = *(GetName)
0x00aa 00170 (main.go:37)       MOVQ    "".p+56(SP), CX					; CX = &Student{}
0x00af 00175 (main.go:37)       MOVQ    CX, (SP)						; 移动CX到栈顶
0x00b3 00179 (main.go:37)       CALL    AX								;call GetName func

结构体类型

来看下结构体类型的代码:

package main

import "fmt"

type People interface {
	GetName() string
}

//go:noinline
func (s Student) GetName() string {
	return s.name
}

type Student struct {
	name string
	age  int
}

func main() {
	var p People
	var s Student = Student{
		name: "XJX",
		age:  15,
	}
	p = s
	p.GetName()
}

这边就不详细说了,基本跟指针类型差不多,但有几点差别:

//iface.go
func convTstring(val string) (x unsafe.Pointer) {
	if val == "" {
		x = unsafe.Pointer(&zeroVal[0])
	} else {
		x = mallocgc(unsafe.Sizeof(val), stringType, true)
		*(*string)(x) = val
	}
	return
}
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
	t := tab._type
	x := mallocgc(t.size, t, true)
	typedmemmove(t, x, elem)
	i.tab = tab
	i.data = x
	return
}
var p People = Student{
    name: "XJX",
    age:  15,
}
p.GetName()

类型断言

golanginterfaceinterface{}golanginterface{}specified interfacespecified interfacestruct
struct
GointerfaceGoInterfaceText
x := interfaceText.(T) //T是某一种类型,此方式断言失败会panic
or
x,err := interfaceText.(T)//T是某一种类型,此方式不会panic,错误信息会返回到err变量

上式是接口断言的一般形式,因为此方法不一定每次都可以完好运行,所以err的作用就是判断是否出错。所以一般接口断言常用以下写法:

if v,err:=InterfaceText.(T);err {//T是一种类型
    possess(v)//处理v
    return
}
vInterfaceTexterrtureerrfalse

我们知道接口分为空接口和非空接口,我们从两类接口开始分析。

空接口

先从例子出发,看一段空接口代码:

package main

import "fmt"

type Student struct {
	name string
}

type People interface {
	GetName() string
}

func (s *Student) GetName() string {
	return s.name
}

func main() {
	var s interface{} = &Student{name: "XJX"}
	v, ok := s.(int)
	if ok {
		fmt.Printf("%v\n", v)
	}
	switch s.(type) {
	case People:
	}
}

通过反编译查看源码:

go tool compile -N -l -S main.go

忽略其余代码我们从类型断言这里开始看起:

......
0x0070 00112 (main.go:17)       LEAQ    type.*"".Student(SB), CX   ;把eface _type的地址放入CX
0x0077 00119 (main.go:17)       MOVQ    CX, "".s+120(SP)		   ;把CX存放值赋值给s+120(SP)
0x007c 00124 (main.go:17)       MOVQ    AX, "".s+128(SP)		   ;把AX存放值赋值给s+128(SP)
0x0084 00132 (main.go:18)       MOVQ    "".s+120(SP), AX		   ;把s+120(SP)存放寄存器AX,即eface _type地址
0x0089 00137 (main.go:18)       MOVQ    "".s+128(SP), CX		   ;把s+128(SP)存放寄存器CX,即eface data地址
0x0091 00145 (main.go:18)       LEAQ    type.int(SB), DX           ;把int的类型type的地址放到 DX
0x0098 00152 (main.go:18)       CMPQ    DX, AX                     ;直接比较 AX DX的地址
......
.rodata_type_type

下面看看空接口对于接口断言的汇编:

0x01b2 00434 (main.go:30)       MOVL    AX, ""..autotmp_14+68(SP)
0x01b6 00438 (main.go:31)       MOVQ    ""..autotmp_12+168(SP), AX
0x01be 00446 (main.go:31)       MOVQ    ""..autotmp_12+176(SP), CX
0x01c6 00454 (main.go:31)       LEAQ    type."".People(SB), DX    ;获取People的类型元素存入DX
0x01cd 00461 (main.go:31)       MOVQ    DX, (SP)				  ;放入栈顶
0x01d1 00465 (main.go:31)       MOVQ    AX, 8(SP)				  ;放入eface
0x01d6 00470 (main.go:31)       MOVQ    CX, 16(SP)
0x01db 00475 (main.go:31)       PCDATA  $1, $0
0x01db 00475 (main.go:31)       NOP
0x01e0 00480 (main.go:31)       CALL    runtime.assertE2I2(SB)    ;判断发起调用
0x01e5 00485 (main.go:31)       MOVBLZX 40(SP), AX                ;bool值
0x01ea 00490 (main.go:31)       MOVB    AL, ""..autotmp_13+67(SP)

assertE2I2
func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {
    //获取eface的类型元数据
	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_typeinterfacetype 

非空接口

非空接口还是直接从例子开始看:

package main

import "fmt"

type People interface {
	GetName() string
}

type Student struct {
	name string
}

func (s *Student) GetName() string {
	return s.name
}

func main() {
	var p People = &Student{name: "XJX"}
	v, ok := p.(People)
	if ok {
		fmt.Printf("%v\n", v)
	}
	v, ok = p.(*Student)
	if ok {
		fmt.Printf("%v\n", v)
	}
}

执行命令

go tool compile -N -l -S main.go

来看下关键的代码汇编片段:

.......
0x00a1 00161 (main.go:26)	MOVUPS	X0, ""..autotmp_4+168(SP)
0x00a9 00169 (main.go:26)	MOVQ	"".p+136(SP), AX
0x00b1 00177 (main.go:26)	MOVQ	"".p+144(SP), CX
0x00b9 00185 (main.go:26)	LEAQ	type."".People(SB), DX       ;获取type People的类型元数据地址到 DX
0x00c0 00192 (main.go:26)	MOVQ	DX, (SP)                     ;DX存入栈底
0x00c4 00196 (main.go:26)	MOVQ	AX, 8(SP)                    ;iface存入8(SP)
0x00c9 00201 (main.go:26)	MOVQ	CX, 16(SP)
0x00ce 00206 (main.go:26)	PCDATA	$1, $1
0x00ce 00206 (main.go:26)	CALL	runtime.assertI2I2(SB)       ;调用方法进行类型断言
0x00d3 00211 (main.go:26)	MOVBLZX	40(SP), AX                   ;返回参数 ok bool值
0x00d8 00216 (main.go:26)	MOVQ	24(SP), CX                   ;新的iface
0x00dd 00221 (main.go:26)	MOVQ	32(SP), DX                   ;新的iface
....... 
runtime.assertI2I2
func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
	tab := i.tab //获取变量i的iface.tab
	if tab == nil {
		return
	}
	if tab.inter != inter { //如果i的face.tab跟inter不一致
		tab = getitab(inter, tab._type, true) //使用inter类型与tab._type具体类型查询itab
		if tab == nil { //如果查询不到,则表示断言失败
			return
		}
	}
    //断言成功 赋值
	r.tab = tab
	r.data = i.data
	b = true
	return
}

通过这样的方式我们就实现了接口类型断言。

我们再来看代码具体类型的断言这里和空接口的例子比较类似编译器也通过汇编代码优化进行了实现,没有去调用runtime函数:

....... 
0x0207 00519 (main.go:30)	MOVQ	$0, ""..autotmp_3+88(SP)
0x0210 00528 (main.go:30)	MOVQ	"".p+144(SP), AX                           ;放入具体的AX
0x0218 00536 (main.go:30)	LEAQ	go.itab.*"".Student,"".People(SB), CX      ;获取具体类型的itab的地址
0x021f 00543 (main.go:30)	NOP
0x0220 00544 (main.go:30)	CMPQ	"".p+136(SP), CX							;比较两个itab的地址进行类型断言
0x0228 00552 (main.go:30)	JEQ	559
0x022a 00554 (main.go:30)	JMP	871
....... 
p+144(SP) p+136(SP)pitab

最后用一张图来总结下类型断言:

runtime.assertE2I2
runtime.assertI2I2
runtime.assertE2I2runtime.assertI2I2itab

参考文档:

【新乐于心】https://www.zhihu.com/people/chen-qiang-song/posts

【draveness】 https://draveness.me/golang/

【幼麟实验室】https://space.bilibili.com/567195437