壹 介绍
在Go语言中接口(interface)是一种类型,一种抽象的类型。相较于之前章节中讲到的那些具体类型(字符串、切片、结构体等)更注重“我是谁”,接口类型更注重“我能做什么”的问题。接口类型就像是一种约定——概括了一种类型应该具备哪些方法,在Go语言中提倡使用面向接口的编程方式实现解耦。接口就是定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
耦合与解耦(题外话了解就行,可以不看):
- 耦合是指两个或两个以上的体系或两种运动形式间通过相互作用而彼此影响以至联合起来的现象。说白了,就是两个东西或者两个东西以上纠缠不清的现象,你需要我,我需要你。
- 在软件工程中,对象之间的耦合度就是对象之间的依赖性。对象之间的耦合越高,维护成本越高,因为很容易动一个参数导致其他功能函数也改变,因此对象的设计应使类和构件之间的耦合最小。
- 解耦字面意思就是解除耦合关系。
- 设计的核心思想:尽可能减少代码耦合,如果发现代码耦合,就要采取解耦技术。让数据模型,业务逻辑和视图显示三层之间彼此降低耦合,把关联依赖降到最低,而不至于牵一发而动全身。原则就是A功能的代码不要写在B的功能代码中,如果两者之间需要交互,可以通过接口,通过消息,甚至可以引入框架,但总之就是不要直接交叉写。
贰 接口类型
一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。
2.1 接口的定义
每个接口类型由任意个方法签名组成:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
// 接口类型名:Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。接口名最好要能突出该接口的类型含义。
// 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
// 参数列表、返回值列表:没有参数列表和返回值列表,不用写,但如果有类型的话,参数列表和返回值列表中的参数变量名可以省略,类型不可以省略。
例子,定义一个包含Text方法的Text接口:
// 可以看出很想结构体定义
type Text interface{
Text([]string) error
}
在这里,调用者可以知道,使用这个接口需要传入一个[]string
字符串类型的切片,然后执行方法返回一个error的错误类型即可,不需要知道其中的实现逻辑是啥,而开发者只需要知道传入一个[]string
字符串类型的切片,实现对应的功能后,返回一个error错误即可。
2.2 实现接口的条件
接口就是规定了一个需要实现的方法列表,如果一个任意类型 T 的方法集为一个接口类型的方法集的超集,则我们说类型 T 实现了此接口类型。
实现j关系在 Go语言中是隐式的,两个类型之间的实现关系不需要在代码中显式地表示出来,就比如说,Go语言中没有类似于 implements 的关键字去限定他,而是隐式的方法进行限制。 而这个隐式限制为Go编译器自动在需要的时候检查两个类型之间的实现关系。
接口定义后,如果需要实现这个接口,那么调用方才能说是正确使用接口。接口的实现需要遵循两条规则才能让接口可用:
- 接口的方法与实现接口的类型方法格式一致
在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。
- 接口中所有方法均需要被实现
当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。
举个例子说明:
package main
import "fmt"
// 我们规定的Singer接口类型,它包含一个Sing方法,即:该方法实现的功能是叫。
// Singer 接口,可以看到我们定义接口后面都会加er
type Singer interface {
Sing(string)
}
// 这时候我们有一个Dog结构体类型
type Dog struct{}
func (s Dog) Sing(str string) {
fmt.Println(str)
}
func main() {
// 定义一个Dog类型和一个Singe类型
dog := Dog{}
var s Singer
// 将dog赋值给s类型
s = dog
// 使用s.Sing()方法,实现接口Sing方法
s.Sing("汪汪汪") // 汪汪汪
}
这时候可能会有疑问,为啥不直接使用dog.Sing?因为这里我们是要将类型去实现接口,而不是单纯使用Sing方法。
接着可以调试下,如果Singer接口的Sing方法的参数注释掉,那么就会报错,因为需要满足实现接口的第一个条件:接口的方法与实现接口的类型方法格式一致;如果,将s.Sing方法注释掉,那么也会报错,因为需要满足实现接口的第二个条件:接口中所有方法均需要被实现。当满足这两个条件时,我们就称类型实现了接口。
2.3 为什么要使用接口?
为什么要使用接口?我先举两个例子:
例子一:A是一个大项目的牵头人,他手下有二三十号人,对于这个大项目,他不可能带着这二三十号人一起一个功能一个功能的开发,由于不同的人写代码的习惯不同,那么会带来开发时间周期长、交流时间长,那如果将这个大项目分成几个模块,每个模块有对应的功能,然后分给每个人,这样开发人员就只要关心自己那部分代码就可以,而不是关心这个项目的全部功能(况且这样是不符合实际的)。这时候这个模块我们就可以理解为我们现在的接口。A将大项目的接口方法内容规划出来,分发给这二三个人去执行对应的功能接口即可,不管这二三十个人开发的怎么样,最后只要实现了对应功能就行。如果某天其中一个人辞职了不干了,A会招聘另一个人,他只需要看A定义的接口,就能实现A要的功能模块了,而不是在离职的那个人的代码里(可能有错误,可能有冗余)修改,这样这个项目就有了很好的扩展性和容错性。
例子二:现在假设我们的代码世界里有很多小动物,下面的代码片段定义了猫和狗,它们饿了都会叫。
package main
import "fmt"
type Cat struct{}
func (c Cat) Say(str string) {
fmt.Println(str)
}
type Dog struct{}
func (d Dog) Say(str string) {
fmt.Println(str)
}
func main() {
c := Cat{}
c.Say("喵喵喵~")
d := Dog{}
d.Say("汪汪汪~")
}
这个时候又跑来了一只羊,羊饿了也会发出叫声。
type Sheep struct{}
func (s Sheep) Say(str string) {
fmt.Println(str)
}
我们接下来定义一个饿肚子的场景。
// MakeCatHungry 猫饿了会喵喵喵~
func MakeCatHungry(c Cat) {
c.Say()
}
// MakeSheepHungry 羊饿了会咩咩咩~
func MakeSheepHungry(s Sheep) {
s.Say()
}
接下来会有越来越多的小动物跑过来,我们的代码世界该怎么拓展呢?
在饿肚子这个场景下,我们可不可以把所有动物都当成一个“会叫的类型”来处理呢?当然可以!使用接口类型就可以实现这个目标。 我们的代码其实并不关心究竟是什么动物在叫,我们只是在代码中调用它的Say()
方法,这就足够了。
package main
import "fmt"
// 我们可以约定一个Sayer类型,它必须实现一个Say()方法,只要饿肚子了,我们就调用Say()方法。
type Sayer interface {
Say(string)
}
// 然后我们定义一个通用的MakeHungry函数,接收Sayer类型的参数。
// MakeHungry是饿肚子了调用Say函数
func MakeHungry(s Sayer, say string) {
s.Say(say)
}
// 通过定义通用的MakeHungry函数,我们就不需要一个一个的写不同类型的MakeHungry方法了
// MakeCatHungry 猫饿了会喵喵喵~
// func MakeCatHungry(c Cat) {
// c.Say()
// }
// MakeSheepHungry 羊饿了会咩咩咩~
// func MakeSheepHungry(s Sheep) {
// s.Say()
// }
// 定义猫类型
type Cat struct{}
func (c Cat) Say(say string) {
fmt.Println(say + "~")
}
// 定义狗类型
type Dog struct{}
func (d Dog) Say(say string) {
fmt.Println(say + "~")
}
// 定义养类型
type Sheep struct{}
func (s Sheep) Say(say string) {
fmt.Println(say + "~")
}
func main() {
// 我们通过使用接口类型,把所有会叫的动物当成Sayer类型来处理,只要实现了Say()方法都能当成Sayer类型的变量来处理。
var c Cat
MakeHungry(c, "喵喵喵")
var d Dog
MakeHungry(d, "汪汪汪")
var s Sheep
MakeHungry(s, "咩咩咩")
}
在电商系统中我们允许用户使用多种支付方式(支付宝支付、微信支付、银联支付等),我们的交易流程中可能不太在乎用户究竟使用什么支付方式,只要它能提供一个实现支付功能的Pay
方法让调用方调用就可以了。
根据两个例子,我们可以看出,接口类型区别于我们之前章节中介绍的那些具体类型,让我们专注于该类型提供的方法,而不是类型本身。让使用者只需要传入接口参数,就能实现对应的功能,而对于开发者,只需要知道传入的大致接口参数,就去实现功能即可。当然如果不好理解,我们可以不适应,一般情况下,在大型项目中使用接口会比较多,但在编写小工具的时候,可以不用。
2.4 面向接口编程
PHP、Java等语言中也有接口的概念,不过在PHP和Java语言中需要显式声明一个类实现了哪些接口,在Go语言中使用隐式声明的方式实现接口。只要一个类型实现了接口中规定的所有方法,那么它就实现了这个接口。接口类型是Go语言提供的一种工具,在实际的编码过程中是否使用它由你自己决定。
2.5 接口类型变量
那实现了接口又有什么用呢?一个接口类型的变量能够存储所有实现了该接口的类型变量。例如在上面的示例中,Dog
和Cat
类型均实现了Sayer
接口,此时一个Sayer
类型的变量就能够接收Cat
和Dog
类型的变量。
var x Sayer // 声明一个Sayer类型的变量x
a := Cat{} // 声明一个Cat类型变量a
b := Dog{} // 声明一个Dog类型变量b
x = a // 可以把Cat类型变量直接赋值给x
x.Say("喵喵喵") // 喵喵喵
x = b // 可以把Dog类型变量直接赋值给x
x.Say("汪汪汪") // 汪汪汪
叁 值接收者和指针接收者
在结构体中,我们介绍了在定义结构体方法时既可以使用值接收者也可以使用指针接收者。那么对于实现接口来说使用值接收者和使用指针接收者有什么区别呢?接下来我们通过一个例子看一下其中的区别。
我们定义一个Mover
接口,它包含一个Move
方法。
// Mover 定义一个接口类型
type Mover interface {
Move()
}
3.1 值接收者实现接口
我们定义一个Dog
结构体类型,并使用值接收者为其定义一个Move
方法。
// Dog 狗结构体类型
type Dog struct{}
// Move 使用值接收者定义Move方法实现Mover接口
func (d Dog) Move() {
fmt.Println("狗会动")
}
此时实现Mover
接口的是Dog
类型。
var x Mover // 声明一个Mover类型的变量x
var d1 = Dog{} // d1是Dog类型
x = d1 // 可以将d1赋值给变量x
x.Move()
var d2 = &Dog{} // d2是Dog指针类型
x = d2 // 也可以将d2赋值给变量x
x.Move()
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量。
3.2 指针接收者实现接口
我们再来测试一下使用指针接收者实现接口有什么区别。
// Cat 猫结构体类型
type Cat struct{}
// Move 使用指针接收者定义Move方法实现Mover接口
func (c *Cat) Move() {
fmt.Println("猫会动")
}
此时实现Mover
接口的是*Cat
类型,我们可以将*Cat
类型的变量直接赋值给Mover
接口类型的变量x
。
var c1 = &Cat{} // c1是*Cat类型
x = c1 // 可以将c1当成Mover类型
x.Move()
但是不能给将Cat
类型的变量赋值给Mover
接口类型的变量x
。
// 下面的代码无法通过编译
var c2 = Cat{} // c2是Cat类型
x = c2 // 不能将c2当成Mover类型
为什么值接收者无论怎么样都可以实现接口,而指针接收者需要特定情况才可以实现接口呢?在3.1小节中Dog
类型是实现了Move方法的,那么根据Go语言语法糖,*Dog
会自动取值到Dog
,然后实现Move方法,而Cat
类型是没有实现Move方法,*Cat
实现了,这时候Cat
不能通过指针自动索引到*Cat
的Move方法中去,所以导致无法编译。
语法糖(题外话了解就行,可以不看):
- 语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
- 之所以叫「语法」糖,不只是因为加糖后的代码功能与加糖前保持一致,更重要的是,糖在不改变其所在位置的语法结构的前提下,实现了运行时的等价。可以简单理解为,加糖后的代码编译后跟加糖前一毛一样。
- 之所以叫语法「糖」,是因为加糖后的代码写起来很爽,包括但不限于:代码更简洁流畅,代码更语义自然。写着爽,看着爽,就像吃了糖。效率高,错误少。
- 据说还有一种叫做「语法盐」的东西,主要目的是通过反人类的语法,让你更痛苦的写代码。其实它同样能达到避免代码书写错误的效果,但编程效率应该是降低了,毕竟提高了语法学习门槛,让人咸到忧伤。
- Go的指针取值语法糖就是
.
,因为在C语言中通过[*ptr.]
或者[ptr->]
的方式来使用指针,而Go只需要使用.
肆 类型与接口的关系
4.1 一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间是彼此独立的,不知道另一个接口的实现。例如狗不仅可以叫,还可以动。我们完全可以分别定义Sayer
接口和Mover
接口,具体代码示例如下:
package main
import "fmt"
// Sayer 接口
type Sayer interface {
Say()
}
// Mover 接口
type Mover interface {
Move()
}
type Dog struct {
Name string // 狗的名字
}
// Dog既可以实现Sayer接口,也可以实现Mover接口。
func (d Dog) Say() {
fmt.Println(d.Name, "汪汪汪~")
}
func (d Dog) Move() {
fmt.Println(d.Name, "会动~")
}
func main() {
var d = Dog{Name: "狗狗"}
// 同一个类型实现不同的接口互相不影响使用
var s Sayer = d
var m Mover = d
s.Say() // 对Sayer类型调用Say方法
m.Move() // 对Mover类型调用Move方法
}
4.2 多种类型实现同一接口
Go语言中不同的类型还可以实现同一接口。例如在我们的代码世界中不仅狗可以动,汽车也可以动。我们可以使用如下代码体现这个关系。
package main
import "fmt"
// Mover 接口
type Mover interface {
Move()
}
// Car结构体类型
type Car struct {
Brand string
}
// Car类型实现Mover接口
func (c Car) Move() {
fmt.Printf("%s速度70迈\n", c.Brand)
}
// Dog结构体类型
type Dog struct {
Name string // 狗的名字
}
// Dog类型实现Mover接口
func (d Dog) Move() {
fmt.Printf("%s会动\n", d.Name)
}
func main() {
var obj Mover
var c = Car{Brand: "法拉利"}
var d = Dog{Name: "狗狗"}
// 把狗和汽车当成一个会动的类型来处理,不必关注它们具体是什么,只需要调用它们的Move方法就可以了
obj = c
obj.Move() // Car,对Mover类型调用Move方法
obj = d
obj.Move() // Dog,对Mover类型调用Move方法
}
一个接口的所有方法,不一定需要由一个类型完全实现,接口的方法可以由一个或者多个其他类型或者结构体来实现。
// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}
// 甩干器
type dryer struct{}
// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}
// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}
// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}
伍 接口之间的组合(没理解)
接口与接口之间可以通过互相嵌套形成新的接口类型,例如Go标准库io
源码中就有很多接口之间互相组合的示例。
// src/io/io.go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// ReadWriter 是组合Reader接口和Writer接口形成的新接口类型
type ReadWriter interface {
Reader
Writer
}
// ReadCloser 是组合Reader接口和Closer接口形成的新接口类型
type ReadCloser interface {
Reader
Closer
}
// WriteCloser 是组合Writer接口和Closer接口形成的新接口类型
type WriteCloser interface {
Writer
Closer
}
对于这种由多个接口类型组合形成的新接口类型,同样只需要实现新接口类型中规定的所有方法就算实现了新接口的子接口类型。
接口也可以作为结构体的一个字段,我们来看一段Go标准库sort
源码中的示例。
// src/sort/sort.go
// Interface 定义通过索引对元素排序的接口类型
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
// reverse 结构体中嵌入了Interface接口
type reverse struct {
Interface
}
下面没理解!
通过在结构体中嵌入一个接口类型,从而让该结构体类型实现了该接口类型,并且还可以改写该接口的方法。
// Less 为reverse类型添加Less方法,重写原Interface接口类型的Less方法
func (r reverse) Less(i, j int) bool {
return r.Interface.Less(j, i)
}
Interface
类型原本的Less
方法签名为Less(i, j int) bool
,此处重写为r.Interface.Less(j, i)
,即通过将索引参数交换位置实现反转。
在这个示例中还有一个需要注意的地方是reverse
结构体本身是不可导出的(结构体类型名称首字母小写),sort.go
中通过定义一个可导出的Reverse
函数来让使用者创建reverse
结构体实例。
func Reverse(data Interface) Interface {
return &reverse{data}
}
这样做的目的是保证得到的reverse
结构体中的Interface
属性一定不为nil
,否者r.Interface.Less(j, i)
就会出现空指针panic。
此外在Go内置标准库database/sql
中也有很多类似的结构体内嵌接口类型的使用示例,各位读者可自行查阅。
陆 空接口(非常重要)
6.1 空接口的定义
空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
package main
import "fmt"
// 空接口
// Any 不包含任何方法的空接口类型
type Any interface{}
// Dog 狗结构体
type Dog struct{}
func main() {
var x Any
x = "你好" // 字符串型
fmt.Printf("type:%T value:%v\n", x, x)
x = 100 // int型
fmt.Printf("type:%T value:%v\n", x, x)
x = true // 布尔型
fmt.Printf("type:%T value:%v\n", x, x)
x = Dog{} // 结构体类型
fmt.Printf("type:%T value:%v\n", x, x)
}
通常我们在使用空接口类型时不必使用type
关键字声明,可以像下面的代码一样直接使用interface{}
。
var x interface{} // 声明一个空接口类型变量x
6.2 空接口的应用
6.2.1 空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
package main
import "fmt"
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
func main() {
str := "这是字符串"
show(str)
int1 := 1
show(int1)
type dog struct {
Name string
}
d := dog{
Name: "狗狗",
}
show(d)
}
6.2.2 空接口作为map的值
使用空接口实现可以保存任意值的字典。
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "沙河娜扎"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
柒 接口值
由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体值之外,还需要记录这个值属于的类型。也就是说接口值由类型和值组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型
和动态值
。
我们接下来通过一个示例来加深对接口值的理解。
下面的示例代码中,定义了一个Mover
接口类型和两个实现了该接口的Dog
和Car
结构体类型。
// 定义了一个Mover接口类型
type Mover interface {
Move()
}
// 定义了一个Dog类型
type Dog struct {
Name string
}
func (d *Dog) Move() {
fmt.Println("狗在跑~")
}
// 定义了一个Cat类型
type Car struct {
Brand string
}
func (c *Car) Move() {
fmt.Println("汽车在跑~")
}
首先,我们创建一个Mover
接口类型的变量m
。
var m Mover
此时,接口变量m
是接口类型的零值,也就是它的类型和值部分都是nil
,就如下图所示:
我们可以使用m == nil
来判断此时的接口值是否为空。
fmt.Println(m == nil) // true
注意:我们不能对一个空接口值调用任何方法,否则会产生panic。
m.Move() // panic: runtime error: invalid memory address or nil pointer dereference
接下来,我们将一个*Dog
结构体指针赋值给变量m
。
m = &Dog{Name: "旺财"}
此时,接口值m
的动态类型会被设置为*Dog
,动态值为结构体变量的拷贝。
然后,我们给接口变量m
赋值为一个*Car
类型的值。
m = new(Car)
这一次,接口值自动将动态类型设置为*Car
,而动态值清空,设置为nil
。
注意:此时接口变量m
与nil
并不相等,因为它只是动态值的部分为nil
,而动态类型部分保存着对应值的类型。
fmt.Println(m == nil) // false
接口值是支持相互比较的,当且仅当接口值的动态类型和动态值都相等时才相等。
var (
x Mover = new(Dog)
y Mover = new(Car)
)
fmt.Println(x == y) // false
但是有一种特殊情况需要特别注意,如果接口值的保存的动态类型相同,但是动态值却不确定,例如:切片和map类型,由于本身说占的内存不确定,值也就无法比较,如果对它们相互比较时就会引发panic。
var a interface{} = map[string]string{"name": "a7cc"}
var b interface{} = map[string]string{"name": "a7cc"}
var c interface{} = []int{1, 2, 3}
var d interface{} = []int{1, 2, 3}
fmt.Println(a == b) // panic: runtime error: comparing uncomparable type []int
fmt.Println(c == d) // panic: runtime error: comparing uncomparable type []int
捌 类型断言
接口值可能被赋为任意类型的值,那我们如何从接口值获取其存储的具体数据和类型呢?换句话说,我们如何知道接口值的类型?
可能有些人会觉得,传参,然后使用fmt包,不就知道了吗?但是有个问题,使用fmt是我们可以看到的输出出来的,可是系统并不知道我们传的是什么类型数据,他们是不会进行识别的,况且fmt包也是用到后面的反射机制在程序运行时获取到类型的名称的,而不是自动识别的。这时候想要从接口值中获取到对应的实际值和类型,就需要使用类型断言,其语法格式如下:
x.(T)
// x:表示接口类型的变量
// T:表示判断x可能是的类型。
该语法返回两个参数,第一个参数是x
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
举个例子:
package main
import (
"fmt"
)
type Mover interface{}
type Dog struct {
Name string
}
func main() {
var n Mover = &Dog{Name: "旺财"}
v, ok := n.(*Dog)
if ok {
fmt.Println("类型断言成功")
v.Name = "富贵" // 变量v是*Dog类型
} else {
fmt.Println("类型断言失败")
}
}
如果对一个接口值有多个实际类型需要判断,推荐使用switch
语句来实现。
// justifyType 对传入的空接口类型变量x进行类型断言
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
由于接口类型变量能够动态存储不同类型值的特点,所以很多初学者会滥用接口类型(特别是空接口)来实现编码过程中的便捷。只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。切记不要为了使用接口类型而增加不必要的抽象,导致不必要的运行时损耗。
在 Go 语言中接口是一个非常重要的概念和特性,使用接口类型能够实现代码的抽象和解耦,也可以隐藏某个功能的内部实现,但是缺点就是在查看源码的时候,不太方便查找到具体实现接口的类型。
请牢记接口是一种类型,一种抽象的类型。区别于我们在之前章节提到的那些具体类型(整型、数组、结构体类型等),它是一个只要求实现特定方法的抽象类型。
小技巧: 下面的代码可以在程序编译阶段验证某一结构体是否满足特定的接口类型。
// 摘自gin框架routergroup.go
type IRouter interface{ ... }
type RouterGroup struct { ... }
var _ IRouter = &RouterGroup{} // 确保RouterGroup实现了接口IRouter
上面的代码中也可以使用var _ IRouter = (*RouterGroup)(nil)
进行验证。