来聊聊 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 个条件
-
方法名称要一样;
-
签名,包括入参、返回值也要一致。
对了,这种没有侵入式的接口实现称为 "Duck typing"
鸭子类型。可以参考下:zh.wikipedia.org/wiki/%E9%B8...
动态类型与动态值
接口动态值指的是接口变量可以持有不同类型的值,接口变量包含两部分
-
动态类型;
-
动态值。
动态类型是接口变量当前持有的具体类型,动态值是具体类型的值。举一个案例吧,有点晦涩难懂。
go
type Speaker interface {
Speak()
}
type Dog struct{}
type Cat struct{}
func (d *Dog) Speak() {
fmt.Println("汪")
}
func (c *Cat) Speak() {
fmt.Println("喵")
}
案例定义了简单接口 Speaker
,两个实现类分别是 Dog
和 Cat
均实现了 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:
-
将一个nil 值赋给接口变量;
-
接口变量未初始化,即声明但未赋值。
除此之外,即使接口变量的动态值为 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 个接口每一个都仅有一种能力,分别是读出、写入和关闭。
总结
-
接口看是非常简单,但一些细节考验大家基本功底。所以不要停留能用和会用,多掌握一些基本功,对后续在项目中的运用是非常有帮助的。
-
写优雅代码离不开设计模式,接口在设计模式中可是常客,感兴趣可以看看往期文章。