用类型嵌入模拟实现“继承”
# 4.用类型嵌入模拟实现“继承“
Go中主要使用的是独立的自定义类型的方法设计,就是这个类型的所有方法都是自己显式实现的。
比如自定义类型 T 有两个方法 M1 和 M2,如果 T 是一个独立的自定义类型,那在声明类型 T 的 Go 包源码文件中一定可以找到其所有方法的实现代码:
func (T) M1() {...}
func (T) M2() {...}
2
还存在某种自定义类型的方法不是自己显式实现的,让某个自定义类型“继承”其他类型的方法实现。Go 语言从设计伊始,就决定不支持经典面向对象的编程范式与语法元素,所以这里只是借用了“继承”这个词汇,说是“继承”,实则依旧是一种==组合==的思想。
这种“继承”,是通过 Go 语言的==类型嵌入(Type Embedding)==来实现的。
# 4.1 类型嵌入
类型嵌入指的就是在一个类型的定义中嵌入了其他类型。
Go 语言支持两种类型嵌入:
- 接口类型的类型嵌入
- 结构体类型的类型嵌入
# 4.1.1 接口类型的类型嵌入
接口类型声明了由一个方法集合代表的接口:
type E interface {
M1()
M2()
}
2
3
4
这个接口类型 E 的方法集合,包含两个方法,分别是 M1 和 M2,它们组成了 E 这个接口类型所代表的接口。如果某个类型实现了方法 M1 和 M2,就说这个类型实现了 E 所代表的接口。
定义另外一个接口类型 I,它的方法集合中包含了三个方法 M1、M2 和 M3:
type I interface {
M1()
M2()
M3()
}
2
3
4
5
接口类型 I 方法集合中的 M1 和 M2,与接口类型 E 的方法集合中的方法完全相同。在这种情况下,可以用接口类型 E 替代上面接口类型 I 定义中 M1 和 M2,如下:
type I interface {
E
M3()
}
2
3
4
像这种在一个接口类型(I)定义中,嵌入另外一个接口类型(E)的方式,就是==接口类型的类型嵌入==。
这种接口类型嵌入的语义就是新接口类型(如接口类型 I)将嵌入的接口类型(如接口类型 E)的方法集合,并入到自己的方法集合中。
按 Go 语言惯例,Go 中的接口类型中只包含少量方法,并且常常只是一个方法。通过在接口类型中嵌入其他接口类型可以实现接口的组合,这也是 Go 语言中基于已有接口类型构建新接口类型的惯用法。
Go 标准库中 io 包的 ReadWriter、ReadWriteCloser 等接口类型就是通过嵌入 Reader、Writer 或 Closer 三个基本的接口类型组合而成的。
下面是仅包含单一方法的 io 包 Reader、Writer 和 Closer 的定义:
// $GOROOT/src/io/io.go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
2
3
4
5
6
7
8
9
10
11
12
13
下面的 io 包的 ReadWriter、ReadWriteCloser 等接口类型,通过嵌入上面基本接口类型组合而形成:
type ReadWriter interface {
Reader
Writer
}
type ReadCloser interface {
Reader
Closer
}
type WriteCloser interface {
Writer
Closer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 4.1.2 结构体类型的类型嵌入
Go 结构体类型:
type S struct {
A int
b string
c T
p *P
_ [10]int8
F func()
}
2
3
4
5
6
7
8
结构体类型 S 中的每个字段(field)都有唯一的名字与对应的类型,即便是使用空标识符占位的字段,它的类型也是明确的。
但这还不是 Go 结构体类型的“完全体”。Go 结构体类型定义还有另外一种形式,那就是带有嵌入字段(Embedded Field)的结构体定义。
type T1 int
type t2 struct{
n int
m int
}
type I interface {
M1()
}
type S1 struct {
T1
*t2
I
a int
b string
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
结构体 S1 定义中有三个“非常规形式”的标识符,分别是 T1、t2 和 I,它们既代表字段的名字,也代表字段的类型。它们的具体含义:
- 标识符 T1 表示字段名为 T1,它的类型为自定义类型 T1;
- 标识符 t2 表示字段名为 t2,它的类型为自定义结构体类型 t2 的指针类型;
- 标识符 I 表示字段名为 I,它的类型为接口类型 I。
这种以某个类型名、类型的指针类型名或接口类型名,直接作为结构体字段的方式就叫做==结构体的类型嵌入==,这些字段也被叫做==嵌入字段(Embedded Field)==。
嵌入字段的可见性与嵌入字段的类型的可见性是一致的。如果嵌入类型的名字是首字母大写的,那么也就说明这个嵌入字段是可导出的。嵌入字段的用法:
type MyInt int
func (n *MyInt) Add(m int) {
*n = *n + MyInt(m)
}
type t struct {
a int
b int
}
type S struct {
*MyInt
t
io.Reader
s string
n int
}
func main() {
m := MyInt(17)
r := strings.NewReader("hello, go")
s := S{
MyInt: &m,
t: t{
a: 1,
b: 2,
},
Reader: r,
s: "demo",
}
var sl = make([]byte, len("hello, go"))
s.Reader.Read(sl)
fmt.Println(string(sl)) // hello, go
s.MyInt.Add(5)
fmt.Println(*(s.MyInt)) // 22
}
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
结构体类型 S 使用了类型嵌入方式进行定义,它有三个嵌入字段 MyInt、t 和 Reader。使用 field:value 方式对 S 类型的变量 s 的各个字段进行初始化。和普通的字段一样,初始化嵌入字段时,可以直接用嵌入字段名作为 field。
通过变量 s 使用这些嵌入字段时,也可以像普通字段那样直接用变量s+字段选择符.+嵌入字段的名字,比如 s.Reader。还可以通过这种方式调用嵌入字段的方法,比如 s.Reader.Read 和 s.MyInt.Add。
Go 还是对嵌入字段有一些约束的。比如,和 Go 方法的 receiver 的基类型一样,嵌入字段类型的底层类型不能为指针类型。而且,嵌入字段的名字在结构体定义也必须是唯一的,这也意味这如果两个类型的名字相同,它们无法同时作为嵌入字段放到同一个结构体定义中。不过,这些约束你了解一下就可以了,一旦违反,Go 编译器会提示你的。
# 4.2 实现"继承"的原理
使用嵌入字段,可以在 Go 中实现方法的“继承”。
type MyInt int
func (n *MyInt) Add(m int) {
*n = *n + MyInt(m)
}
type t struct {
a int
b int
}
type S struct {
*MyInt
t
io.Reader
s string
n int
}
func main() {
m := MyInt(17)
r := strings.NewReader("hello, go")
s := S{
MyInt: &m,
t: t{
a: 1,
b: 2,
},
Reader: r,
s: "demo",
}
var sl = make([]byte, len("hello, go"))
s.Read(sl)
fmt.Println(string(sl))
s.Add(5)
fmt.Println(*(s.MyInt))
}
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
Read 方法与 Add 方法就是类型 S 方法集合中的方法。
这两个方法就来自结构体类型 S 的两个嵌入字段 Reader 和 MyInt。结构体类型 S“继承”了 Reader 字段的方法 Read 的实现,也“继承”了 *MyInt 的 Add 方法的实现。
注意,这里的“继承”用了引号,说明这并不是真正的继承,它只是 Go 语言的一种“障眼法”。工作机制:当通过结构体类型 S 的变量 s 调用 Read 方法时,Go 发现结构体类型 S 自身并没有定义 Read 方法,于是 Go 会查看 S 的嵌入字段对应的类型是否定义了 Read 方法。这个时候,Reader 字段就被找了出来,之后 s.Read 的调用就被转换为 s.Reader.Read 调用。这样一来,嵌入字段 Reader 的 Read 方法就被提升为 S 的方法,放入了类型 S 的方法集合。同理 *MyInt 的 Add 方法也被提升为 S 的方法而放入 S 的方法集合。
从外部来看,这种嵌入字段的方法的提升就给了我们一种结构体类型 S“继承”了 io.Reader 类型 Read 方法的实现,以及 *MyInt 类型 Add 方法的实现的错觉。
类型嵌入这种看似“继承”的机制,实际上是一种组合的思想。更具体点,它是一种组合中的代理(delegate)模式,如下图所示:
S 只是一个代理(delegate),对外它提供了它可以代理的所有方法,如例子中的 Read 和 Add 方法。当外界发起对 S 的 Read 方法的调用后,S 将该调用委派给它内部的 Reader 实例来实际执行 Read 方法。
# 4.3 类型嵌入与方法集合
# 4.3.1 结构体类型中嵌入接口类型
在结构体类型中嵌入接口类型后,结构体类型的方法集合发生的变化:
type I interface {
M1()
M2()
}
type T struct {
I
}
func (T) M3() {}
func main() {
var t T
var p *T
dumpMethodSet(t)
dumpMethodSet(p)
}
/*
main.T's method set:
- M1
- M2
- M3
*main.T's method set:
- M1
- M2
- M3
*/
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
原本结构体类型 T 只带有一个方法 M3,但在嵌入接口类型 I 后,结构体类型 T 的方法集合中又并入了接口类型 I 的方法集合。并且,由于 *T 类型方法集合包括 T 类型的方法集合,因此无论是类型 T 还是类型 *T,它们的方法集合都包含 M1、M2 和 M3。
于是可以得出一个结论:结构体类型的方法集合,包含嵌入的接口类型的方法集合。
注意:当结构体嵌入的多个接口类型的方法集合存在交集时,要小心编译器可能会出现的错误提示。Go 1.14 版本解决了嵌入接口类型的方法集合有交集的情况,但仅限于接口类型中嵌入接口类型,这里是在结构体类型中嵌入方法集合有交集的接口类型。
嵌入了其他类型的结构体类型本身是一个代理,在调用其实例所代理的方法时,Go 会首先查看结构体自身是否实现了该方法。如果实现了,Go 就会优先使用结构体自己实现的方法。如果没有实现,那么 Go 就会查找结构体中的嵌入字段的方法集合中,是否包含了这个方法。如果多个嵌入字段的方法集合中都包含这个方法,那么就说方法集合存在交集。这个时候,Go 编译器就会因无法确定究竟使用哪个方法而报错。如下:
type E1 interface {
M1()
M2()
M3()
}
type E2 interface {
M1()
M2()
M4()
}
type T struct {
E1
E2
}
func main() {
t := T{}
t.M1()
t.M2()
}
/*
main.go:22:3: ambiguous selector t.M1
main.go:23:3: ambiguous selector t.M2
*/
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
Go 编译器给出了错误提示,表示在调用 t.M1 和 t.M2 时,编译器都出现了分歧。在这个例子中,结构体类型 T 嵌入的两个接口类型 E1 和 E2 的方法集合存在交集,都包含 M1 和 M2,而结构体类型 T 自身呢,又没有实现 M1 和 M2,所以编译器会因无法做出选择而报错。
两种解决方案:
- 可以消除 E1 和 E2 方法集合存在交集的情况。
- 为 T 增加 M1 和 M2 方法的实现,这样的话,编译器便会直接选择 T 自己实现的 M1 和 M2,不会陷入两难境地。
// T 增加了 M1 和 M2 方法实现
... ...
type T struct {
E1
E2
}
func (T) M1() { println("T's M1") }
func (T) M2() { println("T's M2") }
func main() {
t := T{}
t.M1() // T's M1
t.M2() // T's M2
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
结构体类型嵌入接口类型在日常编码中有一个妙用,就是可以简化单元测试的编写。
由于嵌入某接口类型的结构体类型的方法集合包含了这个接口类型的方法集合,这就意味着,这个结构体类型也是它嵌入的接口类型的一个实现。即便结构体类型自身并没有实现这个接口类型的任意一个方法,也没有关系。如下:
package employee
type Result struct {
Count int
}
func (r Result) Int() int { return r.Count }
type Rows []struct{}
type Stmt interface {
Close() error
NumInput() int
Exec(stmt string, args ...string) (Result, error)
Query(args []string) (Rows, error)
}
// 返回男性员工总数
func MaleCount(s Stmt) (int, error) {
result, err := s.Exec("select count(*) from employee_tab where gender=?", "1")
if err != nil {
return 0, err
}
return result.Int(), nil
}
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
有一个 employee 包,这个包中的方法 MaleCount,通过传入的 Stmt 接口的实现从数据库获取男性员工的数量。
现在的任务是要对 MaleCount 方法编写单元测试代码。对于这种依赖外部数据库操作的方法,我们的惯例是使用“伪对象(fake object)”来冒充真实的 Stmt 接口实现。
不过现在有一个问题,那就是 Stmt 接口类型的方法集合中有四个方法,而 MaleCount 函数只使用了 Stmt 接口的一个方法 Exec。如果针对每个测试用例所用的伪对象都实现这四个方法,那么这个工作量有些大。
那么这个时候,结构体类型嵌入接口类型便可以帮助我们快速建立伪对象。解决方案:
package employee
import "testing"
type fakeStmtForMaleCount struct {
Stmt
}
func (fakeStmtForMaleCount) Exec(stmt string, args ...string) (Result, error) {
return Result{Count: 5}, nil
}
func TestEmployeeMaleCount(t *testing.T) {
f := fakeStmtForMaleCount{}
c, _ := MaleCount(f)
if c != 5 {
t.Errorf("want: %d, actual: %d", 5, c)
return
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
为 TestEmployeeMaleCount 测试用例建立一个 fakeStmtForMaleCount 的伪对象类型,然后在这个类型中嵌入了 Stmt 接口类型。这样 fakeStmtForMaleCount 就实现了 Stmt 接口,也实现了快速建立伪对象的目的。
下面只需要为 fakeStmtForMaleCount 实现 MaleCount 所需的 Exec 方法,就可以满足这个测试的要求了。
# 4.3.2 结构体类型中嵌入结构体类型
外部的结构体类型 T 可以“继承”嵌入的结构体类型的所有方法的实现。并且,无论是 T 类型的变量实例还是 *T 类型变量实例,都可以调用所有“继承”的方法。
在这种情况下,带有嵌入类型的新类型究竟“继承”了哪些方法:
type T1 struct{}
func (T1) T1M1() { println("T1's M1") }
func (*T1) PT1M2() { println("PT1's M2") }
type T2 struct{}
func (T2) T2M1() { println("T2's M1") }
func (*T2) PT2M2() { println("PT2's M2") }
type T struct {
T1
*T2
}
func main() {
t := T{
T1: T1{},
T2: &T2{},
}
dumpMethodSet(t)
dumpMethodSet(&t)
}
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
结构体类型 T 有两个嵌入字段,分别是 T1 和 *T2,根据对结构体的方法集合的理解,T1 与 *T1、T2 与 *T2 的方法集合是不同的:
- T1 的方法集合包含:T1M1;
- *T1 的方法集合包含:T1M1、PT1M2;
- *T2 的方法集合包含:T2M1;
- T2 的方法集合包含:T2M1、PT2M2。
作为嵌入字段嵌入到 T 中后,对 T 和 *T 的方法集合的影响也是不同的。运行示例输出结果:
/*
main.T's method set:
- PT2M2
- T1M1
- T2M1
*main.T's method set:
- PT1M2
- PT2M2
- T1M1
- T2M1
*/
2
3
4
5
6
7
8
9
10
11
12
T 和 *T 类型的方法集合差别:
- 类型 T 的方法集合 = T1 的方法集合 + *T2 的方法集合
- 类型 *T 的方法集合 = *T1 的方法集合 + *T2 的方法集合
# 4.4 defined 类型与 alias 类型的方法集合
Go 语言中,凡通过类型声明语法声明的类型都被称为 defined 类型。
type I interface {
M1()
M2()
}
type T int
type NT T // 基于已存在的类型T创建新的defined类型NT
type NI I // 基于已存在的接口类型I创建新defined接口类型NI
2
3
4
5
6
7
新定义的 defined 类型与原 defined 类型是不同的类型。
- 对于那些基于接口类型创建的 defined 的接口类型,它们的方法集合与原接口类型的方法集合是一致的。
- 对于基于非接口类型的 defined 类型创建的非接口类型,它们的方法集合不会继承原接口类型的方法集合。
package main
type T struct{}
func (T) M1() {}
func (*T) M2() {}
type T1 T
func main() {
var t T
var pt *T
var t1 T1
var pt1 *T1
dumpMethodSet(t)
dumpMethodSet(t1)
dumpMethodSet(pt)
dumpMethodSet(pt1)
}
/*
main.T's method set:
- M1
main.T1's method set is empty!
*main.T's method set:
- M1
- M2
*main.T1's method set is empty!
*/
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
基于一个 defined 的非接口类型 T 创建了新 defined 类型 T1,并且分别输出 T1 和 *T1 的方法集合来确认它们是否“继承”了 T 的方法集合。
从输出结果上看,新类型 T1 并没有“继承”原 defined 类型 T 的任何一个方法。从逻辑上来说,这也符合 T1 与 T 是两个不同类型的语义。
基于自定义非接口类型的 defined 类型的方法集合为空的事实,也决定了即便原类型实现了某些接口,基于其创建的 defined 类型也没有“继承”这一隐式关联。也就是说,新 defined 类型要想实现那些接口,仍然需要重新实现接口的所有方法。
alias类型:类型别名(type alias)
基于类型别名(type alias)定义的新类型,无论原类型是接口类型还是非接口类型,类型别名都与原类型拥有完全相同的方法集合。
type T struct{}
func (T) M1() {}
func (*T) M2() {}
type T1 = T
func main() {
var t T
var pt *T
var t1 T1
var pt1 *T1
dumpMethodSet(t)
dumpMethodSet(t1)
dumpMethodSet(pt)
dumpMethodSet(pt1)
}
/*
main.T's method set:
- M1
main.T's method set:
- M1
*main.T's method set:
- M1
- M2
*main.T's method set:
- M1
- M2
*/
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
dumpMethodSet 函数甚至都无法识别出“类型别名”,无论类型别名还是原类型,输出的都是原类型的方法集合。