什么是接口
在编程中咱们经常会听到依靠倒置准则, 即:高层模块不应该依靠低层模块, 两者都应该依靠其笼统; 笼统不应该依靠细节; 细节应该依靠笼统。咱们从下面的的一个比如详细解释下。在服务端开发中, 需求进行分层, 从而防止小的功用造成大面积修改。例如:server 层调用 dal 层/仓储层, 通常会这么完成:
type Service struct {
d Dal
}
func NewService(d MysqlDal) Service {
return Service{d: d}
}
func (s Service) List() []string {
// biz logic
return s.d.QueryData()
}
type MysqlDal struct {
}
func (d MysqlDal) QueryData() []string {
// query from mysql
return []string{"mysql"}
}
func main() {
s := NewService(Dal{})
fmt.Println(s.List())
}
上面这种办法完成也没什么大问题「又不是不能跑」, 当咱们的存储选型需求改造, 根据需求做出一下修改:重新创建一个 RedisDal, 用于从 redis 里获取数据, 由于 service 依靠了 dal 的完成细节, 因而改动时需求运用 RedisDal 替换 MysqlDal
func NewService(d RedisDal) Service {
return Service{d: d}
}
func (s Service) List() []string {
// biz logic
return s.d.QueryData()
}
type RedisDal struct {
}
func (d RedisDal) QueryData() []string {
// query from redis
return []string{"redis"}
}
func main() {
//s := NewService(Dal{})
s := NewService(RedisDal{})
fmt.Println(s.List())
}
能够看到 service 并不关心数据是从 redis 来,仍是 mysql 来。能够运用依靠倒置准则, 让 service 依靠 dal 层的接口即可, 改造后的代码如下, 这样咱们无需更新 service 代码, 就能够完成存储替换。

func NewService(d Dal) Service {
return Service{d: d}
}
func (s Service) List() []string {
// biz logic
return s.d.QueryData()
}
type Dal interface {
QueryData() []string
}
type RedisDal struct {
}
func (d RedisDal) QueryData() []string {
// query from mysql
return []string{"redis"}
}
func main() {
s := NewService(RedisDal{}) // MysqlDal
fmt.Println(s.List())
}
接口类型
上节讲到, 运用接口能够有用的将高层模块和底层模块进行解耦。在 go 中, 能够运用 interface 关键字声明接口类型。在 1.8 版本之前, 接口类型既界说了办法的调集, 任何完成接口办法调集的类型都能够被转化成对应接口类型。在 1.8 之后, 为了对范型进行支持, 接口类型界说为类型的调集。即运用 interface 能够声明办法和类型,看下面的比如:
type BasicInterface interface { // 根底接口类型, 只包括办法的调集
Add()
}
type GeneralInterface interface { // 类型的调集, 只能用于类型参数中
~int | ~int32
}
接口运用
这儿咱们只讨论根底接口类型, 即只包括办法的调集。关于通用接口类型, 能够去范型中了解。界说一个接口之后, 需求完成对应的办法, 那么运用值接纳和指针接纳有什么区别呢?
type BasicInterface interface { // 根底接口类型, 只包括办法的调集
Add()
}
type ImplA struct {
Name string
}
func (i ImplA) Add() {
i.Name = "add"
}
type ImplB struct {
Name string
}
func (i *ImplB) Add() {
i.Name = "add"
}
func main() {
var a BasicInterface = &ImplA{"a"} // 办法调用会转为 *(&ImplA{"a"})
var b BasicInterface = ImplA{"b"}
var c BasicInterface = &ImplB{"c"}
// var d BasicInterface = ImplB{} Cannot use 'ImplB{}' (type ImplB) as the type BasicInterface Type does not implement 'BasicInterface' as the 'Add' method has a pointer receiver
a.Add()
b.Add()
c.Add()
fmt.Println(a, b, c) // &{a} {b} &{add}
}
从上面的比如咱们能够看到, 当办法接纳者为指针时, 无法将结构体值赋值给接口类型的变量。一起, 指针和非指针的区别在于: 函数调用时传递的指针 copy 仍是值 copy, 上面的办法调用能够等价为下面的表述:
func AddA(i ImplA) {
i.Name = "add"
}
func AddB(i *ImplB) {
i.Name = "add"
}
func main() {
a, b := &ImplA{"a"}, ImplA{"b"}
c := &ImplB{"c"}
//d := ImplB{"d"}
AddA(*a)
AddA(b)
AddB(c)
// AddB(d) 编译失败
fmt.Println(a, b, c) // &{a} {b} &{add}
}
在正式开发中, 如何确认运用值接纳者仍是指针接受者?详细仍是看场景, 假如需求更新传入方针值那就有必要运用指针; 从性能视点来看, 假如完成接口类型占用的空间比较大, 则能够运用指针, 能够防止办法调用导致的值拷贝。
类型断语与类型转化
接口类型表示一个类型的调集。 例如 BasicInterface, 任何完成 add 办法的类型都属于 BasicInterface 类型。那么如何将接口类型与实在类型彼此转化呢?将接口类型转化为实践类型叫做类型断语「interface_value.(type)」, 将表达式转化为方针类型叫做类型转化「interface_type(value)」。从下面的 case 能够看出, 关于类型转化, 编译器会查看变量是否能够转化成方针类型; 关于类型断语, 只能作用于接口类型, 用于判别接口值的实在类型, 假如未运用安全断语, 则可能会发生 panic。
func main() {
a, b := ImplA{Name: "a"}, ImplB{"b"}
ia := BasicInterface(a) // 类型转化, 编译器会做查看
// ib := BasicInterface(b) Cannot convert an expression of the type 'ImplB' to the type 'BasicInterface'
ib := BasicInterface(&b)
// a = ib.(ImplA) panic: interface conversion: main.BasicInterface is *main.ImplB, not main.ImplA
// 安全断语
if _, ok := ib.(ImplA); !ok {
fmt.Println("can not convert ib to ImplA")
}
a = ia.(ImplA)
switch ib.(type) { // 类型断语
case ImplA:
a = ia.(ImplA)
case *ImplB:
_ = ib.(*ImplB)
}
}
通常利用下面的表达式让编译器判别某个结构体是否完成了对应的接口
var _ BasicInterface = (*ImplA)(nil) // 隐式转化,等价于 var _ BasicInterface = (BasicInterface)((*ImplA)(nil))
空接口
有一种接口比较特殊, 那就空接口。咱们能够将任意类型的接口赋值给空接口。
func main() {
var i interface{} // 空接口
i = 4
i = "string"
i = ImplA{}
i = ImplB{}
var j BasicInterface = ImplA{}
i = j
}
接口赋值
在下面的比如中, i 为 nil, 当把值为 nil 的变量 a 赋值给 i 之后却变成了 non-nil。这儿其实涉及到接口类型的完成, 关于接口类型变量 a 由两部分组成「动态类型和动态值」。因而, i 为一个接口类型变量, 赋值之后它是 non-nil 的, 里边包括两个属性「动态类型: *ImplA, 动态值: nil」
func main() {
var a *ImplA
fmt.Println(a == nil) // true
var i BasicInterface
fmt.Println(i == nil) // true
i = a
fmt.Println(i == nil) // false
}
如下图所示, 当发生赋值时i=a
, i 不再是 nil, 而是一个 type 为 *ImplA, value 为 nil 的接口值。

接口比较
关于接口类型值的比较, 其实比较的是接口对应的类型和值, 两者相同才回来 ture。假如接口体里有不能够比较类型, 则会发生 panic。
func main() {
var x, y BasicInterface
fmt.Println(x == y) // true
x, y = ImplA{}, ImplA{}
fmt.Println(x == y) // ture
x, y = ImplA{}, ImplA{Name: "test"}
fmt.Println(x == y) // false
x, y = ImplA{}, &ImplB{}
fmt.Println(x == y) // false
}
总结
本文主要介绍了一下 go 中的根底接口类型, 主要讲下接口的声明、接口完成、接口比较以及类型断语。在正式开发中不要为了运用接口而界说接口。通常情况下, 关于服务分层, 层与层之间能够界说接口, 但在层的内部就无需再界说接口了。