Go语言基础之接口
Go语言基础之接口
1. 介绍
在面向对象的领域里,接口一般这样定义:接口定义一个对象的行为。接口只指定了对象应该做什么,至于如何实现这个行为(即实现细节),则由对象本身去确定。
在 Go 语言中,接口就是方法签名(Method Signature)的集合。当一个类型定义了接口中的所有方法,我们称它实现了该接口。这与面向对象编程(OOP)的说法很类似。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法。
- 接口是一种类型,也是一种抽象结构的概括,不会和特定的实现细节绑定在一起,不会暴露所含数据的格式、类型及结构。
- 很多面向对象的语言都有相似的接口概念:
传统 java c#:
- 传统的派生式接口及类关系构建的模式,让类型间拥有强耦合的父子关系。这种关系一般会以“类派生图”的方式进行。
- 经常可以看到大型软件极复杂的派生树,随着系统的功能不断增加,这棵“派生树”会变得越来越复杂。
- 实现者在编写方法时,无法预测未来哪些方法会变为接口。一旦某个接口创建出来,要求旧的代码来实现这个接口时,就需要修改旧的代码的派生部分,这一般会造成雪崩式的重新编译。
Go语言:
- Go的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计称为非侵入式设计。非侵入式设计让实现者的所有类型均是平行的、组合的。如何组合则留到使用者编译时再确认。
- 这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。
- Go语言的每个接口中的方法数量不要很多。Go语言希望通过一个接口精准描述它自己的功能,而通过多个接口的嵌入和组合的方式将简单的接口扩展为复杂的接口。
- 因此,使用GO语言时,不需要同时也不可能有“类派生图”,开发者唯一需要关注的就是“我需要什么?”,以及“我能实现什么?”。
2. 接口类型
在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface
是一组method
的集合,是duck-type programming
的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。
用来给变量、参数、返回值等设置类型。
duck typing
描述事物的外部行为而非内部结构
严格说go属于结构化类型语言,类似duck typing
- 接口由使用者定义
- 接口的实现是隐式的
- 只要实现接口里面的方法
注意
方法声明的集合
任何类型的对象实现了在接口中声明的全部方法,则表明该类型实现了对应接口。
可以作为一种数据类型,实现了该接口的任何对象都可以给对应的接口类型变量赋值。
3. 为什么要使用接口
type Cat struct{} |
上面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢?
像类似的例子在我们编程过程中会经常遇到:
比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?
比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?
比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢?
Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。
4. 接口的定义
Go语言提倡面向接口编程。
每个接口由数个方法组成,接口的定义格式如下:
type 接口类型名 interface{ |
其中:
接口名:使用
type
将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er
(行业规范),如有写操作的接口叫Writer
,有字符串功能的接口叫Stringer
,有关闭功能的接口叫 Closer 等。接口名最好要能突出该接口的类型含义。方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个例子:
type writer interface{ |
当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。
5. 接口的实现
一个对象只要全部实现了接口中规定的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
实现接口需要满足2个条件:
- 条件1: 接口的方法与实现接口的类型方法格式一致
- 条件2: 接口中所有方法均被实现
我们来定义一个Sayer
接口:
// Sayer 接口 |
定义dog
和cat
两个结构体:
type dog struct {} |
因为Sayer
接口里只有一个say
方法,所以我们只需要给dog
和cat
分别实现say
方法就可以实现Sayer
接口了。
// dog实现了Sayer接口 |
接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。
注意
实现接口要求按照接口中的方法名,参数列表,返回列表一致来实现。
举个例子:
// 定义一个数据写入器 |
错误1:函数名不一致导致
假设修改 file 结构的 WriteData() 方法名,将这个方法签名(第9行)修改如下,然后编译:
func (d *file) WriteDataX(data interface{}) error { ... } |
panic:cannot use f (type *file) as type DataWriter in assignment: *file does not implement DataWriter (missing WriteData method) |
错误2:实现接口的方法签名不一致导致的报错
假设修改 file 结构的 WriteData() 方法名,把 data 参数的类型从 interface{} 修改为 int 类型,然后编译:
func (d *file) WriteData(data int) error { ... } |
panic:cannot use f (type *file) as type DataWriter in assignment: |
接口中所有方法均被实现
当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。
为 DataWriter中 添加一个CanWrite方法,代码如下,然后编译:
// 定义一个数据写入器 |
panic:cannot use f (type *file) as type DataWriter in assignment: |
需要在 file 中实现 CanWrite() 方法才能正常使用 DataWriter()。
6. 接口类型变量
那实现了接口有什么用呢?
接口类型变量能够存储所有实现了该接口的实例(可以把多个类型的变量通过是否含有某些方法而统一起来为一个接口类型)。 例如上面的示例中,Sayer
类型的变量能够存储dog
和cat
类型的变量。
接口保存的分为两部分:值的类型和值本身
。
type Sayer interface { |
Tips: 观察下面的代码,体味此处_
的妙用
// 摘自gin框架routergroup.go |
7. 值接收者和指针接收者实现接口的区别
定义满足接口的方法
使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。
我们有一个Mover
接口和一个dog
结构体。
type Mover interface { |
7.1 值接收者实现接口
即 func (d dog) speak(){};
func (d dog) move() { |
此时实现接口的是dog
类型:
func main() { |
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui
内部会自动求值*fugui
。
7.2 指针接收者实现接口
即func (d *dog) speak(){};
指针接受者实现接口只能存结构体指针类型的变量。
同样的代码我们再来测试一下使用指针接收者有什么区别:
func (d *dog) move() { |
此时实现Mover
接口的是*dog
类型,所以不能给x
传入dog
类型的wangcai,此时x只能存储*dog
类型的值。
8. 类型与接口的关系
8.1 一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口,如下: Mover
接口。
// Sayer 接口 |
dog既可以实现Sayer接口,也可以实现Mover接口。
type dog struct { |
8.2 多个类型实现同一接口
下图即为传入含有Read方法的Reader接口类型即可,不管结构体的类型:
Go语言中不同的类型还可以实现同一接口 首先我们定义一个Mover
接口,它要求必须由一个move
方法。
// Mover 接口 |
例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:
type dog struct { |
这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move
方法就可以了。
func main() { |
上面的代码执行结果如下:
# 旺财会跑 |
并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
// WashingMachine 洗衣机 |
8.3 函数类型实现接口
其他类型能够实现接口,函数也可以:
package main |
输出如下:
from struct hello |
8.4 http 例子
HTTP 包中有 Handler 接口定义,用于定义每个 HTTP 的请求和响应的处理过程,代码如下:
type Handler interface { |
同时,也可以使用处理函数实现接口,定义如下:
type HandlerFunc func(ResponseWriter, *Request) |
要使用闭包实现默认的 HTTP 请求处理,可以使用 http.HandleFunc() 函数,函数定义如下:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { |
而 DefaultServeMux 是 ServeMux 结构,拥有 HandleFunc() 方法,定义如下:
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { |
上面代码将外部传入的函数 handler() 转为 HandlerFunc 类型,HandlerFunc 类型实现了 Handler 的 ServeHTTP 方法,底层可以同时使用各种类型来实现 Handler 接口进行处理。
9. 接口嵌套
接口与接口间可以通过嵌套创造出新的接口。实现接口时必须实现嵌套接口内部的所有方法。
一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用。
// Sayer 接口 |
嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:
type cat struct { |
Go语言的 io 包中定义了写入器(Writer)、关闭器(Closer)和写入关闭器(WriteCloser)3 个接口:
type Writer interface { |
10. 空接口
10.1 nil 的判断
nil 在 Go语言中只能被赋值给指针和接口。接口在底层的实现有两个部分:type 和 data。
显式地将 nil 赋值给接口时,接口的 type 以及 data 都将为 nil,此时接口与 nil 值判断是相等的。
带类型的 nil 赋值给接口时,接口的 type 不为 nil,data 为 nil,此时接口与 nil 值判断是不相等的。
// 定义一个结构体 |
输出:
GetStringer() != nil |
发现 nil 类型值返回时直接返回 nil
为了避免这类误判的问题,可以在函数返回时,发现带有 nil 的指针时直接返回 nil,代码如下:
func GetStringer() fmt.Stringer { |
10.2 什么是空接口?
空接口是接口类型的特殊形式,空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。
空接口类型类似于 C# 或 Java 语言中的 Object、C语言中的 void*、C++ 中的 std::any。在泛型和模板出现前,空接口是一种非常灵活的数据抽象保存和使用的方法。
golang中的所有程序都实现了interface{}的接口。这意味着所有的类型如string、int、int64、struct、map、slice等类型都就此拥有了interface{}的接口,这种做法和java中的Object类型比较类似。空接口在Go语言中真正的意义是支持多态。
空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。
在一个数据通过函数的方式传进来时,且参数为空接口,也就意味着这个参数被自动的转为interface{}的类型。
空接口
type x interface{}
在函数定义时参数可用空接口(interface{}
可匿名,没有必要起名字),可传递任何类型参数 或者map类型的value可以是任何类型。空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
10.3 接口的定义
值保存到空接口
var any interface{} |
从空接口获取值
// 声明a变量, 类型int, 初始值为1 |
第8行代码编译报错,不能将i变量视为int类型赋值给b:
panic:cannot use i (type interface {}) as type int in assignment: need type assertion
为了让第 8 行的操作能够完成,编译器提示我们得使用 type assertion (类型断言),请参阅下节。
10.4 空接口的值比较
类型不同的空接口间的比较结果不相同
// a保存整型 |
不能比较空接口中的动态值
// c保存包含10的整型切片 |
运行到第6行时,出现运行时错误,提示 []int 是不可比较的类型:
**panic:** runtime error: comparing uncomparable type []int
列举类型及比较的几种情况
10.5 空接口的应用(常用场景)
空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数 |
空接口作为map的值
使用空接口实现可以保存任意值的字典。
// 空接口作为map值 |
11. 类型断言
空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?
11.1 介绍
当空接口进行转换数据类型时,go不像是其他语言可以直接转换,在go中需要使用断言,如下例子:
func funcName(a interface{}) string { |
panic:cannot convert a (type interface{}) to type string: need type assertion
11.2 接口值
一个接口的值(简称接口值)是由一个具体类型
和具体类型的值
两部分组成的。这两部分分别称为接口的动态类型
和动态值
。
我们来看一个具体的例子:
var w io.Writer |
请看下图分解:
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
x.(T) |
其中:
x:表示类型为
interface{}
的变量T:表示断言
x
可能是的类型。该语法返回两个参数,第一个参数是
x
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
举个例子:
func main() { |
上面的示例中如果要断言多次就需要写多个if
判断,这个时候我们可以使用switch
语句来实现:
func justifyType(x interface{}) { |
因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。
关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
11.3 转换
我们可以使用语法 x.(Type) 找出接口的基础动态值,其中 x 是接口,Type是实现接口x的类型。
value, ok := x.(Type) |
直接转换
❗️如果断言失败会导致panic的发生,示例:
var a interface{} |
panic: interface conversion: interface {} is string, not int
尝试转换
为了防止panic的发生,我们需要在断言前进行一定的判断,如果断言失败,那么ok的值将会是false,但是如果断言成功ok的值将会是true,同时value将会得到所期待的正确的值。示例:
value, ok := a.(string) |
类型开关
type-switch 流程控制的语法或许是Go语言中最古怪的语法。 它可以被看作是类型断言的增强版。它和 switch-case 流程控制代码块有些相似。
switch t := t.(type) { |
结构体转换
type InterfaceT interface { |
接口转换为其他接口
package main |
12. sort.Interface 排序
12.1 介绍
- 排序操作和字符串格式化是很多程序经常使用的操作。但是一个健壮的实现需要更多的代码,并且我们不希望每次我们需要的时候都重写或者拷贝这些代码。幸运的是,sort 包内置的提供了根据一些排序函数来对任何序列排序的功能。
- 在很多语言中,排序算法都是和序列数据类型关联,同时排序函数和具体类型元素关联。相比之下,Go语言的 sort.Sort 函数不会对具体的序列和它的元素做任何假设。相反,它使用了一个接口类型 sort.Interface 来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定,序列的表示经常是一个切片。
12.2 使用
一个内置的排序算法需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式;这就是 sort.Interface 的声明:
package sort |
我们可以通过实现sort.Interface来对字符串序列排序:
package main |
12.3 便捷
Go语言中已经提供了一些固定模式的封装以方便开发者迅速对内容进行排序。
常用到的 int32、int64、float32、bool 类型并没有由 sort 包实现,使用时依然需要开发者自己编写。
// demo |
12.4 sort.Slice 切片元素排序
从 Go 1.8 开始,Go语言在 sort 包中提供了 sort.Slice() 函数进行更为简便的排序方法。
sort.Slice() 函数只要求传入需要排序的数据,以及一个排序时对元素的回调函数。
使用 sort.Slice() 不仅可以完成结构体切片排序,还可以对各种切片类型进行自定义排序。
func Slice(slice interface{}, less func(i, j int) bool) |
例子如下:
package main |
13. 常用接口
13.1 Writer
io 包中提供的 Writer 接口
type Writer interface { |
13.2 Stringer
fmt 包中提供的 Stringer 接口,功能类似于 Java 或者 C# 语言里的 ToString 的操作。
type Stringer interface { |
示例:
type Student struct { |
参考感谢
interface 接口 · 语雀 (yuque.com)
Go语言基础之接口 · 语雀 (yuque.com)
07.接口(interface) · 语雀 (yuque.com)
接口 · 语雀 (yuque.com)
Go基础—— Interface · 语雀 (yuque.com)