June's Studio.

Golang-数组与切片

字数统计: 2.3k阅读时长: 9 min
2022/11/23

Go语言与鸭子类型的关系

鸭子类型 Duck Typing

If it looks like a duck,swims like a duck ,and quacks like a duck ,then it probable is a duck

Duck Typing,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。Go 语言作为一门静态语言,它通过通过接口的方式完美支持鸭子类型。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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!")
}


func main() {
golang := Go{}
php := PHP{}

sayHello(golang)
sayHello(php)
}

在 main 函数中,调用调用 sayHello() 函数时,传入了 golang, php 对象,它们并没有显式地声明实现了 IGreeting 类型,只是实现了接口所规定的 sayHello() 函数。
实际上,编译器在调用 sayHello() 函数时,会隐式地将 golang, php 对象转换成 IGreeting 类型,这也是静态语言的类型检查功能

动态语言特点:

变量绑定的类型是不确定的,在运行期间才能确定 函数和方法可以接收任何类型的参数,且调用时不检查参数类型 不需要实现接口

总结

鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由它 “当前方法和属性的集合” 决定。Go 作为一种静态语言,通过接口实现了 鸭子类型,实际上是 Go 的编译器在其中作了隐匿的转换工作。


方法和函数的关系

方法本质上就是一个函数,只是多了一个接收者。接受者可以是值接收者,也可以是指针接收者

当实现了一个接收者是值类型的方法,就可以自动生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者。
但是,当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。

如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。

如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。

使用指针作为方法的接收者的理由:

  • 方法能够修改接收者指向的值。
  • 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。

iface和eface

iface 和 eface 都是 Go 中描述接口的底层结构体,区别在于 iface 描述的接口包含方法,而 eface 则是不包含任何方法的空接口:interface{}。

iface

1
2
3
4
type iface struct {
tab *itab // 指向一个itab结构体
data unsafe.Pointer //接口具体的值。一般指向堆的一个指针
}
1
2
3
4
5
6
7
8
9
10
type itab struct {
inter *interfacetype //接口的类型
_type *_type // 实体的类型,包括内存对齐方式、大小等
link *itab
hash uint32 // copy of _type.hash. Used for type switches. 类型的hash值,用于快速判断类型是否相等时使用
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // variable sized ,第一个方法的地址,实现了接口调用方法的动态分配。
}
1
2
3
4
5
type interfacetype struct {
typ _type // _type 实际上是描述 Go 语言中各种数据类型的结构体
pkgpath name //接口的包名
mhdr []imethod // 接口所定义的函数列表
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
}

eface

1
2
3
4
type eface struct {
_type *_type //表示空接口所承载的具体的实体类型
data unsafe.Pointer // 描述了具体的值
}

接口的 动态类型 和 动态值

iface包含两个字段: itab ,data;分别被称为动态类型 和动态值。 而接口值包含动态类型和动态值。所以 ,接口零值是指 动态类型动态值 都为nil。 当仅且当这两部分的值都为nil的情况下,
这个接口才会被认为 接口值==null


编译器自动检测类型是否实现接口

编译器会由此自动检查 *myWriter 类型是否实现了 io.Writer 接口

1
2
var _ io.Writer = (*myWriter)(nil)
var _ io.Writer = myWriter{}

上述赋值语句会发生隐式地类型转换,在转换的过程中,编译器会检测等号右边的类型是否实现了等号左边接口所规定的函数。

接口的构造过程

类型的转换 和断言 的区别

类型转换

对于类型转换而言,转换前后的两个类型要相互兼容才行。类型转换的语法为:

<结果类型> := <目标类型> ( <表达式> )

断言

前面说过,因为空接口 interface{} 没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是 interface{},那么在函数中,需要对形参进行断言,从而得到它的真实类型。

断言的语法为:

<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言
<目标类型的值> := <表达式>.( 目标类型 )  //非安全类型断言

1
2
3
4
5
6
7
func main() {
var i interface{} = new(Student)
s, ok := i.(Student)
if ok {
fmt.Println(s)
}
}

断言其实还有另一种形式,就是用在利用 switch 语句判断接口的类型。
每一个 case 会被顺序地考虑。当命中一个 case 时,就会执行 case 中的语句,
因此 case 语句的顺序是很重要的,因为很有可能会有多个 case 匹配的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
}

【引申1】

fmt.Println 函数的参数是 interface。对于内置类型,函数内部会用穷举法,得出它的真实类型,然后转换为字符串打印。而对于自定义类型,首先确定该类型是否实现了 String() 方法,如果实现了,则直接打印输出 String() 方法的结果;否则,会通过反射来遍历对象的成员进行打印。

如何用interface实现多态

多态是一种运行期的行为,它有以下几个特点:

1.一种类型具有多重类型的能力
2.允许不同的对象对同一消息做出灵活的反应
3.以一种通用的方式对待每个使用的对象
4.非动态语言必须通过继承和接口的方式来实现

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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
}

定义了一个Person 接口,包含两个函数:
job()
growUp()

然后又定义了两个结构体 ,Student 和 Programmer。同时,类型 *Student、Programmer实现了 Person 定义的两个函数。 (*Student类型实现了接口,Student类型却没有)

之后,又定义了函数参数是Person接口的两个本函数:

func whatJob(p Person)
func growUp(p Person)

main函数里面先生成 Student 和Programmer 的对象,再将他们分别传入到函数 whatJob和growUp。 函数中直接调用接口函数 职级执行的时候看最终传入的实体类型是什么,调用的是实体类型实现的函数。
于是,不同对象针对同一消息就有多重表现,多态就实现了。

CATALOG
  1. 1. Go语言与鸭子类型的关系
    1. 1.1. 鸭子类型 Duck Typing
  2. 2. 总结
  3. 3. 方法和函数的关系
  4. 4. iface和eface
    1. 4.1. iface
    2. 4.2. eface
  5. 5. 接口的 动态类型 和 动态值
  6. 6. 编译器自动检测类型是否实现接口
  7. 7. 接口的构造过程
  8. 8. 类型的转换 和断言 的区别
    1. 8.1. 类型转换
    2. 8.2. 断言
    3. 8.3. 如何用interface实现多态