Interface 底层有两种实现方式:
一种是不包含任何方法的空接口, 底层为 efface , 我们常将另外一个变量赋值给他, 把他当作“任意类型”来使用, 这种情况下他不包含任何方法。
源码如下 :
type iface struct {
_type *_type
data unsafe.Pointer
}
另一种是 包含方法的 iface ,他包含一组方法, 也可能包含其他接口, 我们常用它来做面向对象编程。
源码如下 :
type iface struct {
tab *itab
data unsafe.Pointer
}
其中 data unsafe.Pointer 是指向底层数据的指针。 他们的不同点在于另外一个字段,
先看下 itab 。
type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32 // copy of _type.hash. Used for type switches.
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // variable sized
}
Itab 中包含了 efface 里面的 _type 字段!!! 那么我们可以认为其实 iface 里面包含了 eface 的属性, 是对 eface 的扩展。
再来看 _type , 他是描述 Go 语言中各种数据类型的结构体(描述data字段的类型),底层如下:
type _type struct {
// 类型大小
size uintptr
ptrdata uintptr
// 类型的 hash 值
hash uint32
// 类型的 flag,和反射相关
tflag tflag
// 内存对齐相关
align uint8
fieldalign uint8
// 类型的编号,有bool, slice, struct 等等等等
kind uint8
alg *typeAlg
// gc 相关
gcdata *byte
str nameOff
ptrToThis typeOff
}
再看 interfacetype , 他描述的是接口的类型。
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
他里面也包含了 _type 。
我们再用一张图来看下 iface 的全貌。
对比下 eface 的全貌。
根据上面的源码和图片,我们知道设计 efface 的目的可能就是为了在 interface 中不包含方法的时候节省内存, 而其他多余的字段基本上都是用来描述interface的方法的。
如下代码 :
package main
import "fmt"
type Coder interface {
code()
}
type Gopher struct {
name string
}
func (g Gopher) code() {
fmt.Printf("%s is coding\n", g.name)
}
func main() {
var c Coder
fmt.Println(c == nil) // 输出 true
fmt.Printf("c: %T, %v\n", c, c) // 输出 c: <nil>, <nil>
var g *Gopher
fmt.Println(g == nil) // 输出 true
c = g
fmt.Println(c == nil) // 输出 false
fmt.Printf("c: %T, %v\n", c, c) // 输出 c: *main.Gopher, <nil>
}
声明 c 的时候, c 的类型 和 底层数据都是 nil , 所以 c等于 nil 。
定义 g 的时候, g默认指向 nil , 所以 g 等于 nil 。
当将 g 赋值给 c 的时候, c 的 type 指向了 g , c 的底层数据也指向了 g 的底层数据(还是 nil) , 虽然打印出 c 的值还是 nil , 但是 c 却不等于 nil, 因为 c的 type 不等于 nil 。 其中 type 是接口的动态类型, 底层数据是接口的值。
interface 在编程的时候经常用来作为接口进行解耦。
比如我们的系统要实现存储, 我们的数据库可以选择 mysql、 mongodb等。 最简单的方法是我直接选择 mysql, 定义一个mysql对象, 这个对象实现我们存储需要的各种方法。
但是如果某天我们想把 mysql 换成 mongodb 呢? 这个时候我们有两种选择, 要么将原来的 mysql 对象改成 mongo对象, 这样调用存储的业务层不需要改动。要么我们创建一个新的 mongo对象, mysql对象保留下来, 我们修改业务层的调用为调用 mongo对象。
这两种方案都是可行的, 但是扩展的时候都不方便, 有没有更好的方法呢?
那就是使用接口。 先定义一个接口, 确定我们业务层需要实现的方法, 业务层直接通过这个接口进行调用。 然后我们再根据具体需求增加对应的存储对象(mysql、mongo等), 然后我们只需要在业务层调用之前先用我们需要的存储对象构造我们的接口就行了。 这样扩展、替换都非常方便。
这里面将把数据存储到数据库这个流程拆分成了两个独立的层。 数据存储只负责存储, 业务模块只负责业务逻辑。
分层之后直接调用就是紧耦合的, 我们增加一层接口就是解耦, 业务层和存储层只需要遵循接口而不需要关注对方怎么实现的,这样就可以进行替换。
分层 - > 解耦 -> 替换