文章目录
Duck Typing
什么是鸭子类型
图中的大黄鸭是一只鸭子吗?如果从传统角度来看,图中的大黄鸭并非是一只鸭子,因为它即不会叫也不会跑,甚至连生命都没有
首先看下鸭子类型的定义
If it walks like a duck and it quacks like a duck, then it must be a duck
如果某个东西像鸭子一样走,像鸭子一样嘎嘎叫,那它一定是鸭子
所以,从Duck Typing角度来看,图中的大黄鸭是一只鸭子
鸭子类型,是程序设计中的一种类型推断风格,它描述事物的外部行为而非内部结构
Go语言的鸭子类型
Go语言通过接口的方式实现Duck Typing。不像其他动态语言那样,只能在运行时才能检查到类型不匹配,也不像大多数静态语言那样,需要显示声明实现哪个接口,Go语言接口的独特之处在于它是隐式实现。
概述
接口类型
接口是一种抽象类型,它没有暴露所含数据的布局或者内部结构,当然也没有哪些数据的基本操作,所提供的仅仅是一些方法。当你拿到一个接口类型的变量,你无从知道它是什么,但你能知道它能做什么,或者更精确地讲,仅仅是它提供了哪些方法。
接口定义
Go语言提供了 interface关键字,接口中只能定义需要实现的方法,不能包含任何的变量
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
例如 io.Writer 其实就是接口类型
type Writer interface {
Write(p []byte) (n int, err error)
}
接口与接口间可以嵌套得到新接口,如 io.ReadWriter
type Reader interface {
Read(p []byte) (n int, err error)
}
type ReadWriter interface{
Reader
Writer
}
不包含方法的接口,叫做空接口类型
interface{
}
实现接口
如果一个具体类型实现了一个接口要求的所有方法,那么这个类型实现了这个接口。当具体类型实现了一个接口时,这个具体类型才可以赋值给该接口
如下示例中,定义一个 Runner 接口,只包含一个 run() 方法, Person 结构体实现了 Run() 方法,那么就实现了 Runner 接口
type Runner interface {
Run()
}
type Person struct {
Name string
}
func (p Person) Run() {
fmt.Printf("%s is running\n", p.Name)
}
func main() {
var r Runner
r = Person{
Name: "song_chh"}
r.Run()
}
另外,因为空接口类型是没有定义任何方法的接口,因此所有类型都实现了空接口,也就是说可以把任何类型赋给空接口类型
接口和指针
接口在定义一组方法时,没有对实现的接收者做限制,所以有两种实现方式的接收者,一种是指针接收者,另一种是值接收者
同一个方法不能两种实现同时存在
为Runner接口增加一个 Say() 方法,Person结构体类型使用指针接收者实现 Say() 方法
type Runner interface {
Run()
Say()
}
type Person struct {
Name string
}
func (p Person) Run() {
fmt.Printf("%s is running\n", p.Name)
}
func (p *Person) Say() {
fmt.Printf("hello, %s", p.Name)
}
在对接口变量进行初始化时,可以使用结构体或者结构体指针
var r Runner
r = &Person{
Name: "song_chh"}
r = Person{
Name: "song_chh"}
因为实现接口的接受者类型和接口初始化时的类型都有两个维度,就会产生四种不同情况的编码
- × 表示编译不通过
下面两种情况能够通过编译很好理解:
- 方法接受者和初始化类型都是结构体值
- 方法接受者和初始化类型都是结构体指针
首先,我们来看一下能够通过编译的情况,也就是方法接收者是结构体,而初始化的变量是指针类型
type Runner interface {
Run()
Say()
}
type Person struct {
Name string
}
func (p Person) Run() {
fmt.Printf("%s is running\n", p.Name)
}
func (p *Person) Say() {
fmt.Printf("hello, %s", p.Name)
}
func main() {
var r Runner
r = &Person{
Name: "song_chh"}
r.Run()
r.Say()
}
上述代码中,Person结构体指针是能够直接调用Run和Say,因为作为结构体指针,能够隐式获取到底层的结构体,然后在通过结构体调用对应的方法
如果将引用去掉,即变量初始化使用结构体类型
r = Person{
Name: "song_chh"}
则会提示编译不通过
./pointer.go:24:4: cannot use Person literal (type Person) as type Runner in assignment:
Person does not implement Runner (Say method has pointer receiver)
那么为什么会编译不通过呢?首先在Go语言在进行参数传递都是 值传递
当代码中的变量是 &Person{} 时,在方法调用的过程中会对参数进行复制,创建一个新的 Person 结构体指针,指针指向一个确定的结构体,所以编译器会隐式的对变量解引用获取指针指向的结构体,完成方法的调用
当代码中的变量是 Person{}时,在方法调用的过程中会对参数进行复制,也就是 Run() 和 Say() 会接受一个新的 Person{} 变量。如果方法接收者是 *Person ,编译器无法根据结构体找到一个唯一的指针,所以编译器会报错
注意:一个具体类型T的变量,直接调用*T的方法也是合法的,因为编译器会隐式的帮你完成取地址操作,但这仅仅是一个语法糖
nil和non-nil
再看一段示例,还是Runner接口和Person结构体,注意看main()函数体,首先声明一个接口变量r,打印是否为nil,紧接着定义一个*Person类型的p,打印p是否为nil,最后将p赋值给r,打印此时的r是否为nil
type Runner interface {
Run()
}
type Person struct {
Name string
}
func (p Person) Run() {
fmt.Printf("%s is running\n", p.Name)
}
func main() {
var r Runner
fmt.Println("r:", r == nil)
var p *Person
fmt.Println("p:", p == nil)
r = p
fmt.Println("r:", r == nil)
}
输出结果是什么?
r: true or false
p: