Go语言——接口interface详解
[TOC]
1、Duck Typing 概念
go语言中的duck typing并不是真正的duck typing,但是他是类似的概念,go语言接口的实现就可以看做为duck typing。举例:什么是鸭子?
其他面向对象的语言可能认为,活生生的鸭子才是鸭子,要定义它的属性和方法。但是go语言中,鸭子的定义并不是真实的对象,它是由使用者来决定到底什么是鸭子。比如,真的鸭子和玩具的鸭子。像鸭子走路,像鸭子叫(长得像鸭子),那么就是鸭子。所以,go语言中的接口就是这样,它不必显示的去声明它,它只关注是否实现了相应的方法。它描述的事物的外部行为,而非内部结构
面向对象的继承、抽象接口等等目的都是代码的复用。既然是复用,那就要从使用者的角度去想,我认为是什么样子它就是什么样子。我只关心这段代码结构能做哪些事情,我复用它,我才不管它符不符合常识。
go语言就是一个结构化类型系统,类似 duck typing。只要实现了接口的所有方法,就表示该类型实现了该接口。
2、GO 语言interface特点
- 接口是一个或多个方法签名的集合
- 只要某个类型拥有该接口的所有方法签名,即算实现该接口,无需显示声明实现了哪个接口,这称为 Structural Typing
- 接口只有方法声明,没有实现,没有数据字段
- 接口可以匿名嵌入其它接口,或嵌入到结构中
- 将对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针
- 只有当接口存储的类型和对象都为nil时,接口才等于nil
- 接口调用不会做receiver的自动转换
- 接口同样支持匿名字段方法
- 接口也可实现类似OOP中的多态
- 空接口可以作为任何类型数据的容器
3、接口定义
3.1 接口类型
go语言中的接口也是一种类型,他具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。 定义接口很简单,使用type关键字,其实就是定义一个结构体,但是内部只有方法的声明,没有实现。
type Stringer interface {//接口的定义就是如此的简单。
String() string
}
3.2 接口的实现方式
go语言接口的独特之处就是,不需要显示的去实现接口。一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。 这种隐式实现接口的方式,同时也提高了灵活性。
type Stringer interface {
String() string
}
type Printer interface {
Stringer // 接口嵌⼊。
Print()
}
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("user %d, %s", self.id, self.name)
}
func (self *User) Print() {
fmt.Println(self.String())
}
func main() {
var t Printer = &User{1, "Tom"} // *User ⽅法集包含 String、 Print。
t.Print()
}
上面的代码可以看到,一个类型只要实现了接口定义的所有方法(是指有相同名称、参数列表 (不包括参数名) 以及返回值 ),那么这个类型就实现了这个接口,可以说这个类型现在是这个接口类型,可以直接进行赋值(其实也是隐式转换),比如var t Printer = &User{1, "Tom"}
。 那么既然如此,一个类型就可以实现多个接口,只要它拥有了这些接口类型的所有方法,那么这个类型就是实现了多个接口。同时这个类型也就是多种形式的存在,反过来说一个接口可以被不同类型实现,这就是go语言中的多态了。
3.3 interface{}空接口的实现
空接⼝ interface{} 没有任何⽅法签名,也就意味着任何类型都实现了空接⼝。其作⽤类似⾯向对象语⾔中的根对象 object。
3.4 类型断言
一个类型断言检查接口类型实例是否为某一类型 。语法为x.(T) ,x为类型实例,T为目标接口的类型。比如
value, ok := x.(T)
x :代表要判断的变量,T :代表被判断的类型,value:代表返回的值,ok:代表是否为该类型。即:ok partern方式。
不过我们一般用switch进行判断,叫做 type switch。注意:不支持fallthrough.
func main() {
var o interface{} = &User{1, "Tom"}
switch v := o.(type) {
case nil: // o == nil
fmt.Println("nil")
case fmt.Stringer: // interface
fmt.Println(v)
case func() string: // func
fmt.Println(v())
case *User: // *struct
fmt.Printf("%d, %s\n", v.id, v.name)
default:
fmt.Println("unknown")
}
}
3.5 接口转换
可以将拥有超集的接口转换为子集的接口。
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("%d, %s", self.id, self.name)
}
func main() {
var o interface{} = &User{1, "Tom"}
if i, ok := o.(fmt.Stringer); ok { // ok-idiom
fmt.Println(i)
}
u := o.(*User)
// u := o.(User) // panic: interface is *main.User, not main.User
fmt.Println(u)
}
通过类型判断,如果不同类型转换会发生panic.
3.6 匿名接口
匿名接口可用作变量类型,或者是结构成员。
type Tester struct {
s interface {
String() string
}
}
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("user %d, %s", self.id, self.name)
}
func main() {
t := Tester{&User{1, "Tom"}}
fmt.Println(t.s.String())
}
//输出:
user 1, Tom
4、接口的内部实现
4.1 接口值
接口值可以使用 == 和 !=来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。
然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片) ,将它们进行比较就会失败并且panic。
那么接口值内部到底是什么结构呢?
4.2 接口内部结构
// 没有方法的interface
type eface struct {
_type *_type
data unsafe.Pointer
}
// 记录着Go语言中某个数据类型的基本特征
type _type struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
gcdata *byte
str nameOff
ptrToThis typeOff
}
// 有方法的interface
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32
bad bool
inhash bool
unused [2]byte
fun [1]uintptr
}
// interface数据类型对应的type
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
go语言interface的源码表示,接口其实是一个两个字段长度的数据结构。所以任何一个interface变量都是占用16个byte的内存空间。从大的方面来说,如图:
var n notifier n=user("Bill")
将一个实现了notifier接口实例user赋给变量n。那我们先来看有方法的接口的内部是怎么样的。接口n 内部两个字段 tab *itab 和 data unsafe.Pointer, 第一个字段存储的是指向ITable(j接口表)的指针,这个内部表包括已经存储值得类型和与这个值相关联的一组方法。第二个字段存储的是,指向所存储值的指针。注意:这里是将一个值传递给数组,并非指针,那么就会先将值拷贝一份,开辟内存空间存储,然后将此内存地址赋给接口的data字段。也就是说,值传递时,接口存储的值的指针其实是指向一个副本。
如果是将指针赋值给接口类型,那么第二个字段data存储的就是指针的拷贝,指向的是原来的内存。
再进一步了解,内部是如何存储的。
没有方法的interface 内部变量第一个字段为*_type
类型,这个_type
记录这某种数据类型的基本信息,比如占用内存大小(size),数据类型名称等等。然后,第二个字段存储的还是指向值的指针。
每种数据类型都存在一个与之对应的_type结构体(Go语言原生的各种数据类型,用户自定义的结构体,用户自定义的interface等等)。
在这里引用的https://www.jianshu.com/p/700...:
小结:总的来说接口是一个类型,它是一个struct,是一个或多个方法的集合。任何类型都可以实现接口,并且是隐式实现,可以同时实现多个接口。接口内部只有方法声明没有实现。接口内部存储的其实就是接口值的类型和值,一部分存储类型等各种信息,另一部分存储指向值的指针。如果是将值传给接口,那么这里第二个字段存储的就是原值的副本的指针。接口可以调用实现了接口的方法。
5、方法集
5.1 方法集定义
方法集:方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接受者的类型决定了这个方法时关联到值,还是关联到指针,还是两个都关联。
// 这个示例程序展示 Go 语言里如何使用接口
package main
import (
"fmt"
)
// notifier 是一个定义了
// 通知类行为的接口
type notifier interface {
notify()
}
// user 在程序里定义一个用户类型
type user struct {
name string
email string
}
// notify 是使用指针接收者实现的方法
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}
// main 是应用程序的入口
func main() {
// 创建一个 user 类型的值,并发送通知30 u := user{"Bill", "bill@email.com"}
sendNotification(u)
// ./listing36.go:32: 不能将 u(类型是 user)作为
// sendNotification 的参数类型 notifier:
// user 类型并没有实现 notifier
// (notify 方法使用指针接收者声明)
}
// sendNotification 接受一个实现了 notifier 接口的值
// 并发送通知
func sendNotification(n notifier) {
n.notify()
}
如上面代码,当为struct实现接口的方法notify()方法时,定义的接受者receiver是一个指针类型,所以,它要遵循方法集的规则,如果方法集的receiver 是T 即指针类型,那么属于接口的值必须同样是 T 指针类型。
user 实现了notify 方法,也就是它实现了notifier 接口,当时如果将user 实例传给notifier实例,必须是一个指针类型,因为它实现的方法的receiver是一个指针类型。所以方法集的作用也就是规范接口的实现。
5.2 方法集规则
Values Methods Receivers
-----------------------------------------------
T (t T)
*T (t T) and (t *T)
Methods Receivers Values
-----------------------------------------------
(t T) T and *T
(t *T) *T
如果方法的接受者是 指针类型 ,那么用指针接受者方式实现这个接口,只有指向那个类型的指针才能够算实现对应的接口,所以接口值接收的只能也是一个指针类型。
如果方法的接受者是 值类型,那么用值接收者实现接口,那个类型的值和指针都能够实现对应的接口。
简单讲就是,接受者是(t T),那么T 和 T 都可以实现接口,如果接受者是(t T)那么只有 *T才算实现接口。
反过来看稍微复杂点,判断这个类型变量是否实现了接口,看一下他是值类型还是指针类型,如果是T 值类型,那就看它实现接口方法的receiver是什么类型,如果也是值类型,那么它就实现了接口,如果不是,就没有实现,就不能进行传递。如果他是指针类型,那么不管它的receiver是值还是指针都实现了接口。所以记住上面的图就好。
有疑问加站长微信联系(非本文作者)