Golang原理剖析(interface)

文章目录

go语言并非传统意义上的面向对象的语言,他不像Java或者C++一样有类、继承等一些特性,但是我们也可以借助go语言中的struct和interface来实现这种面向对象的编程。

go语言中interface其实就是一组方法声明任何类型的对象实现了接口的全部方法就是这个接口的一个实现

interface的底层原理

空接口interface{}

没有定义任何方法的接口为空接口,空接口可以接收任意数据类型 ,就是说可以将任意类型的数据赋值给一个空接口,空接口的结构定义位于 src/runtime/runtime2.go 定义如下:

go 复制代码
type eface struct {
	_type *_type
	data  unsafe.Pointer
}

_type: 指向接口的动态类型元数据,即接口变量的类型

data: 指向接口的动态值,data是一个指向变量本身的指针

_type是什么

_type 是 go 里面所有类型的一个抽象,里面包含了类型的大小、哈希、对齐以及类型编号等信息,决定了data如何解析和操作。


Go语言中几乎所有的数据结构都可以抽象成 _type 。关于 _type 的定义在源文件 /usr/local/go/src/internal/abi/type.go 具体定义如下:

go 复制代码
// Type is the runtime representation of a Go type.
//
// Be careful about accessing this type at build time, as the version
// of this type in the compiler/linker may not have the same layout
// as the version in the target binary, due to pointer width
// differences and any experiments. Use cmd/compile/internal/rttype
// or the functions in compiletype.go to access this type instead.
// (TODO: this admonition applies to every type in this package.
// Put it in some shared location?)
type Type struct {
	Size_       uintptr	// 数据类型占用的空间大小
	// 前缀持有所有指针的内存大小
	PtrBytes    uintptr // number of (prefix) bytes in the type that can contain pointers
	// 类型的hash值
	Hash        uint32  // hash of type; avoids computation in hash tables
	// 信息标志
	TFlag       TFlag   // extra type information flags
	// 这种类型在内存中的对齐方式
	Align_      uint8   // alignment of variable with this type
	FieldAlign_ uint8   // alignment of struct field with this type
	// 类型编号
	Kind_       Kind    // enumeration for C
	// function for comparing objects of this type
	// (ptr to object A, ptr to object B) -> ==?
	// 类型的比较函数
	Equal func(unsafe.Pointer, unsafe.Pointer) bool
	// GCData stores the GC type data for the garbage collector.
	// Normally, GCData points to a bitmask that describes the
	// ptr/nonptr fields of the type. The bitmask will have at
	// least PtrBytes/ptrSize bits.
	// If the TFlagGCMaskOnDemand bit is set, GCData is instead a
	// **byte and the pointer to the bitmask is one dereference away.
	// The runtime will build the bitmask if needed.
	// (See runtime/type.go:getGCMask.)
	// Note: multiple types may have the same value of GCData,
	// including when TFlagGCMaskOnDemand is set. The types will, of course,
	// have the same pointer layout (but not necessarily the same size).
	GCData    *byte
	Str       NameOff // string form
	PtrToThis TypeOff // type for pointer to this type, may be zero
}

type _type 变成了 src/internal/abi/type.go 中 type Type struct 这个结构体的别名,且所有成员全部变为 public

什么是动态类型和动态值呢,举个例子

go 复制代码
package main

import "fmt"

type Apple struct {
    PhoneName string
}

func main() {

    a := Apple{PhoneName: "apple"}
    var efc interface{}
    efc = a
    fmt.Println(efc)
}

这里在var efc interface{}定义了一个接口类型实例efc,此时还未对efc赋值,它的结构如下图所示:

在efc = a,对efc赋值了一个Apple类型的变量之后,其底层结构表现如下图所示:

其中_type指针指向a变量的类型元数据,data指针指向a变量的值​

非空接口​

包含方法列表的接口就是非空接口,例如下面定义的接口Phone就是一个非空接口:

go 复制代码
type Phone interface {
    Call()
}

非空接口的底层实现和空接口有所不同,因为其多了方法列表,在底层实现中显然我们需要有地方来存储方法列表,非空接口的结构定义位于src/runtime/runtime2.go,定义如下:

go 复制代码
type iface struct {
	tab  *itab
	data unsafe.Pointer
}

tab:指向一个itab的结构,itab结构里面存储值接口要求的方法列表和 data对应动态类型信息​

data:指向接口的动态值,这里跟空接口一样​

下面看一下itab的结构定义,itab结构定义在src/runtime/runtime2.go,定义如下:

go 复制代码
type itab = abi.ITab
// The first word of every non-empty interface type contains an *ITab.
// It records the underlying concrete type (Type), the interface type it
// is implementing (Inter), and some ancillary information.
//
// allocated in non-garbage-collected memory
type ITab struct {
	Inter *InterfaceType
	Type  *Type
	Hash  uint32     // copy of Type.Hash. Used for type switches.
	Fun   [1]uintptr // variable sized. fun[0]==0 means Type does not implement Inter.
}

inter:指向interfacetype结构的指针,interfacetype结构记录了这个接口类型的描述信息,主要是接口的方法列表

_type:实际类型的指针,指向_type结构,_type结构保存了接口的动态类型信息 ,跟空接口的_type一样,即赋值给这个接口的具体类型信息的元数据

hash:该类型的hash值,itab中的hash和itab._type中的hash相等,其实是从itab._type中拷贝出来的,目的是用于快速判断类型是否相等。它就是把 Type.Hash 复制到 itab.Hash,做"局部缓存",让 hot path 更快。【hot path = 跑得最勤的那条路,优化它最值。】

Fun:Fun是一个指针数组,里面保存了实现该接口的实际类型(只包含接口中的方法)地址 ,这些方法地址实际上是从interfacetype结构中的mhdr拷贝出来的,为了在调用的时候快速定位到方法。如果该接口对应的动态类型没有实现接口的所有方法,那么itab.Fun[0]=0,表示断言失败,该类型不能赋值给该接口。运行时,Fun长度会动态分配。

是 runtime 用变长结构体(尾部数组 / flexible array member) 的玩法:

interfacetype 保存了接口自身的元信息,下面看一下interfacetype结构

go 复制代码
type InterfaceType struct {
	Type		// 类型信息
	// 包路径
	PkgPath Name      // import path
	// 接口的方法列表
	Methods []Imethod // sorted by hash
}

这里主要关注的是Methods 这个字段,定义的接口的方法里表就保存在Methods 数组里

下面还是通过例子看一下,赋值一个非空接口对应的底层结构变化

go 复制代码
package main

import "fmt"

type Apple struct {
	PhoneName string
}

func (a Apple) Call() {
	fmt.Printf("%s有打电话功能\n", a.PhoneName)
}

func (a Apple) SendMessage() {
	fmt.Printf("%s有发短信功能\n", a.PhoneName)
}

func (a Apple) SendEmail() {
	fmt.Printf("%s有发邮件功能\n", a.PhoneName)
}

type Phone interface {
	Call()
	SendMessage()
}

func main() {
	a := Apple{PhoneName: "apple"}
	var ifc Phone
	ifc = a
	fmt.Println(ifc)
}
go 复制代码
root@GoLang:~/proj/goforjob# go run main.go 
{apple}

在程序var ifc Phone,赋值之前,ifc的结构如下图所示:

在ifc = a,给ifc赋值一个包含方法的结构体a之后,ifc的结构如下图:

赋值过程中,data指针其实还是和空接口一样指向具体类型值,这里指向变量a。tab指针则是指向itab这个结构体,itab结构创建的创建主要分为3部分:

  1. _type字段保存接口的动态类型信息,本例中,_type指针指向Apple类型的元数据

  2. inter保存接口自身的一些信息,这里重要处理方法列表,本质上其实是求接口类型(Phone)和具体类型(Apple)的方法列表的交集,将具体类型(Apple)这部分交集的方法地址也保存到interfacetype的mhdr数组中,假设具体类型(Apple)没有实现接口(Phone),那么这里mhdr数组将不包含任何方法的指针

  3. 最后再将mhdr数组中的方法地址拷贝到itab的fun数组中,方便调用方法的时候快速找到方法地址,如果具体类型(Apple)没有实现接口(Phone),那么这里itab.fun[0]=0

itab缓存

在给一个非空接口赋值的时候,itab里面主要是保存具体类型的类型元数据和方法列表,但是我们在给接口赋值的时候,我们可以赋值多个类型相同的动态类型,比如我们可以有如下代码:

go 复制代码
var ifc Phone
a := Apple{PhoneName: "apple1"}
b := Apple{PhoneName: "apple2"}
c := Apple{PhoneName: "apple3"}
ifc = a
ifc = b
ifc = c

同类型的接口多次赋值,虽然具体类型的值不同,但是他们的类型相同,方法列表也相同,显然这个itab结构体是可以被复用的,如果我们每次都创建一个新的itab的话,性能无疑会大大下降。所以可以把用到的itab结构体缓存起来,每个非空的interface的接口类型和具体类型就可以唯一确定一个类型的itab

同类型反复赋值给同一个非空接口,itab 会缓存复用;变的是 data,不变的是 tab。

itabTable

go语言采用itabTable这个结构来缓存所有的itab结构,itabTable的结构定义在 src/runtime/iface.go,定义如下:

go 复制代码
// Note: change the formula in the mallocgc call in itabAdd if you change these fields.
type itabTableType struct {
	// entries数组的长度
	size    uintptr             // length of entries array. Always a power of 2.
	// 当前数组中实际itab的数量
	count   uintptr             // current number of filled entries.
	// 数组,可以当做hash的思想(哈希表)
	entries [itabInitSize]*itab // really [size] large
}

itabTable实际用来存储itab结构的其实是这个entries结构,entries 是一个hash表,key为接口类型与实际类型分别哈希后的异或值

go 复制代码
func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
	// compiler has provided some good hash codes for us.
	return uintptr(inter.Type.Hash ^ typ.Hash)
}

我们在查找一个itab是否存在的时候,

  1. 先计算接口类型的哈希值hash1和实际类型的哈希值hash2

  2. hash1与hash2做异或运算得到最终哈希值hash

  3. entries数组中找到下标为hash的位置

  4. 如果能查询到对应的itab指针(这里需要比较接口类型和实际类型,因为可能出现产生hash冲突,槽位被占用的情况),就直接拿来使用。若没有就要再创建,然后添加到itabTable中。

对于hash冲突问题,采用的是开放地址法,根据计算的空位发现槽位被占用,则采用二次寻址法在数组后面寻找空位插入

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

相关推荐
冬奇Lab2 小时前
【Kotlin系列09】委托机制与属性委托实战:组合优于继承的最佳实践
android·开发语言·kotlin
txinyu的博客2 小时前
手写 C++ 高性能 Reactor 网络服务器
服务器·网络·c++
Vallelonga2 小时前
浅谈 Rust bindgen 工具
开发语言·rust
ElfBoard2 小时前
ElfBoard技术贴|如何在ELF-RK3506开发板上构建AI编程环境
c语言·开发语言·单片机·嵌入式硬件·智能路由器·ai编程·嵌入式开发
洲星河ZXH2 小时前
Java,泛型
java·开发语言·windows
木木木一2 小时前
Rust学习记录--C13 Part1 闭包和迭代器
开发语言·学习·rust
木木木一2 小时前
Rust学习记录--C13 Part2 闭包和迭代器
开发语言·学习·rust
Wcy30765190662 小时前
文件包含漏洞及PHP伪协议
开发语言·php
CopyProfessor2 小时前
Java Agent 入门项目模板(含代码 + 配置 + 说明)
java·开发语言