什么是Go的接口(二)

前言

本文会继续叙述Go接口的特性,会侧重于使用示例来描述接口的使用。

接口的组成

接口底层:itab+data

从实现的角度来看,接口的值本质是一个二元组:

  • itab:描述"这个接口值的动态类型是谁,以及它具体如何实现接口方法"
  • data:指向赋值接口的数据的值的指针
go 复制代码
type iface struct {
    tab  *itab
    data unsafe.Pointer
}
//空接口interface{}(也就是 any)
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

简单来看就是空接口无需带有itab,因为它没有方法集,只需要通过Type知道赋值的具体类型和data指针即可

itab的作用

itab就是"具体类型 + 接口类型的一份运行时产生的适配信息"。

记录接口具体类型信息

它记录了接口赋值的类型,比如:

  • User
  • *User
  • *bytes.Buffer
  • ...

接口的动态类型决定了:

  • 能不能做类型断言
  • 能不能调用方法
  • 反射时可以得到什么
保存方法表,用于动态派发
go 复制代码
var s Speaker = Person{}
s.Speak()

当真正执行接口的方法时,真实执行的函数地址来自具体赋值类型的真实函数地址,也就是Person的Speak()函数地址,而这个真实函数地址会被记录到itab的Fun中。

缓存"类型是否实现接口"

当给接口赋值具体类型时,运行时需要检查:

  • 这个类型是否实现了这个接口
  • 实现接口后,构造查找itab

这个缓存会被以接口类型+具体类型为Key值全局缓存,通过具体方法实现快速检查是否实现接口以及构建itab

itab快速索引:getitab

getitab就是通过全局缓存快速检查实现和查找itab的逻辑,具体可以总结为:

  1. 根据接口类型和具体类型组成一个key
  2. 在运行时维护这个itab哈希表,并在其中查找itab
  3. 如果存在则直接返回
  4. 如果不存在则检查接口类型和具体类型的方法集是否一致,也就是检查具体类型是否实现该接口
  5. 若实现则构建一个新的itab和对应的Key存入哈希表
  6. 若未实现则panic或者编译器报错

简单总结getitab的意义是:

  • 避免每次接口赋值都重新做完整的方法匹配
  • 通过缓存来实现高效率的接口转换和动态调用

接口特性

值接收者和指针接收者方法的区别

它们的区别在于本质和对应方法集。

对于类型T:

  • T的方法集:只包含 值接收者 方法
  • *T的方法集:包含值接收者 和 指针接收者方法

所以:

  • 如果接口要求方法是值接收者实现,T和*T都实现接口
  • 如果接口要求只有指针接收者实现,只有*T实现接口,T不实现接口。

这里提一个小点:对于定义的类型T,如果对指针接收者方法M(),执行T.M()时编译器会自动转换,加入&(取地符)来调用这个方法,但是本质的方法集不会有变化,所以T的方法集不带有指针接收者方法。

例子1:

go 复制代码
package main

import "fmt"

type Describer interface {
	Describe()
}

type User struct {
	Name string
}

// 值接收者
func (u User) Describe() {
	fmt.Println("user:", u.Name)
}

func main() {
	var d1 Describer = User{Name: "Alice"}
	d1.Describe()

	var d2 Describer = &User{Name: "Bob"}
	d2.Describe()
}

输出:

user: Alice

user: Bob

说明T可以被允许调用值接收者方法,这也说明了T的方法集中含有值接收者方法。

例2:

go 复制代码
package main

import "fmt"

type Describer interface {
	Describe()
}

type User struct {
	Name string
}

// 指针接收者
func (u *User) Describe() {
	fmt.Println("user:", u.Name)
}

func main() {
	var d Describer = &User{Name: "Alice"}
	d.Describe()

	// 下面这一行如果放开,会编译报错:
	// var d2 Describer = User{Name: "Bob"}
	// User does not implement Describer (Describe method has pointer receiver)
}

编译器会提示User没有实现这个接口,也就是T类型方法集不含有指针接收者方法。

接口nil值的两种区别

接口值等于nil受制于两方面,分别是itab和data,二者都是nil的情况下接口值一定返回nil,但是在常规操作时很容易出现问题,就是主动为接口赋值一个指向nil的具体类型时,通常接口值不为nil,因为接口值的itab已经被建立绑定了这个指向nil的具体类型,所以整个接口值不为nil

接口是否等于nil:

  • itab/type是否为nil
  • data是否为nil
    二者都为nil时,接口的值才为nil

情况1:

go 复制代码
package main

import "fmt"

type Runner interface {
	Run()
}

func main() {
	var r Runner = nil
	fmt.Println(r == nil) // true
}

直接为接口赋值nil,此时它的itab == nil ,data == nil

所以整个接口为nil

情况2:

go 复制代码
package main

import "fmt"

type Runner interface {
	Run()
}

type Dog struct{}

func (d *Dog) Run() {
	fmt.Println("dog run")
}

func main() {
	var d *Dog = nil
	var r Runner = d

	fmt.Println("d == nil:", d == nil)
	fmt.Println("r == nil:", r == nil)
}

输出:

d == nil: true

r == nil: false

这也印证了:为接口赋值一个具体类型时,即便这个具体类型明确指向nil,那么也会:

  • itab != nil ,因为赋值了Dog类型后,itab已经被建立
  • data == nil , 因为具体类型是一个指向nil,所以数据内容指向nil
    所以接口整体不等于 nil

情况3 :

go 复制代码
package main

import "fmt"

type MyError struct {
	msg string
}

func (e *MyError) Error() string {
	return e.msg
}

func foo() error {
	var e *MyError = nil
	return e
}

func main() {
	err := foo()
	fmt.Println(err == nil) // false
}

因为error是一个接口类型,而这个接口在foo方法执行中被赋值了一个具体类型,那么即便这个具体类型指向nil,此时的error接口整体就已经不再是nil了

空接口的使用

空接口 interface{} ,也就是现在常用的any,表示:
不要求任何方法,因此任意类型都实现了它

因为它没有任何方法集约束,所以适合:

  • 存放任意值
  • 做通用容器
  • 接收未知参数
  • 配合类型断言或者type switch做类型分发

例:

go 复制代码
package main

import "fmt"

func printAnything(v any) {
	fmt.Printf("value=%v, type=%T\n", v, v)
}

func main() {
	printAnything(123)
	printAnything("hello")
	printAnything([]int{1, 2, 3})
	printAnything(struct{ Name string }{"Alice"})
}

输出:

value=123, type=int

value=hello, type=string

value=[1 2 3], type=[]int

value={Alice}, type=struct { Name string }

这也描述了空接口不关心方法实现,因为它没有itab,只有具体类型的type同时也没有任何限制,它只关心当前存放的值,也就是对应type + data的组成方式

它非常灵活,但代价是:失去编译器类型约束。

类型断言:取出真实值

当一个接口存储着具体类型时,可以通过类型断言把它取出来:

go 复制代码
v := i.(T)

它的意思是,我断言接口i的动态类型就是T,所以我要把它取出来赋值给v。

如果断言成功,v会被赋值成接口赋值的那个具体值

如果失败则panic。

所以更加安全的写法是:

go 复制代码
v, ok := i.(T)

例如:

go 复制代码
package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func main() {
	var x any = User{Name: "Alice", Age: 18}

	u, ok := x.(User)
	if ok {
		fmt.Println("name:", u.Name)
		fmt.Println("age:", u.Age)
	} else {
		fmt.Println("type assert failed")
	}
}

输出:

name: Alice

age: 18

通过判断断言是否成功,来决定是否操作取出的具体类型的值

例子2,断言失败:

go 复制代码
package main

import "fmt"

func main() {
	var x any = 100

	s, ok := x.(string)
	fmt.Println("s:", s)
	fmt.Println("ok:", ok)
}

输出:

s:

ok: false

这说明断言失败了,如果不加这个ok的话,那么就会panic

例3,断言时的类型要和接口赋值时类型一致:

go 复制代码
package main

import "fmt"

type User struct {
	Name string
}

func main() {
	origin := &User{Name: "Alice"}
	var x any = origin

	u, ok := x.(*User)
	if ok {
		u.Name = "Bob"
	}

	fmt.Println("origin.Name =", origin.Name)
}

输出:

origin.Name = Bob

说明断言出的是原始指针,那么修改指针中的值,则会直接影响原对象,进而修改Name。

type switch批量处理

如果使用any接收值后,一个一个断言会很麻烦,所以可以通过type switch的方式快速处理

例如:

go 复制代码
package main

import "fmt"

func show(v any) {
	switch val := v.(type) {
	case int:
		fmt.Println("int:", val)
	case string:
		fmt.Println("string:", val)
	case []int:
		fmt.Println("[]int:", val)
	default:
		fmt.Printf("unknown type: %T\n", val)
	}
}

func main() {
	show(10)
	show("hello")
	show([]int{1, 2, 3})
	show(3.14)
}

输出:

int: 10

string: hello

\]int: \[1 2 3

unknown type: float64

通过这样的方式可以更加优雅的处理多分支的类型断言

总结

本文简单通过示例描述了接口的使用方式和细节,核心有这些:

  1. 非空接口本质是itab + data
  2. itab负责描述: 具体类型、 接口类型、方法集 、类型实现关心缓存
  3. 运行时通过getitab等方式来快速查找"具体类型 + 接口类型"对应的接口信息
  4. 值接收者/指针接收者差异,本质就是方法集的差异
  5. 接口的nil值分两种
    • 真nil接口:类型信息和数据都为nil
    • 非nil接口:类型信息存在,但data为nil
  6. 空接口可以装任何值,因为它不要求任何方法
  7. 类型断言可以取出原实例,因为接口内部的Type和data
相关推荐
不会写DN2 小时前
如何设计应用层 ACK 来补充 TCP 的不足?
开发语言·网络·数据库·网络协议·tcp/ip·golang
不会写DN3 小时前
如何给 Go 语言的 TCP 聊天服务加上 ACK 可靠送达机制
开发语言·tcp/ip·golang
ZHENGZJM3 小时前
后端基石:Go 项目初始化与数据库模型设计
开发语言·数据库·golang
人间打气筒(Ada)4 小时前
「码动四季·开源同行」go语言:微服务网关如何作为服务端统一入口点?
微服务·golang·开源·微服务网关·go实战
ん贤5 小时前
Go 并发高频十问:goroutine 与线程的区别是什么?select 底层原理是什么?
开发语言·golang·并发
宁瑶琴17 小时前
COBOL语言的云计算
开发语言·后端·golang
m0_694845571 天前
UVdesk部署教程:企业级帮助台系统实践
服务器·开发语言·后端·golang·github
@atweiwei1 天前
Go语言面试篇数据结构底层原理精讲(下)
数据结构·面试·golang
XMYX-01 天前
03 - Go 常用类型速查表 + 实战建议(实战向)
开发语言·golang