go 进阶—接口(上)
参考 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 组合时只要遵守尺寸规则,无论大小和颜色就可以组合在一起进行拼接。
1200px-MSC_Oscar_(ship,_2014)_002.jpg
以后兼职工作也是一样只要满足规定的条件,在拼接 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 int
func (n Number) Positive() bool{
return n >0
}
屏幕快照 2019-04-21 下午12.42.00.png
抽象类型
这种类型就是接口,并不是说明类型的形状,而是告诉你能够使用这种类型来做什么。
抽象类型并没定义描述如何为这种类型分配内存空间,而是描述类型的行为。按行为为类型进行划分。这些抽象类型有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)
}
只要实现了接口的方法的类型就属于接口类型,所以集合是实体类型的集合。
屏幕快照 2019-04-21 下午2.04.04.png
接口的组合
type ReadWriter interface{
Read
Writer
}
接口是可以组合,这个 ReadWriter 接口,当然实现这个接口类型应该是更强大而范围从图上来看却缩小了。但是接口越详细确定范围也就小。
interface{}
1200px-Rob-pike-oscon.jpg这里有一个 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)) = x
pop(push(x,s)) = s
empty(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 的原则吧
返回给用户(这里用户是函数调用者)应该是确定的实体类型,而接收参数应该是接口*。
隐藏具体的实现
使用接口来隐藏具体的实现
- 将实现与API分离
- 在实现/或提供多个实现之间轻松切换