清水泥沙

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 也类似):

  1. 一个接口需要声明哪些接口方法?
  2. 如果多个类实现了相同的接口方法,应该如何设计接口?比如上面这个 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();
}

上述代码中,定义了两个接口AB,其中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
}