go语言的interface为什么好用?

2023年 7月 14日 28.6k 0

Rob Pike 曾说:

如果只能选择一个Go语言的特 性移植到其他语言中,他会选择接口。

看来,go的接口的设计还是一个比较突破的设计。那么他为什么这么说呢?

目前市场上大多数编程语言的接口都是侵入式的,也就是使用接口时要说明,我实现了某某接口。比如java,想要实现一个接口,就需要使用implements关键字然后加上接口名字。这样可想而知耦合度是增加了的。

其实我们要做的就是面相接口编程,但是我们到底怎么做才真正的是做到:依赖于接口而不是实现,优先使用组合而不是继承呢?

大家都知道 Duck Typing,当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。这句话是非常有哲学的,因为这句话你不知道到底是正确还是错误。假如现在有一个鸭子状的玩具摆在这里,对于一个小女孩看见可能就说这是一个鸭子。但是由于我比较喜欢吃烤鸭,当我看到这个玩具时,我只认为这就是一个玩具,不是鸭子,充其量是一个鸭子状的玩具。但是对于小女孩来说,为什么是鸭子呢?因为她看到了玩具的嘴巴扁扁的,样子和鸭子一样,所以她认为这就是鸭子。这就是因为我们关心的点不一样。

go语言的interface为什么好用?

图片来源于网络

首先看一下go是如何实现一个接口的,我们使用最经典的一个例子:

package main
import (
 "fmt"
 "reflect"
 "strconv"
)
type Stringer interface {
 String() string
}
type Binary uint64
func (i Binary) String() string {
 return strconv.FormatUint(i.Get(), 10)
}
func (i Binary) Get() uint64 {
 return uint64(i)
}
func main() {
 b := Binary(200)
 s := Stringer(b)
 fmt.Println(s.String())
 fmt.Println("s 的类型:", reflect.TypeOf(s))
}

首先我们有了一个接口:Stringer,其中需要String()方法,然后我们有一个Binary类型(retype uint64),Binary类型有两个方法,一个是Get,另外一个就是String,由于Stringer接口只有String一个方法,而Binary类型正好也有这个方法,好,那么我们就说Binary实现了Stringer接口。这就是非侵入式的,我们不需要显式的说明实现了哪个接口,只需要根据我们已有的方法来判断就可以。

这样有什么好处呢?首先我们在写代码时,可以不必关注我需要实现哪些接口,只要实现自己的功能就好,不同于侵入式那样接口修改之后,所有实现类都需要跟着修改。虽然说这是好处,但是现实往往不是这样,在项目中,我们大多数时间还是要考虑好接口的,但是我们可以先写实现类,后写接口了。而且这样不是强耦合了,

想要更好的理解,我们就要看一下go中接口的底层结构。其源代码在 runtime/runtime2.go 文件中。

type iface struct {
 tab *itab
 data unsafe.Pointer
}
type eface struct {
 _type *_type
 data unsafe.Pointer
}

eface 是空的接口,即不需要实现任何方法。iface是有方法的接口,也就是我们平时实现的接口的结构,包含两部分:一部分是确定唯一的包含方法的interface的具体结构类型,一部分是指向具体方法集的指针。下面是一个我从网上看到的图片,很形象的解释了内存关系:

go语言的interface为什么好用?

图片来源于网络

简单来说,一个iface有两个部分,一个是指向具体类型的指针,另一个是指向具体数据的指针。对于指向具体类型的指针,具体结构如下:

type itab struct {
 inter *interfacetype
 _type *_type
 hash uint32 // copy of _type.hash. Used for type switches.
 _ [4]byte
 fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

其中的inter和_type用来确定具体的类型的,fun是其具体的方法的列表。

我们在回头看我们最开始的例子,就是Stringer接口和Binary的例子,在其main中,赋值之后interface的内存是如何的呢?我们看下图 :

go语言的interface为什么好用?

图片来源于网络

这时我们就可以理解了,将b转成Stringer接口类型时,s的内存就赋值了,其data部分就是具体的数据,tab部分就是其具体的类型和其拥有的方法。注意,这两个都是指针,在go中,想要理解接口,我们一定要想着其最终都是指针。

到这里我们可以知道go中的接口时如何实现的了,但是大家是否想过,go中没有明确的说明实现某个接口(即没有implements关键字),那是如何知道具体实现了哪个接口呢?其实这个工作是在运行时解决的,也就是说在运行时才判断。也就是说在使用 interface 时不需要显式在 struct 上声明要实现哪个 interface ,只需要实现对应 interface 中的方法即可,go 会自动进行 interface 的检查,并在运行时执行从其他类型到 interface 的自动转换,即使实现了多个 interface,go 也会在使用对应 interface 时实现自动转换。当然这会导致性能下降的,但是go已经做到降低最小了,这点我们不需要考虑。

到这里我们基本上将接口的使用以及内存的基本结构都梳理了一遍,不知大家对于go的这种方式是否喜欢呢?

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论