接口(interface)是一种抽象的类型,是对其他类型行为的概括和抽象。从语法角度来看,接口是一组方法签名定义的集合。接口是调用方和实现方共同遵守的约定或协议(Protocol),即按照统一的方法命名参数类型和数量来协调逻辑处理的过程。
Go语言中接口是一种数据类型,用于定义行为方法。接口是一组方法定义的集合,定义了对象的一组行为。换句话说,接口就是定义(规范或约束)。接口自身并不会实现所定义的方法,具体实现由类来完成,实现接口的类必须按照接口的声明来实现接口所提供的所有功能。接口的功能是将定义与实现分离以降低耦合度。
Go语言中接口的独到之处在于接口是隐式实现的,也就是说,对于一个具体的类型,无需声明其实现了那些接口,只需要提供接口所必须的方法即可。这种设计让编程人员无需改变已有类型的实现,就可以为类型创建新的接口,对于那些不能修改包的类型特别实用。也就是说,这种设计让你创建全新的接口类型来满足已经存在的具体类型,却不会去改变具体类型的定义,特别是当使用的具体类型来自不受控制的包时尤为有用。
Go语言使用组合的方式来实现对象特性的描述,对象内部使用结构体内嵌组合对象所具有的特性,对外则通过接口暴露能够使用的特性。
鸭子类型Duck-Typing
对于强类型的静态语言,想要通过运行时多态来隔离变化,多个实现就必须属于同一个类型体系,必须通过继承的方式与同一抽象类型建立is-a
的关系。鸭子类型是一种基于特征,而非基于类型的多态方式。鸭子类型仍然关心is-a
,只不过is-a
关系是以对象是否具备相关的特性来确定的。
对于是否满足is-a
关系可使用所谓的鸭子测试Duck Test
进行判断,鸭子测试是基于特征的哲学,给设计提供了强大的灵活性。动态面向对象语言,比如Python、Ruby等都遵从鸭子测试来实现运行时多态。
”当看到一只鸟走起来、游起来、叫起来像鸭子,那么这只鸟就可以被成为鸭子。“
- 动态语言比如JavaScript、Python天然支持这种特性,相对于静态语言,动态语言的类型天生缺乏必要的类型检查。
Go语言的接口设计与鸭子模型有着密切的关系,接口是鸭子类型编程的一种体现,即不关心属性(数据)只关心行为(方法)。和动态语言的鸭子模型不通过的是在编译时,Go语言即可实现必要的类型检查。
Go语言作为静态语言对鸭子类型的支持是通过Structural Typing
来实现的,Structural Typing
是Go语言式的接口,不需要显式地声明类型T
实现了接口I
,只要类型T
的公开方法完全满足接口的要求,即可将类型T
的对象用在需要接口I
的地方。
接口声明
Go语言提供一种数据类型称之为接口,接口把所有具有共性的方法定义在一起,任何类型只要实现其方法即实现了此接口。Go语言中的接口是一组方法签名的集合,是一种抽象的数据类型,任何类型只要实现对应接口中的方法就可以认为属于这种类型。
-
interface
是一组方法的集合,但并不需要实现这些方法,同时interface
中没有变量。 -
interface
中的方法集合可以表示一个对象的特征和能力。 - 当自定义类型需要使用接口方法时,可根据需要实现方法。
每个接口类型由数个方法签名组成
type 接口类型名 interface {
//方法签名
方法名(参数列表) 返回值列表
...
}
- 接口类型名:使用
type
关键字将接口定义为自定义的类型名
Go语言接口命名时会在单词末尾添加er
,比如写操作接口为Writer
、字符串功能接口Stringer
、关闭功能接口Closer
... - 方法名:当方法名首字母为大写且接口类型名首字母也是大写时,此方法可以被接口所在的包之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略
例如:
type Writer interface {
Write([]byte) error
}
-
interface
本身不能创建实例,interface
类型的变量可以指向一个实现该接口的自定义类型的实例。 -
interface
默认是一个指针(引用类型),如果没有对interface
初始化就使用则会输出nil
。
接口实现
Go语言中实现接口的条件是,当一个【任意类型(T)】的方法集合是一个【接口类型】的方法集合的【超集】时,则认为任意类型T
实现了此接口类型。
任意类型T
可以是一个非接口类型,也可以是一个接口类型。
类型之间的实现关系在Go语言中隐式的,不需要在代码中显式的表示出来。
Go语言没有类似implments
关键字,Go语言编译器将自动在需要时检查类型之间的实现关系。
当接口定义完毕后下一步就需要实现接口,调用方才能正确编译通过并使用该接口。
接口的实现必须遵循两条规则才能让接口可用
- 接口的方法和实现接口的类型方法的格式必须保持一致
- 接口中的所有方法都必须被实现
接口的方法与实现接口的类型方法格式一致
只需要在类型中添加与接口签名一致的方法即可实现接口,方法签名包括方法名、参数列表、返回值列表三部分。也就是说,只要实现接口类型中的方法名、参数列表、返回值列表中的任意一项与接口要实现的方法不一致,那么接口中对应的方法就不能被实现。
例如:抽象数据写入的过程
定义一个名为DataWriter
数据写入器的接口来描述数据写入所需要实现的方法,数据写入器接口中的拥有一个WriteData()
的方法表示数据写入。
写入方无需关心写入到哪里,只需要实现接口的类型在实现WriteData()
方法时,会具体编写将数据写入到那种结构中。比如使用file
文件结构体来实现数据写入器接口的写入方法时,方法内部可直接打印日志表示有数据写入。
package main
import "fmt"
//定义数据写入器接口
type DataWriter interface {
//数据写入方法,传入一个空接口类型的data变量,返回error结构表示可能发生的错误。
WriteData(data interface{}) error
}
type File struct {
}
//定义结构体方法,使用指针接收器。
//输入一个空接口类型的变量data,返回error。
func (this *File) WriteData(data interface{}) error {
fmt.Printf("File WriteData: %v", data)
return nil
}
func main() {
//实例化文件结构体
file := new(File)
//声明数据写入器接口
var writer DataWriter
//将接口赋值为结构体实例,即*File类型。
//虽然二者类型不同,但writer是一个接口而且file已经完全实现了DataWrite()方法,因此可以赋值成功。
writer = file
//使用接口来写入数据
writer.WriteData("hello world")//File WriteData: hello world
}
内部结构
Jordan Oreilli:接口是两件事物,接口是一组方法,也是一种类型。
Russ Coxx在《关于接口内部结构的精彩文章》中解释到接口会由两个指针组成
其一是指向【类型】相关信息的指针
其二是指向【数据】相关信息的指针
通过定义接口将具体的实现和调用完全分离,其本质是引入一个中间层对不同的模块进行解耦,上层模块无需依赖某个具体的实现,只需以来一个已经定义好的接口。
interface
底层分别由两个结构体实现,分别是iface
和eface
。
结构体 | 全称 | 名称 | 描述 |
---|---|---|---|
eface | empty interface | 空接口 | 不包含任何方法 |
iface | non-empty interface | 非空接口 | 包含方法的接口 |
从概念上讲,eface
和iface
均由两部分组成,分别是type
和value
。
组成部分 | 描述 |
---|---|
type | 接口的类型描述,提供concrete type 相关的信息。 |
value | 指向接口绑定的具体数据 |
具体类型实例传递给接口称为接口的实例化,接口变量默认值为nil
,需初始化后才有意义。
eface
eface
空接口结构由两个属性组成,一个是类型信息_type
,一个是数据信息data
。
//eface 空接口
type eface struct{
//属性
_type *_type //类型信息
data unsafe.Pointer//数据信息
}
属性 | 名称 | 描述 |
---|---|---|
_type | 类型信息 | 所有类型的公共描述 |
data | 数据信息 | 指向具体的实例数据 |
-
_type
类型信息是Golang中所有类型的公共描述,Golang中所有的数据结构都可以抽象成_type
。_type
负责决定data
应该如何解释和操作。
type _type struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldalign uint8
alg *typeAlg
gcdata *byte
str nameOff
ptrToThis typeOff
}
-
data
数据信息表示指向具体的实例数据,由于Golang的参数传递规则是值传递,如果希望通过interface
对实例数据修改,则需要传入指针,此时data
指向的是指针的副本,但指针指向的实例地址不变,仍然可以对实例数据产生修改。
iface
iface
表示non-empty interface
的数据结构,非空接口初始化的过程就是初始化一个iface
类型的结构,其中data
的作用和eface
的相同。
//iface 非空接口
type iface struct {
tab *itab
data unsafe.Pointer //数据信息,具体的数据
}
itab
iface
非空接口结构中最重要的是itab
结构,每个itab
占32bytes的内存空间。
-
itab
可理解为pair<interface type, concrete type>
-
itab
中包含了接口的关键信息
type itab struct {
inter *interfacetype //接口自身的元信息
_type *_type //具体类型的元信息
link *itab
bad int32
hash int32 //为方便运行接口断言
fun [1]uintptr //函数指针,指向具体类型所实现的方法
}
inter
字段
-
inter
包含了接口自身的元信息,比如包路径pkgpath
,方法集合mhdr
等。 -
iface
和eface
是数据类型转换成interface
之后的实体struct
结构,此处的interfacetype
则是定义interface
的一种抽象表示。
type interfacetype struct {
type _type
pkgpath name
mhdr []imethod
}
-
type
表示具体的类型,与eface
中的type
类型相同。
hash
字段
-
hash
是对_type.hash
的拷贝,会在interface
实例化时快速判断目标类型和接口中的类型是否一致。 - Golang中
interface
的Duck-typing机制也是依赖于hash
字段来实现的。
fun
字段
-
fun
字段是一个动态大小的数组,声明时固定大小为1,使用时会通过fun
指针获取其中的数据。 -
fun
不会检查数组的边界,因此该数组中保存的元素数量是不确定的。
小结
Go语言的接口设计是非入侵式的,接口编写者无需知道被哪些类实现,接口实现者只需要知道应该实现什么样子的接口,但无须指明实现是的哪一个接口。编译器会知道最终编译时所使用哪个类型实现哪个接口,或者接口应该由谁来实现。
缺点在于Duck-typing风格并不关注接口的规则和含义,也没法检查,不确定某个struct
具体实现了哪些interface
,只能通过goru
工具查看。
空接口类型
Go语言提供interface{}
表示空接口类型,可用于保存任何数据,作为参数可使用任意类型,作为参数的方法可接收任何类型。
type InterfaceName interface{}
可直接使用interface{}
作为空接口类型以表示空接口
var i interface{}
func fn(data interface{}){
}
-
interface{}
是一种指针类型的数据类型 -
interface{}
保存了两个指针,一个是对象的类型iTable
,另一个是对象的值。 -
interface{}
没有任何方法,所有类型都实现了空接口,因此任意类型的对象都能赋值给空接口实例。
interface{}
表示没有任何方法的接口,也就是所没有任何方法需要实现。由于所有类型都至少实现零个方法,因此会自动满足该接口,所以任何类型都满足空接口。由于interface{}
是隐式实现的,每种类型都满足空接口锲约,因此任何变量都可以赋值给interface{}
类型的变量。
Go语言中任何对象都可以实现interface{}
,任何对象也都可以保存在interface{}
实例变量中。
package main
import (
"fmt"
)
func main () {
var any interface{}
any = 1
fmt.Printf("any = %v, type = %T\n", any, any)//any = 1, type = int
any = "hello"
fmt.Printf("any = %v, type = %T\n", any, any)//any = hello, type = string
}
由于interface{}
拥有两个指针,内存布局上两个指针会占用2个机器字长。
为什么将切片中的数据拷贝到interface{}
切片中时会报错?
package main
import (
"fmt"
)
func main () {
slice := []int{1, 2, 3, 4}
var newSlice []int
newSlice = slice
fmt.Printf("slice = %v, newSlice = %v\n", slice, newSlice)//slice = [1 2 3 4], newSlice = [1 2 3 4]
var any []interface{}
any = slice//cannot use slice (type []int) as type []interface {} in assignment
}
因为每个interface{}
的内存布局都会占用两个机器字长的内容,对于长度为N的空接口切片而言,它的每个元素都是以2机器字长为单位的连续空间,因此会总共会占用2N个机器字长的空间。然后普通的切片,比如[]int
它的每个元素都是int
类型的,由于int
类型的内存布局和空接口不同。另外这些对象的内存布局在编译期就已经确定好了,所以不能直接将不同内存布局的数据结构进行拷贝。
若想要实现拷贝则需使用for-range
方式,将普通切片中的每个元素都赋值给空接口切片中的空接口元素形成一个个的空接口实例。
package main
import (
"fmt"
)
func main () {
slice := []int{1, 2, 3, 4}
var newSlice []int
newSlice = slice
fmt.Printf("slice = %v, newSlice = %v\n", slice, newSlice)//slice = [1 2 3 4], newSlice = [1 2 3 4]
var any []interface{}
for _,v := range slice{
any = append(any, v)
}
fmt.Printf("any = %v, type = %T\n", any, any)//any = [1 2 3 4], type = []interface {}
}
interface{}
中每个空接口实例都会指向更加底层的各个数据对象
小结
- 使用空接口表示任意数据类型,类似于Java中的
Object
。 - 空接口可以存储任意类型的值,类似于C语言中的
void *
类型。 - 空接口类型让Go语言像其它动态语言一样,在数据结构中存储任意类型的数据。
可定义interface{}
的类型包括array
、slice
、map
、struct
等,用来存放任意类型的对象,因为任何类型都实现了空接口。
例如:创建空接口类型的切片
package main
import (
"fmt"
)
func main () {
any := make([]interface{}, 4)
any[0] = 1
any[1] = "admin"
any[2] = []int{1, 2, 3}
for _,v := range any{
fmt.Printf("type = %T, value = %v\n", v, v)
}
}
type = int, value = 1
type = string, value = admin
type = []int, value = [1 2 3]
type = , value =
接口继承
- 一个接口可继承多个接口,若要实现实现子接口就必须将所继承父接口中的方法都实现。
接口型函数
接口型函数是指使用函数实现接口,这样在调用时会非常简单,这种方式适用于只有一个函数的接口。
典型接口型函数的应用是在编写HTTP服务时会使用http.Handle
方法来注册pattern
对应的Handler
HTTP包中定义了Handler接口,Handler用于定义每个HTTP请求和响应的处理过程。
type Handler interface{
ServeHTTP(ResponseWriter, *Request)
}
根据ServeHTTP接口来定义HandlerFunc普通函数,同时此函数又实现了ServeHTTP接口,直接调用函数本身。
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request){
f(w, r)
}
HandlerFunc
类型实现了ServeHTTP
函数,因此HandlerFunc
是一个Handler
。同时HandlerFunc
类型又是一个参数类型为(ResponseWriter, *Request)
的函数。像HandlerFunc
这样的函数就被称为接口型函数。
类型断言 Type Assertion
- 当不确定某个接口变量存储的是什么类型的变量时,可使用类型断言来判断变量类型。
- 类型断言时若类型不匹配则会报
panic
,因此需添加检测机制,若成功则ok
否则也不要报panic
。 - 可使用
.(TYPE)
将一个接口变量转化为一个显式的类型 - 可使用
switch-type
进行类型断言
例如:使用空接口与转化实现断言
package main
import (
"fmt"
)
func assign(arg interface{}){
switch t := arg.(type){
case string:
fmt.Printf("content = %s, type = %T\n", t, t)
case int:
fmt.Printf("content = %d, type = %T\n", t, t)
case bool:
fmt.Printf("content = %v, type = %T\n", t, t)
}
}
func main () {
assign(1)
}