Go语言的类型系统 -《Go In Action》-Ch5

Go语言的类型系统 -《Go In Action》-Ch5

Go语言是一种静态类型的编程语言,编译器需要在编译时知道每个值的类型

5.1 用户定义类型

声明一个新类型,即告诉编译器类型需要的内存大小和表示信息。

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
// 第一种声明方式
type user struct {
name string
email string
}

// 使用结构类型声明变量,并初始化为其零值
// 声明user 类型的变量
var bill user

// 使用结构字面量来声明一个结构类型的变量
// 声明user 类型的变量,并初始化所有字段
lisa := user{
name: "Lisa",
email: "lisa@email.com",
ext: 123,
privileged: true,
}

// 不使用字段名,创建结构类型的值
// 声明user 类型的变量
lisa := user{"Lisa", "lisa@email.com", 123, true}

// 使用其他结构类型声明字段
// admin 需要一个user 类型作为管理者,并附加权限
type admin struct {
person user
level string
}

// 使用结构字面量来创建字段的值
// 声明admin 类型的变量
fred := admin{
person: user{
name: "Lisa",
email: "lisa@email.com",
ext: 123,
privileged: true,
},
level: "super",
}

// 第二种声明方式
// 基于int64 声明一个新类型
// int64 和 Duration 是两种不同的类型,int64是Duration的基础类型,试图对ini64和Duration相互赋值将产生编译错误
package main
type Duration int64

func main() {
var dur Duration
dur = int64(1000)
}

// prog.go:7: cannot use int64(1000) (type int64) as type Duration
// in assignment

5.2 方法

方法能给用户定义的类型添加新的行为,方法实际上也是函数,只是在声明时,在关键字func和方法名之间的增加了一个参数,
这个参数被称为接收者,将函数和接收者的类型绑在一起。

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
// 这个示例程序展示如何声明
// 并使用方法
package main

import (
"fmt"
)

// user 在程序里定义一个用户类型
type user struct {
name string
email string
}

// notify 使用值接收者实现了一个方法
func (u user) notify() {
fmt.Printf("Sending User Email To %s<%s>\n",
u.name,
u.email)
}

// changeEmail 使用指针接收者实现了一个方法
func (u *user) changeEmail(email string) {
u.email = email
}

// main 是应用程序的入口
func main() {
// user 类型的值可以用来调用
// 使用值接收者声明的方法
bill := user{"Bill", "bill@email.com"}
bill.notify()

// 指向user 类型值的指针也可以用来调用
// 使用值接收者声明的方法
lisa := &user{"Lisa", "lisa@email.com"}
lisa.notify()

// user 类型的值可以用来调用
// 使用指针接收者声明的方法
bill.changeEmail("bill@newdomain.com")
bill.notify()

notify接受者是user值的一个副本,notify也可以使用指针调用,Go会在背后执行一个转换操作

1
2
3
// Go在代码背后的执行动作
lisa := &user{"Lisa", "lisa@email.com"}
*(lisa).notify()

可以不到不管是使用值调用,还是使用指针调用,notify函数的接收者都是一个user的副本,对副本的修改并不会影响原来的值
changeEmail恰恰相反,他的接受者是指针,这种情况函数对值进行的修改,会影响到原来的变量值,绑定指针类型的函数,也可以接受值的调用
Go会在背后做如下优化

1
(&bill).changeEmail("bill@newdomain.com")

5.3 类型的本质

一个类型在以参数在函数间传递或者作为接受者绑定方法时,需要根据类型的特点以及使用的方法,去决定是传指针还是传值

5.3.1 内置类型

原始的,内置类型是语言提供的一组类型,诸如数值类型、字符串类型和布尔类型,对于这种类型的传递一般是传值,因为对这些值进行增加或者删除的时候,会创建一个新的值

5.3.2 引用类型

非原始的,引用类型诸如切片、映射、通道、接口和函数类型,每个引用类型包含一组独特的字段,用于管理底层数据。不需要共享一个引用类型的值,可以通过赋值来传递一个引用类型的值的副本,本质上这就是在共享底层数据结构

5.3.3 结构类型

结构类型有可能是原始的,也有可能是非原始的,需要遵守上面内置类型和引用类型的规范。是使用值接受者还是使用指针接受者,不应该由该方法是否修改了接受到的值来决定,应该基于该类型的本质。

5.4 接口

多态是指代码可以根据类型的具体实现采取不同行为的能力,如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值。

5.4.1 标准库

下面这个程序实现了类似于curl的基本功能,io.Copy的第一个参数是复制到的目标,这个参数是必须实现了io.Writer接口的值,os.Stdout实现了io.Writer。
io.Copy的第二个参数接收一个io.Reader接口类型的值,表示数据流入的源,http.Response.Body实现了io.Reader接口

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
package main

import (
"fmt"
"io"
"net/http"
"os"
)

// init is called before main.
func init() {
if len(os.Args) != 2 {
fmt.Println("Usage: ./example2 <url>")
os.Exit(-1)
}
}

// main is the entry point for the application.
func main() {
// Get a response from the web server.
r, err := http.Get(os.Args[1])
if err != nil {
fmt.Println(err)
return
}

// Copies from the Body to Stdout.
io.Copy(os.Stdout, r.Body)
if err := r.Body.Close(); err != nil {
fmt.Println(err)
}
}

5.4.2 实现

接口是用来定义行为的类型。行为通过方法由用户定义的类型实现。用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。

对接口值方法的调用会执行接口值里存储的用户定义的类型的值的方法。将自定义类型赋值给接口分两种情况,自定义类型的值赋值给接口值和自定义类型指针赋值给接口值。下面两幅图展示了分别赋值给接口值后接口值的内存布局

值赋值后接口值
指针赋值后接口值

接口值是两个字长度的数据结构,第一个字包含一个指向内部表的指针。内部表叫做iTable,包含了所存储的值的类型信息。iTable包含了已存储的值的类型信息和与这个值相关联的的一组方法。
第二个字是一个指向所存储值的指针。

5.4.3 方法集

方法集定义了接口的接受规则

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
package main

import (
"fmt"
)

// notifier is an interface that defined notification
// type behavior.
type notifier interface {
notify()
}

// user defines a user in the program.
type user struct {
name string
email string
}

// notify implements a method with a pointer receiver.
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}

// main is the entry point for the application.
func main() {
// Create a value of type User and send a notification.
u := user{"Bill", "bill@email.com"}

sendNotification(u)

// ./listing36.go:32: cannot use u (type user) as type
// notifier in argument to sendNotification:
// user does not implement notifier
// (notify method has pointer receiver)
}

// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
n.notify()
}

上面的程序会编译失败,错误的原因是user类型的值并没有实现notify接口。这里涉及到了方法集的概念,方法集定义了一组关联到给定类型的值或指针的方法。
定义方法时使用的接收者的类型决定了这个方法时关联到值还是关联到指针,还是两个都关联。
Go语言规范里定义了方法集的规则。

1
2
3
4
5
6
7
8
9
10

Values Methods Receivers
-----------------------------------------------
T (t T)
*T (t T) and (t *T)

Methods Receivers Values
-----------------------------------------------
(t T) T and *T
(t *T) *T

也就是说T类型的方法集只包含了值接收者声明的方法。而*T的方法集即包含了值接收者声明的方法,也包含指针接受者声明的方法。
那么上面错误代码的解决方式就有两种,一种是sendNotification(&n),因为&n即包含了值接收者方法,也包含了指针接收者方法。另一种是将notify修改为值接收方法。

5.4.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
package main

import (
"fmt"
)

// notifier is an interface that defines notification
// type behavior.
type notifier interface {
notify()
}

// user defines a user in the program.
type user struct {
name string
email string
}

// notify implements the notifier interface with a pointer receiver.
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}

// admin defines a admin in the program.
type admin struct {
name string
email string
}

// notify implements the notifier interface with a pointer receiver.
func (a *admin) notify() {
fmt.Printf("Sending admin email to %s<%s>\n",
a.name,
a.email)
}

// main is the entry point for the application.
func main() {
// Create a user value and pass it to sendNotification.
bill := user{"Bill", "bill@email.com"}
sendNotification(&bill)

// Create an admin value and pass it to sendNotification.
lisa := admin{"Lisa", "lisa@email.com"}
sendNotification(&lisa)
}

// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
n.notify()
}

可以看到user和admin都实现了notify接口,对同一个行为做出了不同的表示。

5.6 嵌入类型

将已有的类型直接声明在新的结构类型里被称为嵌入类型。通过嵌入类型,与内部类型相关的标识会提升到外部类型上。这些被提升的标识符和直接声明在外部类型里一样。也是外部类型的一部分。

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
package main

import (
"fmt"
)

// user defines a user in the program.
type user struct {
name string
email string
}

// notify implements a method that can be called via
// a value of type user.
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}

// admin represents an admin user with privileges.
type admin struct {
user // Embedded Type
level string
}

// main is the entry point for the application.
func main() {
// Create an admin user.
ad := admin{
user: user{
name: "john smith",
email: "john@yahoo.com",
},
level: "super",
}

// We can access the inner type's method directly.
ad.user.notify()

// The inner type's method is promoted.
ad.notify()
}

可以看到user被嵌入到admin中,user的notify方法也被提升到了admin类型上,可以直接调用。如果admin自己也实现了notify接口,这时候user的notify方法不会被提升。

5.6 公开和未公开的标识符

当一个标识符的名字以小写字母开头时,这个标识符就是未公开的,即包外的代码不可见。大写字母开头表示是公开的,对包外的代码可见。

5.7 小结

使用关键字struct或者通过指定已存在的类型,可以声明用户定义的类型
方法提供了一种给用户定义的类型增加行为的方式
设计类型时需要确认类型的本质是原始的还是非原始的
接口是声明了一组行为并支持多态的类型
嵌入类型提供了扩展类型的能力,而无需使用继承
标识符要么是从包里公开的,要么是在包里未公开的

# Golang

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×