一、Go 语言中的接口
很多编程语言中都有接口的概念,静态语言 Java 中的接口和 Go 中的接口地位或者概念是不一样的,Go 语言中的接口与 Python 中的接口比较像。
Go 中的接口是一种协议,既调用方和实现方均需要遵守的一种协议,按照统一的方法命名参数类型和数量来协调逻辑处理的过程。
接口的声明
接口是一种协议,一种规范;定义接口时只需定义规范无须关心实现的细节。
type 接口名 interface { 方法1 (参数列表) 返回值列表 方法2 (参数列表) 返回值列表 ... } 复制代码
在 Go 语言中 interface 名字仍然以单个词为优先。命名基本采用驼峰命名法,首字母根据访问控制大写或者小写。对于拥有唯一方法或通过多个拥有唯一方法的接口组合而成的接口,Go 语言的惯例是一般用"方法名+er"的方式为 interface 命名,例如 Reader、Writer 等。
Go 是区分大小写的,当方法名首字母和接口名首字母都是大些时,这个方法可以被接口所在的包之外的代码访问。
接口方法中参数列表和返回值列表中的变量名可以忽略。
func main() { thor := Hero{"Thor, God of Thunder"} thor.Hammer() thor.Aex() } type Fighter interface { Hammer() string Aex() string } type Hero struct { Name string } func (hero Hero) Hammer() string { fmt.Printf("%v 正在使用喵喵锤\n", hero.Name) return "Hammer" } func (hero Hero) Aex() string { fmt.Printf("%v 正在使用暴风战斧\n", hero.Name) return "Aex" } 复制代码
执行上述代码,输出结果如下:
Thor, God of Thunder 正在使用喵喵锤 Thor, God of Thunder 正在使用暴风战斧 复制代码
结构体实现一个接口就需要实现接口中的所有方法,实现方法需要保持方法签名一致,包括方法名称、参数列表、返回值列表,只要有一个不一致都不能算是实现这个接口,并且在调用时会导致报错。
比如方法名不一致会报错:
# command-line-arguments ./ex9.go:9:7: thor.Aex undefined (type Hero has no field or method Aex) 复制代码
Go 语言中的接口也是一种类型,可以直接声明一个接口类型的变量
var fighter Fighter 复制代码
一个小陷阱
在实现接口方法时,方法的接收者如果是结构体指针,那么在给接口变量赋值时就只能赋值结构体指针类型,如果还是赋值结构体实例化对象怎会报错。
type Course struct { Name string Price float64 } type Outputer interface { Output() string } // 使用结构体指针作为函数接收者 func (c *Course) Output() string { fmt.Println(c.Name) return "" } 复制代码
结构体实现接口方法时函数接收者使用指针形式,在将实例化结构体赋值给接口变量时会报错,如下图所示:
二、接口是一种抽象类型
Go 中多态的实现
保持 Hero 结构体不变,在增加一个 Evil 结构体也实现 Fighter 接口的两个方法
type Evil struct { Name string } func (evil Evil) Hammer() string { fmt.Printf("%v 正在使用喵喵锤\n", evil.Name) return "Evil - Hammer" } func (evil Evil) Aex() string { fmt.Printf("%v 正在使用暴风战斧\n", evil.Name) return "Evil - Aex" } 复制代码
在 main 方法中声明一个 Fighter 接口变量,并赋值一个 Hero 结构体实例
func main() { // 多态 var f Fighter = Hero{"Thor, God of Thunder"} f.Aex() f.Hammer() } 复制代码
执行上述代码,输出结果如下:
Thor, God of Thunder 正在使用暴风战斧 Thor, God of Thunder 正在使用喵喵锤 复制代码
上述代码中将结构体实例赋值给一个接口类型变量,实现基于接口的调用,而不是实例化对象本身的调用,如果接口类型变量赋的值不是 Hero 结构体的实例化对象,而是 Evil 结构体的实例化对象,只需更改赋值即可实现 Evil 结构体对 Fighter 接口方法的调用。
func main() { // 多态 var f Fighter = Evil{"Thanos"} f.Aex() f.Hammer() fmt.Printf("%T", f) } 复制代码
执行上述代码,输出结果如下:
Thanos 正在使用暴风战斧 Thanos 正在使用喵喵锤 main.Evil 复制代码
接口实际的类型就是赋值的结构体类型。
接口作为函数参数
接口也可以作为函数的参数,比如我们定义两个方法 HeroSwingStormbreaker 和 EvilSwingStormbreaker,不管是 Hero 还 Evil 都可以拿起暴风战斧,两个函数实现的功能完全是一致的,这种方式就会导致代码的冗余。
func HeroSwingStormbreaker(hero Hero) { fmt.Printf("%v is swing Stormbreaker", hero) } func EvilSwingStormbreaker(evil Evil) { fmt.Printf("%v is swing Stormbreaker", evil) } 复制代码
如果只定义一个方法,并且将 Fighter 接口作为参数,在 Hero 和 Evil 都实现 Fighter 接口的前提下,Hero 和 Evil 都实现拿起暴风战斧的功能。
func main() { SwingStormbreaker(f) } func SwingStormbreaker(f Fighter) { fmt.Printf("%v is swing Stormbreaker", f) } 复制代码
执行上述代码,输出结果如下:
{Thanos} is swing Stormbreaker