Golang接口底层源码解析(一):iface与eface

125 阅读4分钟

一、Golang接口(C++ 显式声明,Golang 隐式实现)

与C++的显式继承不同,Golang采用非侵入式接口设计,实现了独特的类型系统:

// C++显式继承示例(伪代码)
class Duck : public AnimalInterface { /* 必须显式声明继承关系 */ };

// Golang隐式实现
type Duck struct{} 
func (d Duck) Quack() {} // 自动实现Animal接口

C++接口需要显式声明继承关系,只需要实现接口里定义的函数,编译器就会自动识别成接口类型。 Golang,只要一个类型实现了接口里的方法,编译器就会判断他实现了接口

核心机制:鸭子类型(Duck Typing)

  • 当类型满足接口的所有方法签名时,自动实现接口
  • 类型与接口的耦合发生在方法层面,而非类型声明层面

这其实就是Golang里的鸭子类型,如果一个东西,走起来像鸭子,叫起来像鸭子,满足了鸭子的各种行为,那他就是鸭子。

其本质是如果一个对象有我们需要的方法和属性,不管它是来自于哪,从哪继承的,它就是我们需要的“类型”

二、接口底层实现机制

Golang底层会用两个结构体去描述一个接口,并基于此实现了多态。分别是iface和eface。

iface: iface实现了golang接口的多态,使得程序可以在运行时,根据具体类型来调用对应方法。(类型可以是结构体,也可以是自定义类型,如type MyInt int)

eface: eface实现了golang里的interface{}(空接口),也就是any,空接口不包含任何方法。主要作用是在运行时管理和存储这些值。

2.1 iface的组成部分

iface 由itab (接口信息 + 具体类型信息) 和 (data 类型地址)组成

1. tab *itab
  • 功能概述tab 是一个指向 itab 结构体的指针。itab 结构体用于存储接口类型和具体实现类型之间的映射信息,它包含了接口的元数据、具体类型的元数据以及方法地址表等。通过iface绑定到了实体类型,实现了多态。
iface的定义
type iface struct {
    tab *itab // 描述接口对应,方法的地址
    data unsafe.Pointer // 指向接口定义的内存地址,一般在堆上
}

type itab struct {
    inter *interfacetype //描述接口的类型
    _type *_type
    link *itab
    hash uint32 // copy of _type.hash. Used for type switches.
    _ [4]byte
    fun [1]uintptr // variable sized
}
  • itab 结构体详细内容

    • inter *interfacetype:指向接口信息,包含接口名称、接口定义的方法列表等,用于在运行时识别接口的定义
    • _type *_type:指向具体类型,它保存了具体类型的详细信息,如类型的名称、内存布局等。
    • hash uint32:具体类型的哈希值,用于快速比较和查找。
    • fun [1]uintptr:方法地址表,存储了具体类型实现接口方法的函数地址。虽然它的长度只有1,但是实际查找会根据偏移量来访问后续的函数地址。
2. data unsafe.Pointer
  • 功能概述data 是一个通用指针,它指向具体类型的值。当一个具体类型的值被赋值给接口类型变量时,data 指针就会指向这个具体类型的值在内存中的地址。通过这个指针,在调用接口方法时可以访问到具体类型的值,从而
eface的定义
type eface struct {
    _type *_type
    data unsafe.Pointer
}
  • eface结构体详细内容
    • _type *_type:指向具体类型,它保存了具体类型的详细信息,如类型的名称、内存布局等。
    • data unsafe.Pointer 指向接口定义的内存地址,一般在堆上

三、方法接收者类型选择指南

首先,我们先看下,什么是值接收,什么是指针接收 接收者类型直接影响接口实现:

值接收者 vs 指针接收者

type Speaker interface{ Speak() }

// 值接收者实现
type Cat struct{}
func (c Cat) Speak() { fmt.Println("Meow") }

// 指针接收者实现
type Dog struct{}
func (d *Dog) Speak() { fmt.Println("Woof") }

func main() {
    var s Speaker
    
    c := Cat{}
    s = c        // 合法
    s = &c       // 也合法(自动取引用)
    
    d := Dog{}
    s = d        // 非法!需要指针接收者
    s = &d       // 合法
}

选择策略

接收者类型适用场景内存表现
值接收者不可变对象每次赋值产生副本
指针接收者需状态维护共享底层数据

四、接口设计最佳实践-总结

  1. 定义最小接口:遵循接口隔离原则
  2. 避免空接口滥用:明确类型约束
  3. 接口组合艺术: 接口的垂直组合和水平组合,可参考另一篇文章,juejin.cn/post/747301…
type Reader interface{ Read() }
type Writer interface{ Write() }
type ReadWriter interface {
    Reader
    Writer
}
  1. 规范性检查
var _ Speaker = (*Dog)(nil) // 编译时接口实现检查