1、Duck Typing 概念
- 它描述的事物的外部行为,而非内部结构
- 代码的复用,开发者认为是什么样子它就是什么样子。我只关心这段代码结构能做哪些事情,我复用它,内部结构我不care
- 可以用于多态的实现
非入侵式的duck typing到底有多好?
- 侵入式缺点
+ 通过 implements 把实现类与具体接口绑定起来了,因此有了强耦合;
+ 如果我修改了接口,比如改了接口方法,则实现类必须改动,如果我希望实现类再实现一个接口,实现类也必须进行改动;
- 非入侵式优点
+ 可以根据实际情况把类的功能做好,在具体需要使用的地方,我再定义接口。说的专业点:也就是接口是由使用方根据自己真实需求来定义,并且不用关心是否有其它使用方定义过
- 优点举例
+ 开发一个商城系统,m端、app端、pc端都有购物车的需求,底层根据不同的需求已经实现了一个Cart类,通过该类可以获取购物车价格、数量等。例如:
1 | go复制代码type Cart struct { |
+ 不同的高层调用时,他们可以自由定义接口名称用于接受Cart实例,再通过接口调用相应的方法就好了,不同的高层完全可以自己定义一个接口,接口名称、定义的方法顺序都可以不同。
- 总结:真正做到了:依赖于接口而不是实现,优先使用组合而不是继承
2、接口定义
2.1 接口类型
1 | typescript复制代码type Stringer interface {//接口的定义就是如此的简单。 |
2.2 接口的实现方式
- 不需要显示的去实现接口。一个类型如果拥有一个接口需要的所有方法,那么这个类型就自动实现了这个接口,这一特性可以方便的用于多态
- 一个类型只要实现了接口定义的所有方法(是指有相同名称、参数列表、以及返回值 ),那么这个类型就实现了这个接口,可以直接进行赋值(其实也是隐式转换),比如
var t Printer = &User{1, "Tom"}
- 多继承的概念
- 一个类型就可以实现多个接口,只要它拥有了这些接口类型的所有方法,那么这个类型就是实现了多个接口
- 多态
- 一个接口可以被不同类型实现
1 | go复制代码type Stringer interface { |
2.3 interface{}空接口的实现
空接⼝ interface{} 没有任何⽅法签名,也就意味着任何类型都实现了空⼝。其作⽤类似⾯向对象语⾔中的根对象object。
2.4 类型断言
- 一个类型断言检查接口类型实例是否为某一类型 。语法为x.(T) ,x为类型实例,T为目标接口的类型。比如
value, ok := x.(T)
+ x :代表要判断的变量
+ T :代表被判断的类型
+ value:代表返回的值
+ ok:代表是否为该类型。
+ **注意:x 必须为inteface类型,不然会报错。**
- 不过我们一般用switch进行判断,叫做 type switch。注意:不支持fallthrough.
1 | go复制代码func main() { |
2.5 接口转换
- 可以将拥有超集的接口转换为子集的接口,反之出错。
- father->son,儿子一定是父类,反之不行(包括多继承)
- 通过类型判断,如果不同类型转换会发生panic.
1 | go复制代码type User struct { |
2.6 匿名接口
- 匿名接口可用作变量类型,或者是结构成员。
1 | go复制代码type Tester struct { |
3、接口的内部实现
3.1 接口值
- 接口值可以使用 == 和 !=来进行比较
- 两个接口值相等仅当它们都是nil值或者它们的动态类型相同,并且动态值也根据这个动态类型的==操作相等
- 因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。
- 然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片) ,将它们进行比较就会失败并且panic,除非使用reflect.DeepEqal,深度比较
3.2 接口内部结构
1 | go复制代码// 没有方法的interface |
- _type记录着Go语言中某个数据类型的基本特征,_type是go所有类型的公共描述
- 可以简单的认为,接口可以通过一个 _type *_type 直接或间接表述go所有的类型就可以了
- 存在两种interface,一种是带有方法的interface,一种是不带方法的interface
+ 对于不带方法的接口类型,Go语言中的所有变量都可以赋值给interface{}变量,interface可以表述go所有的类型,\_type存储类型信息,data存储类型的值的指针,指向实际值或者实际值的拷贝。
+ 对于带方法的接口类型,`tab *itab` 存储指向了iTable的指针,ITable存储了类型相关(\_type)的信息以及相关方法集,而data 同样存储了实例值的指针,指向实际值或者是实际值的一个拷贝。
- go语言interface的源码表示,接口其实是一个两个字段长度的数据结构。所以任何一个interface变量都是占用16个byte的内存空间。从大的方面来说,如图:
- 注意
var n notifier n=user("Bill")
将一个实现了notifier接口实例user赋给变量n。接口n 内部两个字段 tab *itab 和 data unsafe.Pointer, 第一个字段存储的是指向ITable(接口表)的指针,这个内部表包括已经存储值的类型和与这个值相关联的一组方法。第二个字段存储的是,指向所存储值的指针。注意:这里是将一个值赋值给接口,并非指针,那么就会先将值拷贝一份,开辟内存空间存储,然后将此内存地址赋给接口的data字段。也就是说,值传递时,接口存储的值的指针其实是指向一个副本。- 如果是将指针赋值给接口类型,那么第二个字段data存储的就是指针的拷贝,指向的是原来的内存,如下
- 每种数据类型都存在一个与之对应的_type结构体(Go语言原生的各种数据类型,用户自定义的结构体,用户自定义的interface等等)。
- 小结:总的来说接口是一个类型,它是一个struct,是一个或多个方法的集合。任何类型都可以实现接口,并且是隐式实现,可以同时实现多个接口。接口内部只有方法声明没有实现。接口内部存储的其实就是接口值的类型和值,一部分存储类型等各种信息,另一部分存储指向值的指针。如果是将值传给接口,那么这里第二个字段存储的就是原值的副本的指针。接口可以调用实现了接口的方法。
4、方法集
4.1 方法集定义
- 方法集:方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接受者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。
1 | go复制代码// 这个示例程序展示 Go 语言里如何使用接口 |
4.2 方法集规则
- 方法集规则详解
- 举个例子
1 | kotlin复制代码fun (t T)MyMethod(s string) { |
+ 可以理解成是 `func(T, string)` 类型的方法。方法接收器**像其他参数一样**通过值传递给函数。
+ 因为所有的参数都是通过值传递的,任何一个 `Cat` 类型的值可能会有很多 `*Cat` 类型的指针指向它,如果我们尝试通过 `Cat` 类型的值来调用 `*Cat` 的方法,根本就不知道对应的是哪个指针
+ 相反,如果 `Dog` 类型上有一个方法,通过 `*Dog` 来调用这个方法可以确切的找到该指针对应的 `Gog` 类型的值,从而调用上面的方法。运行时,Go 会自动帮我们做这些,所以我们不需要像 C语言中那样使用类似如下的语句 `d->Speak()`
- 简单讲就是,接受者是(t T),那么T 和 *T 都可以实现接口,如果接受者是(t *T)那么只有 *T才算实现接口
- 原因:编译器并不是总能自动获得一个值的地址,即一个指针类型可以通过其相关的值类型来访问值类型的方法,但是反过来不
5、嵌入类型时接口实现
- 嵌入类型:是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。
- 实现方法重写:外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法。
- 注意声明字段和嵌入类型在语法上的不同 ,嵌入类型直接是写个类型名就行
- 内部类型的标识符提升到了外部类型,可以直接通过外部类型的值来访问内部类型的标识符。 也可以通过内部类型的名间接访问内部类型方法和标识符。
- 内部类型实现接口外部类型默认也实现了该接口。注意方法集的规则。
- 如果内部类型和外部类型同时实现一个接口,就近原则,外部类型不会直接调用内部类型实现的同名方法,而是自己的。当然可以通过内部类型间接显示的去调用内部类型的方法。
5.1 嵌入类型实现接口,同样应用到外部类型
1 | go复制代码// 这个示例程序展示如何将一个类型嵌入另一个类型,以及 |
5.2 内部类型和外部类型同时实现接口
- 优先调用外部
1 | go复制代码// 这个示例程序展示如何将一个类型嵌入另一个类型,以及 |
本文转载自: 掘金