来聊聊 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. 如何在项目中正确使用责任链模式? - 掘金

相关推荐
代码匠心24 分钟前
从零开始学Flink:状态管理与容错机制
java·大数据·后端·flink·大数据处理
分享牛25 分钟前
LangChain4j从入门到精通-11-结构化输出
后端·python·flask
Grassto34 分钟前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
岁岁种桃花儿39 分钟前
SpringCloud超高质量面试高频题300道题
spring·spring cloud·面试
努力学算法的蒟蒻1 小时前
day75(2.3)——leetcode面试经典150
面试·职场和发展
南风知我意9571 小时前
【前端面试3】初中级难度
前端·javascript·面试
知识即是力量ol1 小时前
在客户端直接上传文件到OSS
java·后端·客户端·阿里云oss·客户端直传
华清远见成都中心2 小时前
GPIO(通用输入输出)面试中高频问题
单片机·面试·职场和发展
闻哥2 小时前
深入理解 Spring @Conditional 注解:原理与实战
java·jvm·后端·python·spring
qq_256247052 小时前
Google 账号防封全攻略:从避坑、保号到申诉解封
后端