参考 Understanding Go Interface
感谢 francesc 分享
接口
我们编程中少不了对接口使用和设计,无论你是使用哪种语言或多或少都会使用到**接口**。即使你说明重来没有显示定义过或者使用过接口,我想如果你也可能隐式地用到过接口。
今天我们就说一说 go 语言中的接口是如何设计的;如何使用的以及他与其他语言相比有什么自己的特点。
什么是 interface
接口可以理解是规范、协议、用户使用手册和对类型抽象,对行为描述。说了这么一大堆还需要您自己了解。
> In object-oriented programming, a protocal or interface is a common means for unrelated objects to communicate with each other
wikipedia
上面的话摘字 wiki,这里传递了两个重要的信息
- communicate 接口是用于通讯,接口就是用来定义通讯遵循的规则,类似一种协议。
- unrelated objects 耦合度低的对象,接口定义通讯规则可以使用两个互不相干的对象。通过接口会将关注点放在交流上,而不会关注与通讯无关的类型上。
乐高玩具就是一个好的例子。乐高玩具的一个piece 组合时只要遵守尺寸规则,无论大小和颜色就可以组合在一起进行拼接。
以后兼职工作也是一样只要满足规定的条件,在拼接 Lego 玩具时是否可以拼接是和piece 的颜色和形状没有关系的,只要他们都遵守一定尺寸就可以进行拼接。在软件控制模块搭建和通讯也是通过定义一定接口规范来实现了。我想软件工程也在某些方面借鉴传统的行业。
想到 go 我们就会自然联想到他一个项目 docker,也就是 docker 项目让我们看 go 语言的商业化,docker 可以算上是 go 的最佳实践之一了。
之所以集装箱海运如何兴旺,因为提供一个统一接口,无论是什么样船或者是列车都可以可以运输集装箱,因为集装箱提供一种标准(接口)标准的尺寸和重量。
docker 设计也是源于集装箱设计,我们将 code 进行包装,然后提供一致接口这样我们的 code 就可以运行在任何符合其标准的环境了。
### 什么是 go 的 interface
在 go 语言中我们可以通过两个维度对类型进行划分
- 抽象类型
- 实体类型
当然在 go 语言中有很多种类型,不过我们大致可以将归为两种一种类型属于 abstract 类型(抽象类型)和 concrete 类型(实体类型)
实体类型
- 用于描述类型在内存中分配情况,根据类型我们就可以知道其在内存中存在形式。
int8/int16/int32/int64/struct/float
- 使用方法赋予数据一定的行为
type Number intfunc (n Number) Positive() bool{ return n >0}
抽象类型
这种类型就是接口,并不是说明类型的形状,而是告诉你能够使用这种类型来做什么。
抽象类型并没定义描述如何为这种类型分配内存空间,而是描述类型的行为。按行为为类型进行划分。这些抽象类型有 io.Reader 、 io.Writer 和 fmt.String 等等
type Positiver interface{ Positive() bool}
在 Positiver 接口中定义了 Positive 这里大家已经注意到了我们并没有指定接口的接收者(也就是实现者)。这就是 go 语言设计初衷,也就是我不关心你是什么类型只要你有一个形状(包括函数名、参数和返回值)一致的方法,我就认为你是实现了这个接口,你属于这个类型。
用来说明 go 语言接口的经典接口 Writer 和 Reader 接口
type Reader interface{ Read(b []byte)(int,error)}type Writer interface{ Write(b []byte)(int,error)}
只要实现了接口的方法的类型就属于接口类型,所以集合是实体类型的集合。
接口的组合
type ReadWriter interface{ Read Writer}
接口是可以组合,这个 ReadWriter 接口,当然实现这个接口类型应该是更强大而范围从图上来看却缩小了。但是接口越详细确定范围也就小。
interface{}
这里有一个 interface{} , Rob Pike 指着 interface{} 是没有任何意思,因为没有任何限制,没有限制也就是没有意义,这个应该不难理解。在 go 语言中可以用 interface 表示类型,但是从 interface{} 来看我们是无法了解其代表类型的具体信息。
使用接口原因
- 编写通用的算法
- 隐藏实现的细节
- 提供拦截点
以上三点是我们使用接口原因,这里逐一给大家进行讲解。
a) func writeTo(f *os.File) error
b) func writeTo(w *os.ReadWriteCloser) error
c) func writeTo(w io.Writer) error
d) func writeTo(w interface{}) error
上面选择题您选择哪个?
答案是 b 或 c,a 选项问题是这里接收一个实体类型作为参数,这样对于测试 writeTo 就造成问题,我们每次测试这个方法就需要实例化一个 File 对象,这样得不偿失。
d 答案是对参数没有任何限制,从而也就是失去意义。没有任何关于类型有价值的信息,编译器也不会进行类型检查的。
b 和 c 根据你应用场景而定,接口的方法越少,复用性就越高。
> The bigger the interface, the weaker the abstraction
rob pike
> Be conservative in what you do, be liberal in what you accept from others
这是一条 robustness 理论,这句话源于 TCP 网络协议,在 TCP 网络协议是遵循发送到网络上数据是没有问题的,而从网络上接收数据即使有问题也是可以接受的。
### 抽象数据类型
数据类型的数学模型
通过下面依据来定义数据的行为
- 需要考虑数据可能的值
- 对此类型数据的可能操作
- 这些操作的行为
我们看一看栈这个抽象数据模型,有什么行为
top(push(x,s)) = x
我们 push 数据 x 入栈 s 然后用 top 方法进行出栈就会得到 x,逻辑性很强吧
pop(push(x,s)) = s
我们 push 数据 x 入栈 s 然后用 pop 弹出会返回栈进行push 前的 s。
empty(new())not empty(push(s,x))
通过上面分析我们就得到数据模型 stack
top(push(x,s)) = xpop(push(x,s)) = sempty(new())!empty(push(s,x))
这个比较抽象,我们通过定义一些行为来表述一种数据结构,理解起来可能会话费一段时间,可以通过行为表述抽象数据结构,例如先进先出就是数据结构。我们用行为表述了 stack 这一抽象数据。
接下来在 go 语言中,用具体代码对上面描述进行接口定义来表述 stack 抽象数据结构。
type Stack interface{ Push(v interface{}) Stack Pop() Stack Empty() bool}
在抽象数据结构上我们可以定义一些算法,这里 Size 算法需要传入抽象数据结构具有 Empty() 和 Pop() 行为,而且 Size 算法并不关系实现了这两个方法的类型的详细信息。这要实现了这两个方法即可。
func Size(s Stack) int { if s.Empty(){ return 0 } return Size(s.Pop()) + 1}
通过定义可排序的数据结构
type Interface interface{ Less(i,j int) bool Swap(i, j int) Len() int}
到现在大家可能有一些了解了在 go 语言中使用接口套路,我们先根据抽象数据结构可能有的操作,来推断出这些操作的行为。在这些抽象数据结构上定义一些通用算法,这些算法接收接口作为参数,只要这些类型提供其算法所需要的行为就行。看一看我们在 Writer 和 Reader 上的一些算法。
func Fprintln(w Writer, ar ...interface{}) (int,error)func Fscan(r Reader, a ...interface{}) (int, error)func Copy(w Writer, r Reader)(int, error)
我们可以在接口上写通用方法
现在我们明白之前说的那句话了吧,我们只是提供对的,对于接收的我们只是接收就好了。
这个选择题可以帮助你判断是否对上面所讲已经理解了。
a) func New() *os.File
b) func New() io.ReadWriteCloser
c) func New() io.Writer
d) func New() interface{}
首先我们来排除 d ,因为 interface{} 是没有任何意义和帮助的。b 和 c 也没有什么区别只是限制用户使用范围,如果只想让用户有写的能力就可以用 io.Writer 。最佳答案是 a,因为 robustness 原则是我们给出应该是好的确定的所以应该是 *os.File 实体类型而非仅是限制用户的接口。
那么我们终结一下 go 语言程序 robuness 的原则吧
返回给用户(这里用户是函数调用者)应该是确定的实体类型,而接收参数应该是接口。