接口为Go语言提供了多态特性,多态是指代码可以根据类型的具体实现采用不同行为的能力。如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值。
鸭子类型 Duck Typing
f it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
也就是说,如果一个类型实现了某个接口所定义的所有方法时,就说这个类型实现了该接口,而无需显式地声明某个类型实现了某个接口。Golang将类型检查的工作交给了编译器,在调用某个方法时,编译器会将对应的类型隐式地转换为对应接口的类型。
实现
接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。
//interface definition
type VowelsFinder interface {
FindVowels() []rune
}
type MyString string
//MyString implements VowelsFinder
func (ms MyString) FindVowels() []rune {
var vowels []rune
for _, rune := range ms {
if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
vowels = append(vowels, rune)
}
}
return vowels
}
调用FindVowels
函数
func main() {
name := MyString("Sam Anderson")
var v VowelsFinder
v = name // possible since MyString implements VowelsFinder
fmt.Printf("Vowels are %c", v.FindVowels())
}
值接收者和指针接收者
各类源码分析写的太复杂了,这里先记录一个方便记忆的思路:
在Golang中,指针类型的值可以解引用为其指向的目标值,
而方法的调用,实际是将接收者进行值拷贝后执行对应的函数,因此,如果一个方法的接收者是值类型的,那么使用对应类型值的指针去调用该方法是可行的,编译器会先将该指针拷贝过来,然后将指针解引用为对应的值,然后执行函数,反之则无法根据拷贝的值找到对应的指针,那么也就无法执行该方法了。
举个例子:
定义一个接口coder
包含两个方法code
和debug
:
type coder interface {
code()
debug()
}
定义一个结构体Gopher
去实现这个接口:
type Gopher struct {
language string
}
使用值接收者来实现接口coder
:
func (g Gopher) code(){
...
}
func (g Gopher) debug() {
...
}
那么,下面两种方式都能正常调用
func main() {
var gp = &Gopher{}
var g = Gopher{}
// g和gp均能正常调用code()和debug()方法
}
反之,如果使用Goper类型的指针作为接收者,那么就不能使用Goper的值类型调用code()和debug()
func (g *Gopher) code(){
...
}
func (g *Gopher) debug() {
...
}
func main() {
var gp = &Gopher{}
var g = Gopher{}
// 执行g.code()会报错,提示g并未实现coder接口
// 当gp调用对应的方法时,会同时修改原始gp的值
}
空接口
空接口没有定义任何函数,那么Golang中的任何类型都实现了一个空接口
对空接口的类型转换被称为断言。
看一个例子:
type Person struct {
name string
age int
}
var i = interface{}
s = i.(Person)
断言的语法为: <目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言 <目标类型的值> := <表达式>.( 目标类型 ) //非安全类型断言
eface和iface (暂时未整理)
方法能给用户自定义的类型添加一些行为,给一个函数添加一个接收者,那么这个函数就变成了这个类型的方法,
interface分为空接口和包含方法的接口两大类型,在go语言底层,这两大类型分别用eface和iface指定:
struct Eface
{
Type* type;
void* data;
};
struct Iface
{
Itab* tab;
void* data;
};
因此,go语言中的interface组成结构可以抽象为两类:
- 包含已存储值的类型信息及方法集的内部表iTable
- 所存储的值数据
综上,go语言的interface类型可以抽象如下图
eface和iface的区别仅仅在方法集的有无
eface
举例说明无方法集的接口赋值过程
import (
"fmt"
"strconv"
)
type Binary uint64
func main() {
b := Binary(200)
any := (interface{})(b)
fmt.Println(any)
}
iface
如果一个类型实现了interface中所有的方法,可以认为该类型实现了该interface
// 定义一个通知类行为的接口notifier
type notifier interface {
notify()
}
// 定义一个用户类结构体user
type user struct {
name string
email string
}
// 使用user指针接受者实现notify方法,从而实现notifier接口
// 注意,这里实现notifier接口的是user指针而非user本身
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n,
u.name,
u.email)
}
// 接受一个实现了notifier接口的值并发送通知
func sendNotification(n notifier) {
n.notify()
}
u := user{"Bill", "bill@email.com"}
// 错误的调用方法
sendNotification(u)
// 正确的调用方法
sendNotification(&u)
通过一个例子来展示iface结构的赋值过程
package main
import (
"fmt"
"strconv"
)
type Binary uint64
func (i Binary) String() string {
return strconv.FormatUint(i.Get(), 10)
}
func (i Binary) Get() uint64 {
return uint64(i)
}
func main() {
b := Binary(200)
any := fmt.Stringer(b)
fmt.Println(any)
}
/*
// fmt.Stringer是一个包含String方法的接口
type Stringer interface {
String() string
}
*/
方法集
方法集定义了接口接受的规则,参考上一节代码,由于实现notify方法的是user的指针类型而非user类型,因此不能将u(user类型)作为参数传递至sendNotification,参考方法集的描述
Values | Methods Receivers |
---|---|
T | (t T) |
*T | (t T) and (t *T) |
由此可见,指向T类型的指针方法集可接收的类型包含T和*T两种类型,这里提现了go语言底层对指针类型数据的处理方式,而非方法集本身特有属性。
参考
go in action
https://i6448038.github.io/2018/10/01/Golang-interface/