来聊聊 Go 接口类型

来聊聊 Go 接口类型

GO 接口基本概念

接口(interface)是定义一组方法签名的类型,接口类型非常特殊,不能实例化。这就意味着,你不能用 new 函数或者 make 函数创建一个变量值,也没有办法定义一个字面量表示它。

另外,定义了接口,但没有任何类型实现它,接口毫无意义。

接口定义跟结构体定义类似,定义如下

go 复制代码
type xx interface {
	zz()
}

结构体定义

go 复制代码
type yy struct {
	xx string
}

它们也有本质区别,接口类型花括号内是它方法的申明,结构体花括号内是它属性的申明,这点和其它编程语言可能不同。

接口类型包含的方法称为该接口的方法集合,要实现该接口必须要把所有方法都实现。

接口实现

首先,申明接口 ICache[T any] ,缓存管理器接口。定义了 3 个方法,Set、Get、Del 对应缓存的增、删、获取。

go 复制代码
// ICache 管理器接口
type ICache[T any] interface {
	Set(ctx context.Context, params map[string]T) error
	Get(ctx context.Context, keys []string) (map[string]T, error)
	Del(ctx context.Context, keys []string) error
}

只要有一个数据类型的方法中有这 3 个方法,它就实现了这个接口,举一个栗子

go 复制代码
type CacheImpl[T any] int

func (e CacheImpl[T]) Set(ctx context.Context, params map[string]T) error {
	return nil
}

func (e CacheImpl[T]) Get(ctx context.Context, keys []string) (map[string]T, error) {
	return nil, nil
}

func (e CacheImpl[T]) Del(ctx context.Context, keys []string) error {
	return nil
}

是不是觉得很神奇?int 类型也能实现接口,不过该场景 int 类型实现这接口意义不大,应该定义 struct 来实现接口。

从实现类可以看出,实现接口有 2 个条件

  1. 方法名称要一样;

  2. 签名,包括入参、返回值也要一致。

对了,这种没有侵入式的接口实现称为 "Duck typing" 鸭子类型。可以参考下:zh.wikipedia.org/wiki/%E9%B8...

动态类型与动态值

接口动态值指的是接口变量可以持有不同类型的值,接口变量包含两部分

  1. 动态类型;

  2. 动态值。

动态类型是接口变量当前持有的具体类型,动态值是具体类型的值。举一个案例吧,有点晦涩难懂。

go 复制代码
type Speaker interface {
	Speak()
}

type Dog struct{}
type Cat struct{}

func (d *Dog) Speak() {
	fmt.Println("汪")
}

func (c *Cat) Speak() {
	fmt.Println("喵")
}

案例定义了简单接口 Speaker ,两个实现类分别是 DogCat 均实现了 Speak 方法,输出特定内容。

go 复制代码
func main() {
	var (
		// 创建接口变量
		speaker Speaker
	)

	// 将 Dog 类型赋值给接口变量
	dog := &Dog{}
	speaker = dog
	speaker.Speak() // 输出 汪

	// 将 Cat 类型赋值给接口变量
	cat := &Cat{}
	speaker = cat
	speaker.Speak() // 输出: 喵
}

接口变量 speaker 首先持有 Dog 类型的值输出"汪",再持有 Cat 类型的值输出"喵"。尽管 speaker 具体类型不同,但它们都实现了 Speak 方法,因此可以被同样的接口变量持有。

变量 speaker,赋给它的值叫称动态值,该值的类型称动态类型。比如,我们把 dog 值赋给了变量 speaker,这个结果值就是变量 speaker 的动态值,此结果值的类型 *Dog 就是该变量的动态类型。

所以,speaker 变量是永远不会变化的,只是它的动态类型会跟着赋给它的动态值的变化来变化。

空接口

空接口(empty interface)是一个不包含任何方法声明的接口。它可以表示任意类型的值,因为所有类型都至少实现了零个方法,所以它可以接受任何类型的值。在范型没有出现之前用的比较广泛。

scss 复制代码
func main() {
	Print("张三")
	Print(1)
}

func Print(val interface{}) {
	fmt.Printf("Type: %T, Value: %v\n", val, val)
}

空接口支持断言,ok 为 true 断言成功。

go 复制代码
func Print(val interface{}) {
	str, ok := val.(string)
	if ok {
		fmt.Println(str)
	}
}

接口为 nil

接口变量的值在以下情况下才会真正为 nil:

  1. 将一个nil 值赋给接口变量;

  2. 接口变量未初始化,即声明但未赋值。

除此之外,即使接口变量的动态值为 nil,接口变量本身也不会是 nil。举一个栗子

go 复制代码
type Person interface {
	Name()
}

type StudentImpl struct {
}

func (receiver *StudentImpl) Name() {
	fmt.Println("a")
}

func main() {
	var (
		person Person
		impl   *StudentImpl
	)
	person = impl
	println(person)

	person.Name()
}

结果输出为

css 复制代码
(0xf8a2598,0x0)
a

从输出可以看出,peson 并非为 nil,那可能是语言层面又给包了一层。于是百度了下,果真如此,这个变量的值其实是某一个专用数据结构的一个实例,而不是我们赋给该变量的那个实际的值。于是写了一段代码验证下

go 复制代码
func main() {
	var (
		person Person
		impl   *StudentImpl
	)
	person = impl
	println(person)

	person.Name()
	test(person)
}

// 获取接口变量的底层数据信息
func test(i interface{}) {
	type tmp struct {
		ptr  *uintptr
		data unsafe.Pointer
	}

	tmpPtr := *(*tmp)(unsafe.Pointer(&i))
	fmt.Printf("type: %v,data: %v", tmpPtr.ptr, tmpPtr.data)
}

结果输出为

go 复制代码
type: 0xa7f2240,data: <nil>Exiting.

定义了一个 tmp 它包含了 2 个指针,一个指向类型,另外一个指向值。结构赋动态值的时候,存储的是包含了这个动态值的副本的一个结构更加复杂的值。

这里引出另外一个问题,把值为 nil 的实现类变量赋给接口变量,该接口变量可以调用该接口的方法?

答案是可以的,但不能用该实现类的属性字段,否则会 Panic。

接口的组合

接口组合是指将多个接口组合成一个新的接口。组合接口包含了所有组成接口的方法。这种方式允许将多个行为组合在一起,从而形成一个具有更多功能的接口。

不同接口中的方法具有相同的名称和签名,那么组合接口只会有一个该方法的声明。

go 复制代码
type Person interface {
	Student
	Teacher
}

type Student interface {
	Name()
}

type Teacher interface {
	Name()
}

type Impl struct {
}

func (receiver *Impl) Name() {
	fmt.Println("a")
}

但,不同接口方法名称相同,签名不同就编译报错,提示:Duplicate method 'Name'。

go 复制代码
type Person interface {
	Student
	Teacher
}

type Student interface {
	Name(name string)
}

type Teacher interface {
	Name()
}

如果大家有研究 GO 标准库的习惯,Go 标准库接口方法声明都较少,并通过这种接口间的组合来扩展程序、增加程序的灵活性。

我感觉 GO 提倡将接口设计得尽可能小,以便实现更灵活、可维护和可复用的代码;相比较包含很多方法的大而全的接口,小接口可以更加专注地表达某一种能力或某一类特征,也更容易被组合在一起。

GO 标准库 IO 包,每个接口只有一个方法。通过接口组合的思路把小接口组合成更大的接口,比如:ReadWriteCloser 它组合了 Reader、Writer、Closer 3 个接口。这三个接口都只包含了一个方法,是典型的小接口,这 3 个接口每一个都仅有一种能力,分别是读出、写入和关闭。

总结

  1. 接口看是非常简单,但一些细节考验大家基本功底。所以不要停留能用和会用,多掌握一些基本功,对后续在项目中的运用是非常有帮助的。

  2. 写优雅代码离不开设计模式,接口在设计模式中可是常客,感兴趣可以看看往期文章。

    a. 深入设计模式之适配器模式GO版本「附详细案例」 - 掘金

    b. 工厂模式GO版本「附详细案例」 - 掘金

    c. 工厂模式GO版本「附详细案例」 - 掘金

    d. 策略模式GO版本「附详细案例」 - 掘金

    e. 如何在项目中正确使用责任链模式? - 掘金

相关推荐
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
码农小旋风3 小时前
详解K8S--声明式API
后端
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~4 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616884 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
睡觉谁叫~~~5 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
鱼跃鹰飞7 小时前
大厂面试真题-简单说说线程池接到新任务之后的操作流程
java·jvm·面试
2401_865854887 小时前
iOS应用想要下载到手机上只能苹果签名吗?
后端·ios·iphone
AskHarries7 小时前
Spring Boot集成Access DB实现数据导入和解析
java·spring boot·后端