golang基础(31.接口的定义和实现)
接口在go中有这至关重要的地位,如果说goroutine和channel是撑起整个GO语言并发的基石,那么接口就是类型系统的基石。
传统的侵入式接口
什么是侵入式接口,接口作为不同类之间的抽象定义,它是一种契约方式的存在,只要契约存在,就必须要履行契约。通俗地讲只要继承了某一个接口,就必须去实现这个接口中的所有方法。
// 声明一个'iTemplate'接口
interface iTemplate
{
public function setVariable($name, $var);
public function getHtml($template);
}
// 实现接口
// 下面的写法是正确的
class Template implements iTemplate
{
private $vars = array();
public function setVariable($name, $var)
{
$this->vars[$name] = $var;
}
public function getHtml($template)
{
foreach($this->vars as $name => $value) {
$template = str_replace('{' . $name . '}', $value, $template);
}
return $template;
}
}
这个时候如果有一个接口iTemplate2声明了与iTemplate完全一样的接口方法,甚至名字一致,只不过在不同的命名空间下,这时候编译器会认为上面的类只实现了其中某一个接口,而没有实现另一个接口。这些在我们的认知中是理所当然的,如果我们没有在代码中明确地指定接口的层级和继承自哪个接口,那么我们就没有实现这个接口。这个类和这个接口就完全没有任何关系,这些接口中的声明和实现都是显式的。我们把这种接口称之为侵入式接口,尤其是在设计标准库的时候,因为标准库必然涉及到接口设计,接口的需求方是业务实现类,只有具体编写业务实现类的时候才知道需要定义哪些方法,而在此之前,标准库的接口就已经设计好了,我们要么按照约定好的接口进行实现,如果没有合适的接口需要自己去设计,这里的问题就是接口的设计和业务的实现是分离的,接口的设计者并不能总是预判到业务方要实现哪些功能,这就造成了设计与实现的脱节。接口的过度设计会导致某些声明的方法完全不需要去实现,如果设计得太简单又无法满足业务需求。以 PHP 自带的 SessionHandlerInterface 接口为例,该接口声明的接口方法如下:
SessionHandlerInterface {
/* 方法 */
abstract public close ( void ) : bool
abstract public destroy ( string $session_id ) : bool
abstract public gc ( int $maxlifetime ) : int
abstract public open ( string $save_path , string $session_name ) : bool
abstract public read ( string $session_id ) : string
abstract public write ( string $session_id , string $session_data ) : bool
}
用户自定义的 Session 管理器需要实现该接口,也就是要实现该接口声明的所有方法,但是实际在做业务开发的时候,某些方法其实并不需要实现,比如如果我们基于 Redis 或 Memcached 作为 Session 存储器的话,它们自身就包含了过期回收机制,所以 gc 方法根本不需要实现,又比如 close 方法对于大部分驱动来说,也是没有什么意义的。正是因为这种不合理的设计,所以在编写 PHP 类库中的每个接口时都需要纠结以下两个问题(Java 也类似):
- 一个接口需要声明哪些接口方法?
- 如果多个类实现了相同的接口方法,应该如何设计接口?比如上面这个 SessionHandlerInterface,有没有必要拆分成多个更细分的接口,以适应不同实现类的需要?
接下我们来看看 Go 语言的接口是如何避免这些问题的。在go语言中,我们并不需要显式指定类要实现哪个接口,一个类只要实现了某个接口要求得所有方法,那么它就实现了这个接口。例如,我们定义了一个 File 类,并实现了 Read()、Write()、Seek()、Close() 四个方法:
type File struct {
}
func (f *File) Read(buf []byte) (n int, err error) {
return
}
func (f *File) Write(buf []byte) (n int, err error) {
return
}
func (f *File) Seek(off int64, whence int) (pos int64, err error) {
return
}
func (f *File) Close() error {
return nil
}
假设我们有如下接口(Go 语言通过关键字 interface 来声明接口,以示和结构体类型的区别,花括号内包含的是待实现的方法集合):
type IFile interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error
}
尽管file类型并没有显示指定实现了IFile接口,甚至不知道这个接口的存在,但是在go中我们依然任务File类型实现IFile接口。与 Java、PHP 相对,我们把 Go 语言的这种接口称作非侵入式接口,因为类与接口的实现关系不是通过显式声明,而是系统根据两者的方法集合进行判断。这样做有两个好处:
- 其一,Go 语言的标准库不需要绘制类库的继承/实现树图,在 Go 语言中,类的继承树并无意义,你只需要知道这个类实现了哪些方法,每个方法是干什么的就足够了。
- 其二,定义接口的时候,只需要关心自己应该提供哪些方法即可,不用再纠结接口需要拆得多细才合理,也不需要为了实现某个接口而引入接口所在的包,接口由使用方按需定义,不用事先设计,也不用考虑之前是否有其他模块定义过类似接口。
这样一来,就完美地避免了传统面向对象编程中的接口设计问题。
通过组合实现接口继承
在其他语言中大部分都支持通过extends关键字来实现接口之间的继承:
<?php
interface A
{
public function foo();
}
interface B extends A
{
public function bar();
}
上述代码中,定义了两个接口A和B,其中B继承自A,如果某一个类需要实现B接口,那么同时它也需要实现A接口。go中也支持类型接口继承的特性,不过是通过是通过组合来完成的。以上面代码为例:
package main
type A interface {
Foo()
}
type B interface {
A
Bar()
}
type T struct {
}
func (t T) Foo() {
}
func (t T) Bar() {
}
func TestA(a A) {
}
func TestB(b B) {
}
func main() {
t := new(T)
//t中实现了Foo方法所以可以存入
TestA(t)
//t中实现了Foo和Bar方法所以可以存入
TestB(t)
}
上述代码中,我们通过组合的方式,如过某个类型中包含了Foo和Bar两个方法,那么就意味着它,实现了接口B,也实现了接口A。因为接口实现不是强制的,是根据类实现的方法来动态判定的。可以认为接口组合是匿名类型组合(没有显式为组合类型设置对应的属性名称)的一个特定场景,只不过接口只包含方法,而不包含任何属性。Go 语言底层很多包就是基于接口组合实现的,比如 io 里面的 Reader、Writer、ReadWriter 这些接口:
// Reader is the interface that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
Reader
Writer
}