接口类型是对其他类型行为的概括与抽象,通过使用借口,我们可以写出更加灵活和通用的函数,这些函数不用绑定在一个特性的类型实现上,很多面向对象的语言都有接口这个概念,Go语言的接口的独特之处在于它是隐式实现。换句话说,对于一个具体的类型,无需声明它实现了哪些接口,只要提供接口所必须的方法即可。这种设计让你无须改变现有类型的实现,就可以为这些类型创建新的接口,对于那些不能修改包的类型,这一点特别有用。
Go语言中还有另外一种类型称为接口类型,它是一种抽象类型,它并没有暴露所含数据的布局或者内部结构,当然也没有那些数据的基本操作,它所提供的仅仅是一些方法而已。如果你拿到一个接口类型的值,你无从知道它是什么,你能知道的仅仅是它能做什么,或者更精确地讲,仅仅是它提供了哪些方法。如果一个类型实现了一个接口要求的所有方法,那么这个类型实现了这个接口。比如*os.File类型实现了io.Reader、Writer、Closer和ReaderWriter接口。*bytes.Buffer实现了Reader、Writer和ReaderWriter,但没有实现Closer,因为它没有Close方法。
从概念上来讲,一个接口类型的值(简称接口值)其实有两个部分:一个具体类型和该类型的一个值,二者称为接口的动态类型和动态值。对于像Go这样的静态类型语言,类型仅仅是一个编译的时候的概念,所以类型不是一个值。在我们的概念模型中,用类型描述符来提供每个类型的具体信息,比如它的名字和方法。对于一个接口值,类型部分就用对应的类型描述符来表述。
当设计一个新包的时候,我们容易首先创建一系列接口,然后再定义满足这些接口的具体类型,这种方式会产生很多接口,但这些接口只有一个单独的实现。这种接口是不必要的抽象,还有运行时的成本。我们可以尝试用导出机制来限制一个类型的哪些方法或结构体的哪些字段是对包外可见的,哪些是不可见的。仅在有两个或者两个以上具体类型需要按统一的方法处理时才需要接口。
这个规则也有特例,如果接口和类型实现处于依赖的原因不能放在同一个包里面,那么一个接口只有一个具体类型实现也是可以的,在这种情况下,接口是一个解耦两个包的好方式。因为接口仅在两个或者两个类型满足的情况下存在,所以它就必然会抽象掉那些特有的实现细节,这样设计的结果就是出现了更具有简单和更少方法的接口,比如io.Writer和fmt.Stringer都只有一个方法。设计新类型时越小的接口越容易满足,一个不错的接口设计经验是仅要求你需要的。
有疑问加站长微信联系(非本文作者)