Go语言与鸭子类型的关系
Duck Typing
,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。Go 语言作为一门静态语言,它通过通过接口的方式完美支持鸭子类型。例如,在动态语言 python 中,定义一个这样的函数:
def hello_world(coder):
coder.say_hello()
当调用此函数的时候,可以传入任意类型,只要它实现了
say_hello()
函数就可以。如果没有实现,运行过程中会出现错误。而在静态语言如 Java, C++ 中,必须要显示地声明实现了某个接口,之后,才能用在任何需要这个接口的地方。如果你在程序中调用
hello_world
函数,却传入了一个根本就没有实现 say_hello()
的类型,那在编译阶段就不会通过。这也是静态语言比动态语言更安全的原因。Go 语言作为一门现代静态语言,是有后发优势的。它引入了动态语言的便利,同时又会进行静态语言的类型检查,写起来是非常 Happy 的。Go 采用了折中的做法:不要求类型显示地声明实现了某个接口,只要实现了相关的方法即可,编译器就能检测到。
来看个例子:先定义一个接口,和使用此接口作为参数的函数:
type IGreeting interface {
sayHello()
}
func sayHello(i IGreeting) {
i.sayHello()
}
// 在定义两个结构体
type Go struct {}
func (g Go) sayHello() {
fmt.Println("Hi, I am GO!")
}
type PHP struct {}
func (p PHP) sayHello() {
fmt.Println("Hi, I am PHP!")
}
// main函数调用sayHello
func main() {
golang := Go{}
php := PHP{}
sayHello(golang)
sayHello(php)
}
在 main 函数中,调用调用 sayHello() 函数时,传入了
golang, php
对象,它们并没有显式地声明实现了 IGreeting 类型,只是实现了接口所规定的 sayHello() 函数。实际上,编译器在调用 sayHello() 函数时,会隐式地将 golang, php
对象转换成 IGreeting 类型,这也是静态语言的类型检查功能。总结一下,鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由它"当前方法和属性的集合"决定。Go 作为一种静态语言,通过接口实现了
鸭子类型
,实际上是 Go 的编译器在其中作了隐匿的转换工作。值接收者和指针接收者的区别
方法
方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是
值接收者
,也可以是指针接收者
。在调用方法的时候,值类型既可以调用
值接收者
的方法,也可以调用指针接收者
的方法;指针类型既可以调用指针接收者
的方法,也可以调用值接收者
的方法。也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。
来看个例子:
package main
import "fmt"
type Person struct {
age int
}
func (p Person) howOld() int {
return p.age
}
func (p *Person) growUp() {
p.age += 1
}
func main() {
// qcrao 是值类型
qcrao := Person{age: 18}
// 值类型 调用接收者也是值类型的方法
fmt.Println(qcrao.howOld())
// 值类型 调用接收者是指针类型的方法
qcrao.growUp()
fmt.Println(qcrao.howOld())
// ----------------------
// stefno 是指针类型
stefno := &Person{age: 100}
// 指针类型 调用接收者是值类型的方法
fmt.Println(stefno.howOld())
// 指针类型 调用接收者也是指针类型的方法
stefno.growUp()
fmt.Println(stefno.howOld())
}
调用了
growUp
函数后,不管调用者是值类型还是指针类型,它的 Age
值都改变了。实际上,当类型和方法的接收者类型不同时,其实是编译器在背后做了一些工作,用一个表格来呈现:
-
值接收者
指针接收者
值/指针接收者
不管接收者类型是值类型还是指针类型,都可以通过值类型或指针类型调用,这里面实际上通过语法糖起作用的。
先说结论:实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。
通俗额解释: 接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。
两者分别何时调用
如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。
使用指针作为方法的接收者的理由:
- 方法能够修改接收者指向的值。
- 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。
是使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的
本质
。iface和eface的区别
iface
和 eface
都是 Go 中描述接口的底层结构体,区别在于 iface
描述的接口包含方法,而 eface
则是不包含任何方法的空接口:interface{}
。type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32 // copy of _type.hash. Used for type switches.
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // variable sized
}
iface
内部维护两个指针,tab
指向一个 itab
实体, 它表示接口的类型以及赋给这个接口的实体类型。data
则指向接口具体的值,一般而言是一个指向堆内存的指针。再来仔细看一下
itab
结构体:_type
字段描述了实体的类型,包括内存对齐方式,大小等;inter
字段则描述了接口的类型。fun
字段放置和接口方法对应的具体数据类型的方法地址,实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时会更新此表,或者直接拿缓存的 itab。再看一下
interfacetype
类型,它描述的是接口的类型:type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
可以看到,它包装了
_type
类型,_type
实际上是描述 Go 语言中各种数据类型的结构体。我们注意到,这里还包含一个 mhdr
字段,表示接口所定义的函数列表, pkgpath
记录定义了接口的包名。这里通过一张图来看下
iface
结构体的全貌:接着来看一下
eface
的源码:type eface struct {
_type *_type
data unsafe.Pointer
}
相比
iface
,eface
就比较简单了。只维护了一个 _type
字段,表示空接口所承载的具体的实体类型。data
描述了具体的值。我们最后再来看下
_type
结构体:type _type struct {
// 类型大小
size uintptr
ptrdata uintptr
// 类型的 hash 值
hash uint32
// 类型的 flag,和反射相关
tflag tflag
// 内存对齐相关
align uint8
fieldalign uint8
// 类型的编号,有bool, slice, struct 等等等等
kind uint8
alg *typeAlg
// gc 相关
gcdata *byte
str nameOff
ptrToThis typeOff
}
Go 语言各种数据类型都是在
_type
字段的基础上,增加一些额外的字段来进行管理的:type arraytype struct {
typ _type
elem *_type
slice *_type
len uintptr
}
type chantype struct {
typ _type
elem *_type
dir uintptr
}
type slicetype struct {
typ _type
elem *_type
}
type structtype struct {
typ _type
pkgPath name
fields []structfield
}
这些数据类型的结构体定义,是反射实现的基础。
接口动态类型和动态值
从源码里可以看到:
iface
包含两个字段:tab
是接口表指针,指向类型信息;data
是数据指针,则指向具体的数据。它们分别被称为动态类型
和动态值
。而接口值包括动态类型
和动态值
。接口值的零值是指
动态类型
和动态值
都为 nil
。当仅且当这两部分的值都为 nil
的情况下,这个接口值就才会被认为 接口值 == nil
。package main
import "fmt"
type Coder interface {
code()
}
type Gopher struct {
name string
}
func (g Gopher) code() {
fmt.Printf("%s is coding\n", g.name)
}
func main() {
var c Coder
fmt.Println(c == nil)
fmt.Printf("c: %T, %v\n", c, c)
var g *Gopher
fmt.Println(g == nil)
c = g
fmt.Println(c == nil)
fmt.Printf("c: %T, %v\n", c, c)
}
一开始,
c
的 动态类型和动态值都为 nil
,g
也为 nil
,当把 g
赋值给 c
后,c
的动态类型变成了 *main.Gopher
,仅管 c
的动态值仍为 nil
,但是当 c
和 nil
作比较的时候,结果就是 false
了。接口的构造过程 - TODO
我们已经看过了
iface
和 eface
的源码,知道 iface
最重要的是 itab
和 _type
。为了研究清楚接口是如何构造的,接下来我会拿起汇编的武器,还原背后的真相。
类型转换和断言
我们知道,Go 语言中不允许隐式类型转换,也就是说
=
两边,不允许出现类型不相同的变量。类型转换
、类型断言
本质都是把一个类型转换成另外一个类型。不同之处在于,类型断言是对接口变量进行的操作。类型转换
对于
类型转换
而言,转换前后的两个类型要相互兼容才行。类型转换的语法为:<结果类型> := <目标类型> (<表达式>)package main
import "fmt"
func main() {
var i int = 9
var f float64
f = float64(i)
fmt.Printf("%T, %v\n", f, f)
f = 10.8
a := int(f)
fmt.Printf("%T, %v\n", a, a)
// s := []int(i)
上面的代码里,我定义了一个
in
型和 float64
型的变量,尝试在它们之前相互转换,结果是成功的:int
型和 float64
是相互兼容的。断言
前面说过,因为空接口
interface{}
没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是 interface{}
,那么在函数中,需要对形参进行断言,从而得到它的真实类型。断言的语法为:
```bash
// 安全类型断言
<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 )
//非安全类型断言
<目标类型的值> := <表达式>.( 目标类型 )
```
package main
import "fmt"
type Student struct {
Name string
Age int
}
func main() {
var i interface{} = new(Student)
s := i.(Student)
fmt.Println(s)
}
直接
panic
了,这是因为 i
是 *Student
类型,并非 Student
类型,断言失败。这里直接发生了 panic
,线上代码可能并不适合这样做,可以采用“安全断言”的语法:func main() {
var i interface{} = new(Student)
s, ok := i.(Student)
if ok {
fmt.Println(s)
}
}
这样,即使断言失败也不会
panic
。断言其实还有另一种形式,就是用在利用
switch
语句判断接口的类型。每一个 case
会被顺序地考虑。当命中一个 case
时,就会执行 case
中的语句,因此 case
语句的顺序是很重要的,因为很有可能会有多个 case
匹配的情况。代码示例如下:
func main() {
//var i interface{} = new(Student)
//var i interface{} = (*Student)(nil)
var i interface{}
fmt.Printf("%p %v\n", &i, i)
judge(i)
}
func judge(v interface{}) {
fmt.Printf("%p %v\n", &v, v)
switch v := v.(type) {
case nil:
fmt.Printf("%p %v\n", &v, v)
fmt.Printf("nil type[%T] %v\n", v, v)
case Student:
fmt.Printf("%p %v\n", &v, v)
fmt.Printf("Student type[%T] %v\n", v, v)
case *Student:
fmt.Printf("%p %v\n", &v, v)
fmt.Printf("*Student type[%T] %v\n", v, v)
default:
fmt.Printf("%p %v\n", &v, v)
fmt.Printf("unknow\n")
}
}
type Student struct {
Name string
Age int
}
用interface实现多态
Go
语言并没有设计诸如虚函数、纯虚函数、继承、多重继承等概念,但它通过接口却非常优雅地支持了面向对象的特性。多态是一种运行期的行为,它有以下几个特点:
- 1.一种类型具有多种类型的能力
- 2.允许不同的对象对同一消息做出灵活的反应
- 3.以一种通用的方式对待个使用的对象
- 4.非动态语言必须通过继承和接口的方式来实现
package main
import "fmt"
func main() {
qcrao := Student{age: 18}
whatJob(&qcrao)
growUp(&qcrao)
fmt.Println(qcrao)
stefno := Programmer{age: 100}
whatJob(stefno)
growUp(stefno)
fmt.Println(stefno)
}
func whatJob(p Person) {
p.job()
}
func growUp(p Person) {
p.growUp()
}
type Person interface {
job()
growUp()
}
type Student struct {
age int
}
func (p Student) job() {
fmt.Println("I am a student.")
return
}
func (p *Student) growUp() {
p.age += 1
return
}
type Programmer struct {
age int
}
func (p Programmer) job() {
fmt.Println("I am a programmer.")
return
}
func (p Programmer) growUp() {
// 程序员老得太快 ^_^
p.age += 10
return
}
main
函数里先生成 Student
和 Programmer
的对象,再将它们分别传入到函数 whatJob
和 growUp
。函数中,直接调用接口函数,实际执行的时候是看最终传入的实体类型是什么,调用的是实体类型实现的函数。于是,不同对象针对同一消息就有多种表现,多态
就实现了。更深入一点来说的话,在函数
whatJob()
或者 growUp()
内部,接口 person
绑定了实体类型 *Student
或者 Programmer
。根据前面分析的 iface
源码,这里会直接调用 fun
里保存的函数,类似于:s.tab->fun[0]
,而因为 fun
数组里保存的是实体类型实现的函数,所以当函数传入不同的实体类型时,调用的实际上是不同的函数实现,从而实现多态。
Loading Comments...