第5章 Go语言的类型系统

本章主要内容

Go语言是一种静态类型的编程语言。这意味着,编译器需要在编译时知晓程序里每个值的类型。如果提前知道类型信息,编译器就可以确保程序合理地使用值。这有助于减少潜在的内存异常和bug,并且使编译器有机会对代码进行一些性能优化,提高执行效率。

值的类型给编译器提供两部分信息:第一部分,需要分配多少内存给这个值(即值的规模);第二部分,这段内存表示什么。对于许多内置类型的情况来说,规模和表示是类型名的一部分。int64类型的值需要8字节(64位),表示一个整数值;float32类型的值需要4字节(32位),表示一个IEEE-754定义的二进制浮点数;bool类型的值需要1字节(8位),表示布尔值truefalse

有些类型的内部表示与编译代码的机器的体系结构有关。例如,根据编译所在的机器的体系结构,一个int值的大小可能是8字节(64位),也可能是4字节(32位)。还有一些与体系结构相关的类型,如Go语言里的所有引用类型。好在创建和使用这些类型的值的时候,不需要了解这些与体系结构相关的信息。但是,如果编译器不知道这些信息,就无法阻止用户做一些导致程序受损甚至机器故障的事情。

5.1 用户定义的类型

Go语言允许用户定义类型。当用户声明一个新类型时,这个声明就给编译器提供了一个框架,告知必要的内存大小和表示信息。声明后的类型与内置类型的运作方式类似。Go语言里声明用户定义的类型有两种方法。最常用的方法是使用关键字struct,它可以让用户创建一个结构类型。

结构类型通过组合一系列固定且唯一的字段来声明,如代码清单5-1所示。结构里每个字段都会用一个已知类型声明。这个已知类型可以是内置类型,也可以是其他用户定义的类型。

代码清单5-1 声明一个结构类型

01 // user在程序里定义一个用户类型
02 type user struct {
03   name    string
04   email   string
05   ext    int
06   privileged bool
07 }

在代码清单5-1中,可以看到一个结构类型的声明。这个声明以关键字type开始,之后是新类型的名字,最后是关键字struct。这个结构类型有4个字段,每个字段都基于一个内置类型。读者可以看到这些字段是如何组合成一个数据的结构的。一旦声明了类型(如代码清单5-2所示),就可以使用这个类型创建值。

代码清单5-2 使用结构类型声明变量,并初始化为其零值

09 // 声明user类型的变量
10 var bill user

在代码清单5-2的第10行,关键字var创建了类型为user且名为bill的变量。当声明变量时,这个变量对应的值总是会被初始化。这个值要么用指定的值初始化,要么用零值(即变量类型的默认值)做初始化。对数值类型来说,零值是0;对字符串来说,零值是空字符串;对布尔类型,零值是false。对这个例子里的结构,结构里每个字段都会用零值初始化。

任何时候,创建一个变量并初始化为其零值,习惯是使用关键字var。这种用法是为了更明确地表示一个变量被设置为零值。如果变量被初始化为某个非零值,就配合结构字面量和短变量声明操作符来创建变量。

代码清单5-3展示了如何声明一个user类型的变量,并使用某个非零值作为初始值。在第13行,我们首先给出了一个变量名,之后是短变量声明操作符。这个操作符是冒号加一个等号(:=)。一个短变量声明操作符在一次操作中完成两件事情:声明一个变量,并初始化。短变量声明操作符会使用右侧给出的类型信息作为声明变量的类型。

代码清单5-3 使用结构字面量来声明一个结构类型的变量

12 // 声明user类型的变量,并初始化所有字段
13 lisa := user{
14   name:    "Lisa",
15   email:   "lisa@email.com",
16   ext:    123,
17   privileged: true,
18 }

既然要创建并初始化一个结构类型,我们就使用结构字面量来完成这个初始化,如代码清单5-4所示。结构字面量使用一对大括号括住内部字段的初始值。

代码清单5-4 使用结构字面量创建结构类型的值

13 user{
14   name:    "Lisa",
15   email:   "lisa@email.com",
16   ext:    123,
17   privileged: true,
18 }

结构字面量可以对结构类型采用两种形式。代码清单5-4中使用了第一种形式,这种形式在不同行声明每个字段的名字以及对应的值。字段名与值用冒号分隔,每一行以逗号结尾。这种形式对字段的声明顺序没有要求。第二种形式没有字段名,只声明对应的值,如代码清单5-5所示。

代码清单5-5 不使用字段名,创建结构类型的值

12 // 声明user类型的变量
13 lisa := user{"Lisa", "lisa@email.com", 123, true}

每个值也可以分别占一行,不过习惯上这种形式会写在一行里,结尾不需要逗号。这种形式下,值的顺序很重要,必须要和结构声明中字段的顺序一致。当声明结构类型时,字段的类型并不限制在内置类型,也可以使用其他用户定义的类型,如代码清单5-6所示。

代码清单5-6 使用其他结构类型声明字段

20 // admin需要一个user类型作为管理者,并附加权限
21 type admin struct {
22   person user
23   level string
24 }

代码清单5-6展示了一个名为admin的新结构类型。这个结构类型有一个名为personuser类型的字段,还声明了一个名为levelstring字段。当创建具有person这种字段的结构类型的变量时,初始化用的结构字面量会有一些变化,如代码清单5-7所示。

代码清单5-7 使用结构字面量来创建字段的值

26 // 声明admin类型的变量
27 fred := admin{
28   person: user{
29     name:    "Lisa",
30     email:   "lisa@email.com",
31     ext:    123,
32     privileged: true,
33   },
34   level: "super",
35 }

为了初始化person字段,我们需要创建一个user类型的值。代码清单5-7的第28行就是在创建这个值。这行代码使用结构字面量的形式创建了一个user类型的值,并赋给了person字段。

另一种声明用户定义的类型的方法是,基于一个已有的类型,将其作为新类型的类型说明。当需要一个可以用已有类型表示的新类型的时候,这种方法会非常好用,如代码清单5-8所示。标准库使用这种声明类型的方法,从内置类型创建出很多更加明确的类型,并赋予更高级的功能。

代码清单5-8 基于int64声明一个新类型

type Duration int64

代码清单5-8展示的是标准库的time包里的一个类型的声明。Duration是一种描述时间间隔的类型,单位是纳秒(ns)。这个类型使用内置的int64类型作为其表示。在Duration类型的声明中,我们把int64类型叫作Duration的基础类型。不过,虽然int64是基础类型,Go并不认为Durationint64是同一种类型。这两个类型是完全不同的有区别的类型。

为了更好地展示这种区别,来看一下代码清单5-9所示的小程序。这个程序本身无法通过编译。

代码清单5-9 给不同类型的变量赋值会产生编译错误

01 package main
02
03 type Duration int64
04
05 func main() {
06   var dur Duration
07   dur = int64(1000)
08 }

代码清单5-9所示的程序在第03行声明了Duration类型。之后在第06行声明了一个类型为Duration的变量dur,并使用零值作为初值。之后,第7行的代码会在编译的时候产生编译错误,如代码清单5-10所示。

代码清单5-10 实际产生的编译错误

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

编译器很清楚这个程序的问题:类型int64的值不能作为类型Duration的值来用。换句话说,虽然int64类型是基础类型,Duration类型依然是一个独立的类型。两种不同类型的值即便互相兼容,也不能互相赋值。编译器不会对不同类型的值做隐式转换。

5.2 方法

方法能给用户定义的类型添加新的行为。方法实际上也是函数,只是在声明时,在关键字func和方法名之间增加了一个参数,如代码清单5-11所示。

代码清单5-11 listing11.go

01 // 这个示例程序展示如何声明
02 // 并使用方法
03 package main
04
05 import (
06   "fmt"
07 )
08
09 // user在程序里定义一个用户类型
10 type user struct {
11   name string
12   email string
13 }
14
15 // notify使用值接收者实现了一个方法
16 func (u user) notify() {
17   fmt.Printf("Sending User Email To %s<%s>\n",
18     u.name,
19     u.email)
20 }
21
22 // changeEmail使用指针接收者实现了一个方法
23 func (u *user) changeEmail(email string) {
24   u.email = email
25 }
26
27 // main是应用程序的入口
28 func main() {
29   // user类型的值可以用来调用
30   // 使用值接收者声明的方法
31   bill := user{"Bill", "bill@email.com"}
32   bill.notify()
33
34   // 指向user类型值的指针也可以用来调用
35   // 使用值接收者声明的方法
36   lisa := &user{"Lisa", "lisa@email.com"}
37   lisa.notify()
38
39   // user类型的值可以用来调用
40   // 使用指针接收者声明的方法
41   bill.changeEmail("bill@newdomain.com")
42   bill.notify()
43
44   // 指向user类型值的指针可以用来调用
45   // 使用指针接收者声明的方法
46   lisa.changeEmail("lisa@newdomain.com")
47   lisa.notify()
48 }

代码清单5-11的第16行和第23行展示了两种类型的方法。关键字func和函数名之间的参数被称作接收者,将函数与接收者的类型绑在一起。如果一个函数有接收者,这个函数就被称为方法。当运行这段程序时,会得到代码清单5-12所示的输出。

代码清单5-12 listing11.go的输出

Sending User Email To Bill<bill@email.com>
Sending User Email To Lisa<lisa@email.com>
Sending User Email To Bill<bill@newdomain.com>
Sending User Email To Lisa<lisa@comcast.com>

让我们来解释一下代码清单5-13所示的程序都做了什么。在第10行,该程序声明了名为user的结构类型,并声明了名为notify的方法。

代码清单5-13 listing11.go:第09行到第20行

09 // user在程序里定义一个用户类型
10 type user struct {
11   name string
12   email string
13 }
14
15 // notify使用值接收者实现了一个方法
16 func (u user) notify() {
17   fmt.Printf("Sending User Email To %s<%s>\n",
18     u.name,
19     u.email)
20 }

Go语言里有两种类型的接收者:值接收者指针接收者。在代码清单5-13的第16行,使用值接收者声明了notify方法,如代码清单5-14所示。

代码清单5-14 使用值接收者声明一个方法

func (u user) notify() {

notify方法的接收者被声明为user类型的值。如果使用值接收者声明方法,调用时会使用这个值的一个副本来执行。让我们跳到代码清单5-11的第32行来看一下如何调用notify方法,如代码清单5-15所示。

代码清单5-15 listing11.go:第29行到第32行

29   // user类型的值可以用来调用
30   // 使用值接收者声明的方法
31   bill := user{"Bill", "bill@email.com"}
32   bill.notify()

代码清单5-15展示了如何使用user类型的值来调用方法。第31行声明了一个user类型的变量bill,并使用给定的名字和电子邮件地址做初始化。之后在第32行,使用变量bill来调用notify方法,如代码清单5-16所示。

代码清单5-16 使用变量来调用方法

bill.notify()

这个语法与调用一个包里的函数看起来很类似。但在这个例子里,bill不是包名,而是变量名。这段程序在调用notify方法时,使用bill的值作为接收者进行调用,方法notify会接收到bill的值的一个副本。

也可以使用指针来调用使用值接收者声明的方法,如代码清单5-17所示。

代码清单5-17 listing11.go:第34行到第37行

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

代码清单5-17展示了如何使用指向user类型值的指针来调用notify方法。在第36行,声明了一个名为lisa的指针变量,并使用给定的名字和电子邮件地址做初始化。之后在第37行,使用这个指针变量来调用notify方法。为了支持这种方法调用,Go语言调整了指针的值,来符合方法接收者的定义。可以认为Go语言执行了代码清单5-18所示的操作。

代码清单5-18 Go在代码背后的执行动作

(*lisa).notify()

代码清单5-18展示了Go编译器为了支持这种方法调用背后做的事情。指针被解引用为值,这样就符合了值接收者的要求。再强调一次,notify操作的是一个副本,只不过这次操作的是从lisa指针指向的值的副本。

也可以使用指针接收者声明方法,如代码清单5-19所示。

代码清单5-19 listing11.go:第22行到第25行

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

代码清单5-19展示了changeEmail方法的声明。这个方法使用指针接收者声明。这个接收者的类型是指向user类型值的指针,而不是user类型的值。当调用使用指针接收者声明的方法时,这个方法会共享调用方法时接收者所指向的值,如代码清单5-20所示。

代码清单5-20 listing11.go:第36行和第44行到第46行

36   lisa := &user{"Lisa", "lisa@email.com"}

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

在代码清单5-20中,可以看到声明了lisa指针变量,还有第46行使用这个变量调用了changeEmail方法。一旦changeEmail调用返回,这个调用对值做的修改也会反映在lisa指针所指向的值上。这是因为changeEmail方法使用了指针接收者。总结一下,值接收者使用值的副本来调用方法,而指针接受者使用实际值来调用方法。

也可以使用一个值来调用使用指针接收者声明的方法,如代码清单5-21所示。

代码清单5-21 listing11.go:第31行和第39行到第41行

31   bill := user{"Bill", "bill@email.com"}

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

在代码清单5-21中可以看到声明的变量bill,以及之后使用这个变量调用使用指针接收者声明的changeEmail方法。Go语言再一次对值做了调整,使之符合函数的接收者,进行调用,如代码清单5-22所示。

代码清单5-22 Go在代码背后的执行动作

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

代码清单5-22展示了Go编译器为了支持这种方法调用在背后做的事情。在这个例子里,首先引用bill值得到一个指针,这样这个指针就能够匹配方法的接收者类型,再进行调用。Go语言既允许使用值,也允许使用指针来调用方法,不必严格符合接收者的类型。这个支持非常方便开发者编写程序。

应该使用值接收者,还是应该使用指针接收者,这个问题有时会比较迷惑人。可以遵从标准库里一些基本的指导方针来做决定。后面会进一步介绍这些指导方针。

5.3 类型的本质

在声明一个新类型之后,声明一个该类型的方法之前,需要先回答一个问题:这个类型的本质是什么。如果给这个类型增加或者删除某个值,是要创建一个新值,还是要更改当前的值?如果是要创建一个新值,该类型的方法就使用值接收者。如果是要修改当前值,就使用指针接收者。这个答案也会影响程序内部传递这个类型的值的方式:是按值做传递,还是按指针做传递。保持传递的一致性很重要。这个背后的原则是,不要只关注某个方法是如何处理这个值,而是要关注这个值的本质是什么。

5.3.1 内置类型

内置类型是由语言提供的一组类型。我们已经见过这些类型,分别是数值类型、字符串类型和布尔类型。这些类型本质上是原始的类型。因此,当对这些值进行增加或者删除的时候,会创建一个新值。基于这个结论,当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本。让我们看一下标准库里使用这些内置类型的值的函数,如代码清单5-23所示。

代码清单5-23 golang.org/src/strings/strings.go:第620行到第625行

620 func Trim(s string, cutset string) string {
621   if s == "" || cutset == "" {
622     return s
623   }
624   return TrimFunc(s, makeCutsetFunc(cutset))
625 }

在代码清单5-23中,可以看到标准库里strings包的Trim函数。Trim函数传入一个string类型的值作操作,再传入一个string类型的值用于查找。之后函数会返回一个新的string值作为操作结果。这个函数对调用者原始的string值的一个副本做操作,并返回一个新的string值的副本。字符串(string)就像整数、浮点数和布尔值一样,本质上是一种很原始的数据值,所以在函数或方法内外传递时,要传递字符串的一份副本。

让我们看一下体现内置类型具有的原始本质的第二个例子,如代码清单5-24所示。

代码清单5-24 golang.org/src/os/env.go:第38行到第44行

38 func isShellSpecialVar(c uint8) bool {
39   switch c {
40   case '*', '#', '$', '@', '!', '?', '0', '1', '2', '3', '4', '5',
                              '6', '7', '8', '9':
41     return true
42   }
43   return false
44 }

代码清单5-24展示了env包里的isShellSpecialVar函数。这个函数传入了一个int8类型的值,并返回一个bool类型的值。注意,这里的参数没有使用指针来共享参数的值或者返回值。调用者传入了一个uint8值的副本,并接受一个返回值true或者false

5.3.2 引用类型

Go语言里的引用类型有如下几个:切片、映射、通道、接口和函数类型。当声明上述类型的变量时,创建的变量被称作标头(header)值。从技术细节上说,字符串也是一种引用类型。每个引用类型创建的标头值是包含一个指向底层数据结构的指针。每个引用类型还包含一组独特的字段,用于管理底层数据结构。因为标头值是为复制而设计的,所以永远不需要共享一个引用类型的值。标头值里包含一个指针,因此通过复制来传递一个引用类型的值的副本,本质上就是在共享底层数据结构。

让我们看一下net包里的类型,如代码清单5-25所示。

代码清单5-25 golang.org/src/net/ip.go:第32行

32 type IP []byte

代码清单5-25展示了一个名为IP的类型,这个类型被声明为字节切片。当要围绕相关的内置类型或者引用类型来声明用户定义的行为时,直接基于已有类型来声明用户定义的类型会很好用。编译器只允许为命名的用户定义的类型声明方法,如代码清单5-26所示。

代码清单5-26 golang.org/src/net/ip.go:第329行到第337行

329 func (ip IP) MarshalText() ([]byte, error) {
330   if len(ip) == 0 {
331     return []byte(""), nil
332   }
333   if len(ip) != IPv4len && len(ip) != IPv6len {
334     return nil, errors.New("invalid IP address")
335   }
336   return []byte(ip.String()), nil
337 }

代码清单5-26里定义的MarshalText方法是用IP类型的值接收者声明的。一个值接收者,正像预期的那样通过复制来传递引用,从而不需要通过指针来共享引用类型的值。这种传递方法也可以应用到函数或者方法的参数传递,如代码清单5-27所示。

代码清单5-27 golang.org/src/net/ip.go:第318行到第325行

318 // ipEmptyString像ip.String一样, 
319 // 只不过在没有设置ip时会返回一个空字符串
320 func ipEmptyString(ip IP) string {
321   if len(ip) == 0 {
322     return ""
323   }
324   return ip.String()
325 }

在代码清单5-27里,有一个ipEmptyString函数。这个函数需要传入一个IP类型的值。再一次,你可以看到调用者传入的是这个引用类型的值,而不是通过引用共享给这个函数。调用者将引用类型的值的副本传入这个函数。这种方法也适用于函数的返回值。最后要说的是,引用类型的值在其他方面像原始的数据类型的值一样对待。

5.3.3 结构类型

结构类型可以用来描述一组数据值,这组值的本质即可以是原始的,也可以是非原始的。如果决定在某些东西需要删除或者添加某个结构类型的值时该结构类型的值不应该被更改,那么需要遵守之前提到的内置类型和引用类型的规范。让我们从标准库里的一个原始本质的类型的结构实现开始,如代码清单5-28所示。

代码清单5-28 golang.org/src/time/time.go:第39行到第55行

39 type Time struct {
40   // sec给出自公元1年1月1日00:00:00
41   // 开始的秒数
42   sec int64
43
44   // nsec指定了一秒内的纳秒偏移,
45   // 这个值是非零值,
46   // 必须在[0, 999999999]范围内
47   nsec int32
48
49   // loc指定了一个Location, 
50   // 用于决定该时间对应的当地的分、小时、
51   // 天和年的值
52   // 只有Time的零值,其loc的值是nil
53   // 这种情况下,认为处于UTC时区
54   loc *Location
55 }

代码清单5-28中的Time结构选自time包。当思考时间的值时,你应该意识到给定的一个时间点的时间是不能修改的。所以标准库里也是这样实现Time类型的。让我们看一下Now函数是如何创建Time类型的值的,如代码清单5-29所示。

代码清单5-29 golang.org/src/time/time.go:第781行到第784行

781 func Now() Time {
782   sec, nsec := now()
783   return Time{sec + unixToInternal, nsec, Local}
784 }

代码清单5-29中的代码展示了Now函数的实现。这个函数创建了一个Time类型的值,并给调用者返回了Time值的副本。这个函数没有使用指针来共享Time值。之后,让我们来看一个Time类型的方法,如代码清单5-30所示。

代码清单5-30 golang.org/src/time/time.go:第610行到第622行

610 func (t Time) Add(d Duration) Time {
611   t.sec += int64(d / 1e9)
612   nsec := int32(t.nsec) + int32(d%1e9)
613   if nsec >= 1e9 {
614     t.sec++
615     nsec -= 1e9
616   } else if nsec < 0 {
617     t.sec--
618     nsec += 1e9
619   }
620   t.nsec = nsec
621   return t
622 }

代码清单5-30中的Add方法是展示标准库如何将Time类型作为本质是原始的类型的绝佳例子。这个方法使用值接收者,并返回了一个新的Time值。该方法操作的是调用者传入的Time值的副本,并且给调用者返回了一个方法内的Time值的副本。至于是使用返回的值替换原来的Time值,还是创建一个新的Time变量来保存结果,是由调用者决定的事情。

大多数情况下,结构类型的本质并不是原始的,而是非原始的。这种情况下,对这个类型的值做增加或者删除的操作应该更改值本身。当需要修改值本身时,在程序中其他地方,需要使用指针来共享这个值。让我们看一个由标准库中实现的具有非原始本质的结构类型的例子,如代码清单5-31所示。

代码清单5-31 golang.org/src/os/file_unix.go:第15行到第29行

15 // File表示一个打开的文件描述符
16 type File struct {
17   *file
18 }
19
20 // file是*File的实际表示
21 // 额外的一层结构保证没有哪个os的客户端
22 // 能够覆盖这些数据。如果覆盖这些数据,
23 // 可能在变量终结时关闭错误的文件描述符
24 type file struct {
25   fd int
26   name string
27   dirinfo *dirInfo // 除了目录结构,此字段为nil
28   nepipe int32 // Write操作时遇到连续EPIPE的次数
29 }

可以在代码清单5-31里看到标准库中声明的File类型。这个类型的本质是非原始的。这个类型的值实际上不能安全复制。对内部未公开的类型的注释,解释了不安全的原因。因为没有方法阻止程序员进行复制,所以File类型的实现使用了一个嵌入的指针,指向一个未公开的类型。本章后面会继续探讨内嵌类型。正是这层额外的内嵌类型阻止了复制。不是所有的结构类型都需要或者应该实现类似的额外保护。程序员需要能识别出每个类型的本质,并使用这个本质来决定如何组织类型。

让我们看一下Open函数的实现,如代码清单5-32所示。

代码清单5-32 golang.org/src/os/file.go:第238行到第240行

238 func Open(name string) (file *File, err error) {
239   return OpenFile(name, O_RDONLY, 0)
240 }

代码清单5-32展示了Open函数的实现,调用者得到的是一个指向File类型值的指针。Open创建了File类型的值,并返回指向这个值的指针。如果一个创建用的工厂函数返回了一个指针,就表示这个被返回的值的本质是非原始的。

即便函数或者方法没有直接改变非原始的值的状态,依旧应该使用共享的方式传递,如代码清单5-33所示。

代码清单5-33 golang.org/src/os/file.go:第224行到第232行

224 func (f *File) Chdir() error {
225   if f == nil {
226     return ErrInvalid
227   }
228   if e := syscall.Fchdir(f.fd); e != nil {
229     return &PathError{"chdir", f.name, e}
230   }
231   return nil
232 }

代码清单5-33中的Chdir方法展示了,即使没有修改接收者的值,依然是用指针接收者来声明的。因为File类型的值具备非原始的本质,所以总是应该被共享,而不是被复制。

是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质。这条规则的一个例外是,需要让类型值符合某个接口的时候,即便类型的本质是非原始本质的,也可以选择使用值接收者声明方法。这样做完全符合接口值调用方法的机制。5.4节会讲解什么是接口值,以及使用接口值调用方法的机制。

5.4 接口

多态是指代码可以根据类型的具体实现采取不同行为的能力。如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值。标准库里有很好的例子,如io包里实现的流式处理接口。io包提供了一组构造得非常好的接口和函数,来让代码轻松支持流式数据处理。只要实现两个接口,就能利用整个io包背后的所有强大能力。

不过,我们的程序在声明和实现接口时会涉及很多细节。即便实现的是已有接口,也需要了解这些接口是如何工作的。在探究接口如何工作以及实现的细节之前,我们先来看一下使用标准库里的接口的例子。

5.4.1 标准库

我们先来看一个示例程序,这个程序实现了流行程序curl的功能,如代码清单5-34所示。

代码清单5-34 listing34.go

01 // 这个示例程序展示如何使用io.Reader和io.Writer接口
02 // 写一个简单版本的curl程序
03 package main
04
05 import (
06   "fmt"
07   "io"
08   "net/http"
09   "os"
10 )
11
12 // init在main函数之前调用
13 func init() {
14   if len(os.Args) != 2 {
15     fmt.Println("Usage: ./example2 <url>")
16     os.Exit(-1)
17   }
18 }
19
20 // main是应用程序的入口
21 func main() {
22   // 从Web服务器得到响应
23   r, err := http.Get(os.Args[1])
24   if err != nil {
25     fmt.Println(err)
26     return
27   }
28
29   // 从Body复制到Stdout
30   io.Copy(os.Stdout, r.Body)
31   if err := r.Body.Close(); err != nil {
32     fmt.Println(err)
33   }
34 }

代码清单5-34展示了接口的能力以及在标准库里的应用。只用了几行代码我们就通过两个函数以及配套的接口,完成了curl程序。在第23行,调用了http包的Get函数。在与服务器成功通信后,http.Get函数会返回一个http.Response类型的指针。http.Response类型包含一个名为Body的字段,这个字段是一个io.ReadCloser接口类型的值。

在第30行,Body字段作为第二个参数传给io.Copy函数。io.Copy函数的第二个参数,接受一个io.Reader接口类型的值,这个值表示数据流入的源。Body字段实现了io.Reader接口,因此我们可以将Body字段传入io.Copy,使用Web服务器的返回内容作为源。

io.Copy的第一个参数是复制到的目标,这个参数必须是一个实现了io.Writer接口的值。对于这个目标,我们传入了os包里的一个特殊值Stdout。这个接口值表示标准输出设备,并且已经实现了io.Writer接口。当我们将BodyStdout这两个值传给io.Copy函数后,这个函数会把服务器的数据分成小段,源源不断地传给终端窗口,直到最后一个片段读取并写入终端,io.Copy函数才返回。

io.Copy函数可以以这种工作流的方式处理很多标准库里已有的类型,如代码清单 5-35所示。

代码清单5-35 listing35.go

01 // 这个示例程序展示bytes.Buffer也可以
02 // 用于io.Copy函数
03 package main
04
05 import (
06   "bytes"
07   "fmt"
08   "io"
09   "os"
10 )
11
12 // main是应用程序的入口
13 func main() {
14   var b bytes.Buffer
15
16   // 将字符串写入Buffer
17   b.Write([]byte("Hello"))
18
19   // 使用Fprintf将字符串拼接到Buffer
20   fmt.Fprintf(&b, "World!")
21
22   // 将Buffer的内容写到Stdout
23   io.Copy(os.Stdout, &b)
24 }

代码清单5-35展示了一个程序,这个程序使用接口来拼接字符串,并将数据以流的方式输出到标准输出设备。在第14行,创建了一个bytes包里的Buffer类型的变量b,用于缓冲数据。之后在第17行使用Write方法将字符串Hello写入这个缓冲区b。第20行,调用fmt包里的Fprintf函数,将第二个字符串追加到缓冲区b里。

fmt.Fprintf函数接受一个io.Writer类型的接口值作为其第一个参数。由于bytes.Buffer类型的指针实现了io.Writer接口,所以可以将缓存b传入fmt.Fprintf函数,并执行追加操作。最后,在第23行,再次使用io.Copy函数,将字符写到终端窗口。由于bytes.Buffer类型的指针也实现了io.Reader接口,io.Copy函数可以用于在终端窗口显示缓冲区b的内容。

希望这两个小程序展示出接口的好处,以及标准库内部是如何使用接口的。下一步,让我们看一下实现接口的细节。

5.4.2 实现

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

对接口值方法的调用会执行接口值里存储的用户定义的类型的值对应的方法。因为任何用户定义的类型都可以实现任何接口,所以对接口值方法的调用自然就是一种多态。在这个关系里,用户定义的类型通常叫作实体类型,原因是如果离开内部存储的用户定义的类型的值的实现,接口值并没有具体的行为。

并不是所有值都完全等同,用户定义的类型的值或者指针要满足接口的实现,需要遵守一些规则。这些规则在5.4.3节介绍方法集时有详细说明。探寻方法集的细节之前,了解接口类型值大概的形式以及用户定义的类型的值是如何存入接口的,会有很多帮助。

图5-1展示了在user类型值赋值后接口变量的值的内部布局。接口值是一个两个字长度的数据结构,第一个字包含一个指向内部表的指针。这个内部表叫作iTable,包含了所存储的值的类型信息。iTable包含了已存储的值的类型信息以及与这个值相关联的一组方法。第二个字是一个指向所存储值的指针。将类型信息和指针组合在一起,就将这两个值组成了一种特殊的关系。

图5-1 实体值赋值后接口值的简图

图5-2展示了一个指针赋值给接口之后发生的变化。在这种情况里,类型信息会存储一个指向保存的类型的指针,而接口值第二个字依旧保存指向实体值的指针。

图5-2 实体指针赋值后接口值的简图

5.4.3 方法集

方法集定义了接口的接受规则。看一下代码清单 5-36 所示的代码,有助于理解方法集在接口中的重要角色。

代码清单5-36 listing36.go

01 // 这个示例程序展示Go语言里如何使用接口
02 package main
03
04 import (
05   "fmt"
06 )
07
08 // notifier是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11   notify()
12 }
13
14 // user在程序里定义一个用户类型
15 type user struct {
16   name string
17   email string
18 }
19
20 // notify是使用指针接收者实现的方法
21 func (u *user) notify() {
22   fmt.Printf("Sending user email to %s<%s>\n",
23     u.name,
24     u.email)
25 }
26
27 // main是应用程序的入口
28 func main() {
29   // 创建一个user类型的值,并发送通知
30   u := user{"Bill", "bill@email.com"}
31
32   sendNotification(u)
33
34   // ./listing36.go:32: 不能将u(类型是user)作为
35   //            sendNotification的参数类型notifier:
36   //  user类型并没有实现notifier
37   //                (notify方法使用指针接收者声明)
38 }
39
40 // sendNotification接受一个实现了notifier接口的值
41 // 并发送通知
42 func sendNotification(n notifier) {
43   n.notify()
44 }

代码清单5-36中的程序虽然看起来没问题,但实际上却无法通过编译。在第10行中,声明了一个名为notifier的接口,包含一个名为notify的方法。第15行中,声明了名为user的实体类型,并通过第21行中的方法声明实现了notifier接口。这个方法是使用user类型的指针接收者实现的。

代码清单5-37 listing36.go:第40行到第44行

40 // sendNotification接受一个实现了notifier接口的值
41 // 并发送通知
42 func sendNotification(n notifier) {
43   n.notify()
44 }

在代码清单5-37的第42行,声明了一个名为sendNotification的函数。这个函数接收一个notifier接口类型的值。之后,使用这个接口值来调用notify方法。任何一个实现了notifier接口的值都可以传入sendNotification函数。现在让我们来看一下main函数,如代码清单5-38所示。

代码清单5-38 listing36.go:第28行到第38行

28 func main() {
29   // 创建一个user类型的值,并发送通知
30   u := user{"Bill", "bill@email.com"}
31
32   sendNotification(u)
33
34   // ./listing36.go:32: 不能将u(类型是user)作为
35   //            sendNotification的参数类型notifier:
36   //  user类型并没有实现notifier
37   //                (notify方法使用指针接收者声明)
38 }

main函数里,代码清单5-38的第30行,创建了一个user实体类型的值,并将其赋值给变量u。之后在第32行将u的值传入sendNotification函数。不过,调用sendNotification的结果是产生了一个编译错误,如代码清单5-39所示。

代码清单5-39 将user类型的值存入接口值时产生的编译错误

./listing36.go:32: 不能将u(类型是user)作为sendNotification的参数类型notifier:
 user类型并没有实现notifier(notify方法使用指针接收者声明)

既然user类型已经在第21行实现了notify方法,为什么这里还是产生了编译错误呢?让我们再来看一下那段代码,如代码清单5-40所示。

代码清单5-40 listing36.go:第08行到第12行,第21行到第25行

08 // notifier是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11   notify()
12 }

21 func (u *user) notify() {
22   fmt.Printf("Sending user email to %s<%s>\n",
23     u.name,
24     u.email)
25 }

代码清单5-40展示了接口是如何实现的,而编译器告诉我们user类型的值并没有实现这个接口。如果仔细看一下编译器输出的消息,其实编译器已经说明了原因,如代码清单 5-41所示。

代码清单5-41 进一步查看编译器错误

(notify method has pointer receiver)

要了解用指针接收者来实现接口时为什么user类型的值无法实现该接口,需要先了解方法集。方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。

让我们先解释一下Go语言规范里定义的方法集的规则,如代码清单5-42所示。

代码清单5-42 规范里描述的方法集

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

代码清单5-42展示了规范里对方法集的描述。描述中说到,T类型的值的方法集只包含值接收者声明的方法。而指向T类型的指针的方法集既包含值接收者声明的方法,也包含指针接收者声明的方法。从值的角度看这些规则,会显得很复杂。让我们从接收者的角度来看一下这些规则,如代码清单5-43所示。

代码清单5-43 从接收者类型的角度来看方法集

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

代码清单5-43展示了同样的规则,只不过换成了接收者的视角。这个规则说,如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。现在再看一下代码清单5-36所示的代码,就能理解出现编译错误的原因了,如代码清单5-44所示。

代码清单5-44 listing36.go:第28行到第38行

28 func main() {
29   // 使用user类型创建一个值,并发送通知
30   u := user{"Bill", "bill@email.com"}
31
32   sendNotification(u)
33
34   // ./listing36.go:32: 不能将u(类型是user)作为
35   //            sendNotification的参数类型notifier:
36   //  user类型并没有实现notifier
37   //                (notify方法使用指针接收者声明)
38 }

我们使用指针接收者实现了接口,但是试图将user类型的值传给sendNotification方法。代码清单5-44的第30行和第32行清晰地展示了这个问题。但是,如果传递的是user值的地址,整个程序就能通过编译,并且能够工作了,如代码清单5-45所示。

代码清单5-45 listing36.go:第28行到第35行

28 func main() {
29   // 使用user类型创建一个值,并发送通知
30   u := user{"Bill", "bill@email.com"}
31
32   sendNotification(&u)
33
34   // 传入地址,不再有错误
35 }

在代码清单5-45里,这个程序终于可以编译并且运行。因为使用指针接收者实现的接口,只有user类型的指针可以传给sendNotification函数。

现在的问题是,为什么会有这种限制?事实上,编译器并不是总能自动获得一个值的地址,如代码清单5-46所示。

代码清单5-46 listing46.go

01 // 这个示例程序展示不是总能
02 // 获取值的地址
03 package main
04
05 import "fmt"
06
07 // duration是一个基于int类型的类型
08 type duration int
09
10 // 使用更可读的方式格式化duration值
11 func (d *duration) pretty() string {
12   return fmt.Sprintf("Duration: %d", *d)
13 }
14
15 // main是应用程序的入口
16 func main() {
17   duration(42).pretty()
18
19   // ./listing46.go:17: 不能通过指针调用duration(42)的方法
20   // ./listing46.go:17: 不能获取duration(42)的地址
21 }

代码清单5-46所示的代码试图获取duration类型的值的地址,但是获取不到。这展示了不能总是获得值的地址的一种情况。让我们再看一下方法集的规则,如代码清单5-47所示。

代码清单5-47 再看一下方法集的规则

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

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

因为不是总能获取一个值的地址,所以值的方法集只包括了使用值接收者实现的方法。

5.4.4 多态

现在了解了接口和方法集背后的机制,最后来看一个展示接口的多态行为的例子,如代码清单5-48所示。

代码清单5-48 listing48.go

01 // 这个示例程序使用接口展示多态行为
02 package main
03
04 import (
05   "fmt"
06 )
07
08 // notifier是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11   notify()
12 }
13
14 // user在程序里定义一个用户类型
15 type user struct {
16   name string
17   email string
18 }
19
20 // notify使用指针接收者实现了notifier接口
21 func (u *user) notify() {
22   fmt.Printf("Sending user email to %s<%s>\n",
23     u.name,
24     u.email)
25 }
26
27 // admin定义了程序里的管理员
28 type admin struct {
29   name string
30   email string
31 }
32
33 // notify使用指针接收者实现了notifier接口
34 func (a *admin) notify() {
35   fmt.Printf("Sending admin email to %s<%s>\n",
36     a.name,
37     a.email)
38 }
39
40 // main是应用程序的入口
41 func main() {
42   // 创建一个user值并传给sendNotification
43   bill := user{"Bill", "bill@email.com"}
44   sendNotification(&bill)
45
46   // 创建一个admin值并传给sendNotification
47   lisa := admin{"Lisa", "lisa@email.com"}
48   sendNotification(&lisa)
49 }
50
51 // sendNotification接受一个实现了notifier接口的值
52 // 并发送通知
53 func sendNotification(n notifier) {
54   n.notify()
55 }

在代码清单5-48中,我们有了一个展示接口的多态行为的例子。在第10行,我们声明了和之前代码清单中一样的notifier接口。之后第15行到第25行,我们声明了一个名为user的结构,并使用指针接收者实现了notifier接口。在第28行到第38行,我们声明了一个名为admin的结构,用同样的形式实现了notifier接口。现在,有两个实体类型实现了notifier接口。

在第53行中,我们再次声明了多态函数sendNotification,这个函数接受一个实现了notifier接口的值作为参数。既然任意一个实体类型都能实现该接口,那么这个函数可以针对任意实体类型的值来执行notifier方法。因此,这个函数就能提供多态的行为,如代码清单5-49所示。

代码清单5-49 listing48.go:第40行到第49行

40 // main是应用程序的入口
41 func main() {
42   // 创建一个user值并传给sendNotification
43   bill := user{"Bill", "bill@email.com"}
44   sendNotification(&bill)
45
46   // 创建一个admin值并传给sendNotification
47   lisa := admin{"Lisa", "lisa@email.com"}
48   sendNotification(&lisa)
49 }

最后,可以在代码清单5-49中看到这种多态的行为。main函数的第43行创建了一个user类型的值,并在第44行将该值的地址传给了sendNotification函数。这最终会导致执行user类型声明的notify方法。之后,在第47行和第48行,我们对admin类型的值做了同样的事情。最终,因为sendNotification接受notifier类型的接口值,所以这个函数可以同时执行useradmin实现的行为。

5.5 嵌入类型

Go语言允许用户扩展或者修改已有类型的行为。这个功能对代码复用很重要,在修改已有类型以符合新类型的时候也很重要。这个功能是通过嵌入类型(type embedding)完成的。嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。

通过嵌入类型,与内部类型相关的标识符会提升到外部类型上。这些被提升的标识符就像直接声明在外部类型里的标识符一样,也是外部类型的一部分。这样外部类型就组合了内部类型包含的所有属性,并且可以添加新的字段和方法。外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法。这就是扩展或者修改已有类型的方法。

让我们通过一个示例程序来演示嵌入类型的基本用法,如代码清单5-50所示。

代码清单5-50 listing50.go

01 // 这个示例程序展示如何将一个类型嵌入另一个类型,以及
02 // 内部类型和外部类型之间的关系
03 package main
04
05 import (
06   "fmt"
07 )
08
09 // user在程序里定义一个用户类型
10 type user struct {
11   name string
12   email string
13 }
14
15 // notify实现了一个可以通过user类型值的指针
16 // 调用的方法
17 func (u *user) notify() {
18   fmt.Printf("Sending user email to %s<%s>\n",
19   u.name,
20   u.email)
21 }
22
23 // admin代表一个拥有权限的管理员用户
24 type admin struct {
25   user // 嵌入类型
26   level string
27 }
28
29 // main是应用程序的入口
30 func main() {
31   // 创建一个admin用户
32   ad := admin{
33     user: user{
34       name: "john smith",
35       email: "john@yahoo.com",
36     },
37     level: "super",
38   }
39
40   // 我们可以直接访问内部类型的方法
41   ad.user.notify()
42
43   // 内部类型的方法也被提升到外部类型
44   ad.notify()
45 }

在代码清单5-50中,我们的程序演示了如何嵌入一个类型,并访问嵌入类型的标识符。我们从第10行和第24行中的两个结构类型的声明开始,如代码清单5-51所示。

代码清单5-51 listing50.go:第09行到第13行,第23行到第27行

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

23 // admin代表一个拥有权限的管理员用户
24 type admin struct {
25   user // 嵌入类型
26   level string
27 }

在代码清单5-51的第10行,我们声明了一个名为user的结构类型。在第24行,我们声明了另一个名为admin的结构类型。在声明admin类型的第25行,我们将user类型嵌入admin类型里。要嵌入一个类型,只需要声明这个类型的名字就可以了。在第26行,我们声明了一个名为level的字段。注意声明字段和嵌入类型在语法上的不同。

一旦我们将user类型嵌入admin,我们就可以说user是外部类型admin的内部类型。有了内部类型和外部类型这两个概念,就能更容易地理解这两种类型之间的关系。

代码清单5-52展示了使用user类型的指针接收者声明名为notify的方法。这个方法只是显示一行友好的信息,表示将邮件发给了特定的用户以及邮件地址。

代码清单5-52 listing50.go:第15行到第21行

15 // notify实现了一个可以通过user类型值的指针
16 // 调用的方法
17 func (u *user) notify() {
18   fmt.Printf("Sending user email to %s<%s>\n",
19   u.name,
20   u.email)
21 }

现在,让我们来看一下main函数,如代码清单5-53所示。

代码清单5-53 listing50.go:第30行到第45行

30 func main() {
31   // 创建一个admin用户
32   ad := admin{
33     user: user{
34       name: "john smith",
35       email: "john@yahoo.com",
36     },
37     level: "super",
38   }
39
40   // 我们可以直接访问内部类型的方法
41   ad.user.notify()
42
43   // 内部类型的方法也被提升到外部类型
44   ad.notify()
45 }

代码清单5-53中的main函数展示了嵌入类型背后的机制。在第32行,创建了一个admin类型的值。内部类型的初始化是用结构字面量完成的。通过内部类型的名字可以访问内部类型,如代码清单5-54所示。对外部类型来说,内部类型总是存在的。这就意味着,虽然没有指定内部类型对应的字段名,还是可以使用内部类型的类型名,来访问到内部类型的值。

代码清单5-54 listing50.go:第40行到第41行

40   // 我们可以直接访问内部类型的方法
41   ad.user.notify()

在代码清单5-54中第41行,可以看到对notify方法的调用。这个调用是通过直接访问内部类型user来完成的。这展示了内部类型是如何存在于外部类型内,并且总是可访问的。不过,借助内部类型提升,notify方法也可以直接通过ad变量来访问,如代码清单5-55所示。

代码清单5-55 listing50.go:第43行到第45行

43   // 内部类型的方法也被提升到外部类型
44   ad.notify()
45 }

代码清单5-55的第44行中展示了直接通过外部类型的变量来调用notify方法。由于内部类型的标识符提升到了外部类型,我们可以直接通过外部类型的值来访问内部类型的标识符。让我们修改一下这个例子,加入一个接口,如代码清单5-56所示。

代码清单5-56 listing56.go

01 // 这个示例程序展示如何将嵌入类型应用于接口
02 package main
03
04 import (
05   "fmt"
06 )
07
08 // notifier是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11   notify()
12 }
13
14 // user在程序里定义一个用户类型
15 type user struct {
16   name string
17   email string
18 }
19
20 // 通过user类型值的指针
21 // 调用的方法
22 func (u *user) notify() {
23   fmt.Printf("Sending user email to %s<%s>\n",
24   u.name,
25   u.email)
26 }
27
28 // admin代表一个拥有权限的管理员用户
29 type admin struct {
30   user
31   level string
32 }
33
34 // main是应用程序的入口
35 func main() {
36   // 创建一个admin用户
37   ad := admin{
38     user: user{
39       name: "john smith",
40       email: "john@yahoo.com",
41     },
42     level: "super",
43   }
44
45   // 给admin用户发送一个通知
46   // 用于实现接口的内部类型的方法,被提升到
47   // 外部类型
48   sendNotification(&ad)
49 }
50
51 // sendNotification接受一个实现了notifier接口的值
52 // 并发送通知
53 func sendNotification(n notifier) {
54   n.notify()
55 }

代码清单5-56所示的示例程序的大部分和之前的程序相同,只有一些小变化,如代码清单5-57所示。

代码清单5-57 第08行到第12行,第51行到第55行

08 // notifier是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11   notify()
12 }

51 // sendNotification接受一个实现了notifier接口的值
52 // 并发送通知
53 func sendNotification(n notifier) {
54   n.notify()
55 }

在代码清单5-57的第08行,声明了一个notifier接口。之后在第53行,有一个sendNotification函数,接受notifier类型的接口的值。从代码可以知道,user类型之前声明了名为notify``的方法,该方法使用指针接收者实现了notifier接口。之后,让我们看一下main函数的改动,如代码清单5-58所示。

代码清单5-58 listing56.go:第35行到第49行

35 func main() {
36   // 创建一个admin用户
37   ad := admin{
38     user: user{
39       name: "john smith",
40       email: "john@yahoo.com",
41     },
42     level: "super",
43   }
44
45   // 给admin用户发送一个通知
46   // 用于实现接口的内部类型的方法,被提升到
47   // 外部类型
48   sendNotification(&ad)
49 }

这里才是事情变得有趣的地方。在代码清单5-58的第37行,我们创建了一个名为ad的变量,其类型是外部类型admin。这个类型内部嵌入了user类型。之后第48行,我们将这个外部类型变量的地址传给sendNotification函数。编译器认为这个指针实现了notifier接口,并接受了这个值的传递。不过如果看一下整个示例程序,就会发现admin类型并没有实现这个接口。

由于内部类型的提升,内部类型实现的接口会自动提升到外部类型。这意味着由于内部类型的实现,外部类型也同样实现了这个接口。运行这个示例程序,会得到代码清单5-59所示的输出。

代码清单5-59 listing56.go的输出

20 // 通过user类型值的指针
21 // 调用的方法
22 func (u *user) notify() {
23   fmt.Printf("Sending user email to %s<%s>\n",
24   u.name,
25   u.email)
26 }
 
Output:
Sending user email to john smith<john@yahoo.com>

可以在代码清单5-59中看到内部类型的实现被调用。

如果外部类型并不需要使用内部类型的实现,而想使用自己的一套实现,该怎么办?让我们看另一个示例程序是如何解决这个问题的,如代码清单5-60所示。

代码清单5-60 listing60.go

01 // 这个示例程序展示当内部类型和外部类型要
02 // 实现同一个接口时的做法
03 package main
04
05 import (
06   "fmt"
07 )
08
08 // notifier是一个定义了
09 // 通知类行为的接口
11 type notifier interface {
12   notify()
13 }
14
15 // user在程序里定义一个用户类型
16 type user struct {
17   name string
18   email string
19 }
20
21 // 通过user类型值的指针
22 // 调用的方法
23 func (u *user) notify() {
24   fmt.Printf("Sending user email to %s<%s>\n",
25     u.name,
26     u.email)
27 }
28
29 // admin代表一个拥有权限的管理员用户
30 type admin struct {
31   user
32   level string
33 }
34
35 // 通过admin类型值的指针
36 // 调用的方法
37 func (a *admin) notify() {
38   fmt.Printf("Sending admin email to %s<%s>\n",
39     a.name,
40     a.email)
41 }
42
43 // main是应用程序的入口
44 func main() {
45   // 创建一个admin用户
46   ad := admin{
47     user: user{
48       name: "john smith",
49       email: "john@yahoo.com",
50     },
51     level: "super",
52   }
53
54   // 给admin用户发送一个通知
55   // 接口的嵌入的内部类型实现并没有提升到
56   // 外部类型
57   sendNotification(&ad)
58
59   // 我们可以直接访问内部类型的方法
60   ad.user.notify()
61
62   // 内部类型的方法没有被提升
63   ad.notify()
64 }
65
66 // sendNotification接受一个实现了notifier接口的值
67 // 并发送通知
68 func sendNotification(n notifier) {
69   n.notify()
70 }

代码清单5-60所示的示例程序的大部分和之前的程序相同,只有一些小变化,如代码清单5-61所示。

代码清单5-61 listing60.go:第35行到第41行

35 // 通过admin类型值的指针
36 // 调用的方法
37 func (a *admin) notify() {
38   fmt.Printf("Sending admin email to %s<%s>\n",
39     a.name,
40     a.email)
41 }

这个示例程序为admin类型增加了notifier接口的实现。当admin类型的实现被调用时,会显示"Sending admin email"。作为对比,user类型的实现被调用时,会显示"Sending user email"

main函数里也有一些变化,如代码清单5-62所示。

代码清单5-62 listing60.go:第43行到第64行

43 // main是应用程序的入口
44 func main() {
45   // 创建一个admin用户
46   ad := admin{
47     user: user{
48       name: "john smith",
49       email: "john@yahoo.com",
50     },
51     level: "super",
52   }
53
54   // 给admin用户发送一个通知
55   // 接口的嵌入的内部类型实现并没有提升到
56   // 外部类型
57   sendNotification(&ad)
58
59   // 我们可以直接访问内部类型的方法
60   ad.user.notify()
61
62   // 内部类型的方法没有被提升
63   ad.notify()
64 }

代码清单5-62的第46行,我们再次创建了外部类型的变量ad。在第57行,将ad变量的地址传给sendNotification函数,这个指针实现了接口所需要的方法集。在第60行,代码直接访问user内部类型,并调用notify方法。最后,在第63行,使用外部类型变量ad来调用notify方法。当查看这个示例程序的输出(如代码清单5-63所示)时,就会看到区别。

代码清单5-63 listing60.go的输出

Sending admin email to john smith<john@yahoo.com>
Sending user email to john smith<john@yahoo.com>
Sending admin email to john smith<john@yahoo.com>

这次我们看到了admin类型是如何实现notifier接口的,以及如何由sendNotification函数以及直接使用外部类型的变量ad来执行admin类型实现的方法。这表明,如果外部类型实现了notify方法,内部类型的实现就不会被提升。不过内部类型的值一直存在,因此还可以通过直接访问内部类型的值,来调用没有被提升的内部类型实现的方法。

5.6 公开或未公开的标识符

要想设计出好的 API,需要使用某种规则来控制声明后的标识符的可见性。Go语言支持从包里公开或者隐藏标识符。通过这个功能,让用户能按照自己的规则控制标识符的可见性。在第3章讨论包的时候,谈到了如何从一个包引入标识符到另一个包。有时候,你可能不希望公开包里的某个类型、函数或者方法这样的标识符。在这种情况,需要一种方法,将这些标识符声明为包外不可见,这时需要将这些标识符声明为未公开的。

让我们用一个示例程序来演示如何隐藏包里未公开的标识符,如代码清单5-64所示。

代码清单5-64 listing64/

counters/counters.go
-----------------------------------------------------------------------
01 // counters包提供告警计数器的功能
02 package counters
03
04 // alertCounter是一个未公开的类型
05 // 这个类型用于保存告警计数
06 type alertCounter int

listing64.go
-----------------------------------------------------------------------
01 // 这个示例程序展示无法从另一个包里
02 // 访问未公开的标识符
03 package main
04
05 import (
06   "fmt"
07
08   "github.com/goinaction/code/chapter5/listing64/counters"
09 )
10
11 // main是应用程序的入口
12 func main() {
13   // 创建一个未公开的类型的变量
14   // 并将其初始化为10
15   counter := counters.alertCounter(10)
16
17   // ./listing64.go:15: 不能引用未公开的名字
18   //          counters.alertCounter
19   // ./listing64.go:15: 未定义:counters.alertCounter
20
21   fmt.Printf("Counter: %d\n", counter)
22 }

这个示例程序有两个代码文件。一个代码文件名字为counters.go,保存在counters包里;另一个代码文件名字为listing64.go,导入了counters包。让我们先从counters包里的代码开始,如代码清单5-65所示。

代码清单5-65 counters/counters.go

01 // counters包提供告警计数器的功能
02 package counters
03
04 // alertCounter是一个未公开的类型
05 // 这个类型用于保存告警计数
06 type alertCounter int

代码清单5-65展示了只属于counters包的代码。你可能会首先注意到第02行。直到现在,之前所有的示例程序都使用了package main,而这里用到的是package counters。当要写的代码属于某个包时,好的实践是使用与代码所在文件夹一样的名字作为包名。所有的Go工具都会利用这个习惯,所以最好遵守这个好的实践。

counters包里,我们在第06行声明了唯一一个名为alertCounter的标识符。这个标识符是一个使用int作为基础类型的类型。需要注意的是,这是一个未公开的标识符。

当一个标识符的名字以小写字母开头时,这个标识符就是未公开的,即包外的代码不可见。如果一个标识符以大写字母开头,这个标识符就是公开的,即被包外的代码可见。让我们看一下导入这个包的代码,如代码清单5-66所示。

代码清单5-66 listing64.go

01 // 这个示例程序展示无法从另一个包里
02 // 访问未公开的标识符
03 package main
04
05 import (
06   "fmt"
07
08   "github.com/goinaction/code/chapter5/listing64/counters"
09 )
10
11 // main是应用程序的入口
12 func main() {
13   // 创建一个未公开的类型的变量
14   // 并将其初始化为10
15   counter := counters.alertCounter(10)
16
17   // ./listing64.go:15: 不能引用未公开的名字
18   //                       counters.alertCounter
19   // ./listing64.go:15: 未定义:counters.alertCounter
20
21   fmt.Printf("Counter: %d\n", counter)
22 }

代码清单5-66中的listing64.go的代码在第03行声明了main包,之后在第08行导入了counters包。在这之后,我们跳到main函数里的第15行,如代码清单5-67所示。

代码清单5-67 listing64.go:第13到19行

13   // 创建一个未公开的类型的变量
14   // 并将其初始化为10
15   counter := counters.alertCounter(10)
16
17   // ./listing64.go:15: 不能引用未公开的名字
18   //                       counters.alertCounter
19   // ./listing64.go:15: 未定义:counters.alertCounter

在代码清单5-67的第15行,代码试图创建未公开的alertCounter类型的值。不过这段代码会造成第15行展示的编译错误,这个编译错误表明第15行的代码无法引用counters.alertCounter这个未公开的标识符。这个标识符是未定义的。

由于counters包里的alertCounter类型是使用小写字母声明的,所以这个标识符是未公开的,无法被listing64.go的代码访问。如果我们把这个类型改为用大写字母开头,那么就不会产生编译器错误。让我们看一下新的示例程序,如代码清单5-68所示,这个程序在counters包里实现了工厂函数。

代码清单5-68 listing68/

counters/counters.go
-----------------------------------------------------------------------
01 // counters包提供告警计数器的功能
02 package counters
03
04 // alertCounter是一个未公开的类型
05 // 这个类型用于保存告警计数
06 type alertCounter int
07
08 // New创建并返回一个未公开的
09 // alertCounter类型的值
10 func New(value int) alertCounter {
11   return alertCounter(value)
12 }

listing68.go
-----------------------------------------------------------------------
01 // 这个示例程序展示如何访问另一个包的未公开的
02 // 标识符的值
03 package main
04
05 import (
06   "fmt"
07
08   "github.com/goinaction/code/chapter5/listing68/counters"
09 )
10
11 // main是应用程序的入口
12 func main() {
13   // 使用counters包公开的New函数来创建
14   // 一个未公开的类型的变量
15   counter := counters.New(10)
16
17   fmt.Printf("Counter: %d\n", counter)
18 }

这个例子已经修改为使用工厂函数来创建一个未公开的alertCounter类型的值。让我们先看一下counters包的代码,如代码清单5-69所示。

代码清单5-69 counters/counters.go

01 // counters包提供告警计数器的功能
02 package counters
03
04 // alertCounter是一个未公开的类型
05 // 这个类型用于保存告警计数
06 type alertCounter int
07
08 // New创建并返回一个未公开的
09 // alertCounter类型的值
10 func New(value int) alertCounter {
11   return alertCounter(value)
12 }

代码清单5-69展示了我们对counters包的改动。alertCounter类型依旧是未公开的,不过现在在第10行增加了一个名为New的新函数。将工厂函数命名为New是Go语言的一个习惯。这个New函数做了些有意思的事情:它创建了一个未公开的类型的值,并将这个值返回给调用者。让我们看一下listing68.go的main函数,如代码清单5-70所示。

代码清单5-70 listing68.go

11 // main是应用程序的入口
12 func main() {
13   // 使用counters包公开的New函数来创建
14   // 一个未公开的类型的变量
15   counter := counters.New(10)
16
17   fmt.Printf("Counter: %d\n", counter)
18 }

在代码清单5-70的第15行,可以看到对counters包里New函数的调用。这个New函数返回的值被赋给一个名为counter的变量。这个程序可以编译并且运行,但为什么呢?New函数返回的是一个未公开的alertCounter类型的值,而main函数能够接受这个值并创建一个未公开的类型的变量。

要让这个行为可行,需要两个理由。第一,公开或者未公开的标识符,不是一个值。第二,短变量声明操作符,有能力捕获引用的类型,并创建一个未公开的类型的变量。永远不能显式创建一个未公开的类型的变量,不过短变量声明操作符可以这么做。

让我们看一个新例子,这个例子展示了这些可见的规则是如何影响到结构里的字段,如代码清单5-71所示。

代码清单5-71 listing71/

entities/entities.go
-----------------------------------------------------------------------
01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // User在程序里定义一个用户类型
06 type User struct {
07   Name string
08   email string
09 }

listing71.go
-----------------------------------------------------------------------
01 // 这个示例程序展示公开的结构类型中未公开的字段
02 // 无法直接访问
03 package main
04
05 import (
06   "fmt"
07
08   "github.com/goinaction/code/chapter5/listing71/entities"
09 )
10
11 // main是应用程序的入口
12 func main() {
13   // 创建entities包中的User类型的值
14   u := entities.User{
15     Name: "Bill",
16     email: "bill@email.com",
17   }
18
19   // ./example69.go:16: 结构字面量中结构entities.User
20   //          的字段’email’未知
21
22   fmt.Printf("User: %v\n", u)
23 }

代码清单5-71中的代码有一些微妙的变化。现在我们有一个名为entities的包,声明了名为User的结构类型,如代码清单5-72所示。

代码清单5-72 entities/entities.go

01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // User在程序里定义一个用户类型
06 type User struct {
07   Name string
08   email string
09 }

代码清单5-72的第06行中的User类型被声明为公开的类型。User类型里声明了两个字段,一个名为Name的公开的字段,一个名为email的未公开的字段。让我们看一下listing71.go的代码,如代码清单5-73所示。

代码清单5-73 listing71.go

01 // 这个示例程序展示公开的结构类型中未公开的字段
02 // 无法直接访问
03 package main
04
05 import (
06   "fmt"
07
08   "github.com/goinaction/code/chapter5/listing71/entities"
09 )
10
11 // main是程序的入口
12 func main() {
13   // 创建entities包中的User类型的值
14   u := entities.User{
15     Name: "Bill",
16     email: "bill@email.com",
17   }
18
19   // ./example69.go:16: 结构字面量中结构entities.User
20   //          的字段'email'未知
21
22   fmt.Printf("User: %v\n", u)
23 }

代码清单5-73的第08行导入了entities包。在第14行声明了entities包中的公开的类型User的名为u的变量,并对该字段做了初始化。不过这里有一个问题。第16行的代码试图初始化未公开的字段email,所以编译器抱怨这是个未知的字段。因为email这个标识符未公开,所以它不能在entities包外被访问。

让我们看最后一个例子,这个例子展示了公开和未公开的内嵌类型是如何工作的,如代码清单5-74所示。

代码清单5-74 listing74/

entities/entities.go
-----------------------------------------------------------------------
01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // user在程序里定义一个用户类型
06 type user struct {
07   Name string
08   Email string
09 }
10
11 // Admin在程序里定义了管理员
12 type Admin struct {
13   user  // 嵌入的类型是未公开的
14   Rights int
15 }


listing74.go
-----------------------------------------------------------------------
01 // 这个示例程序展示公开的结构类型中如何访问
02 // 未公开的内嵌类型的例子
03 package main
04
05 import (
06   "fmt"
07
08   "github.com/goinaction/code/chapter5/listing74/entities"
09 )
10
11 // main是应用程序的入口
12 func main() {
13   // 创建entities包中的Admin类型的值
14   a := entities.Admin{
15     Rights: 10,
16   }
17
18   // 设置未公开的内部类型的
19   // 公开字段的值
20   a.Name = "Bill"
21   a.Email = "bill@email.com"
22
23   fmt.Printf("User: %v\n", a)
24 }

现在,在代码清单5-74里,entities包包含两个结构类型,如代码清单5-75所示。

代码清单5-75 entities/entities.go

01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // user在程序里定义一个用户类型
06 type user struct {
07   Name string
08   Email string
09 }
10
11 // Admin在程序里定义了管理员
12 type Admin struct {
13   user  // 嵌入的类型未公开
14   Rights int
15 }

在代码清单5-75的第06行,声明了一个未公开的结构类型user。这个类型包括两个公开的字段NameEmail。在第12行,声明了一个公开的结构类型AdminAdmin有一个名为Rights的公开的字段,而且嵌入一个未公开的user类型。让我们看一下listing74.go的main函数,如代码清单5-76所示。

代码清单5-76 listing74.go:第11到24行

11 // main是应用程序的入口
12 func main() {
13   // 创建entities包中的Admin类型的值
14   a := entities.Admin{
15     Rights: 10,
16   }
17
18   // 设置未公开的内部类型的
19   // 公开字段的值
20   a.Name = "Bill"
21   a.Email = "bill@email.com"
22
23   fmt.Printf("User: %v\n", a)
24 }

让我们从代码清单5-76的第14行的main函数开始。这个函数创建了entities包中的Admin类型的值。由于内部类型user是未公开的,这段代码无法直接通过结构字面量的方式初始化该内部类型。不过,即便内部类型是未公开的,内部类型里声明的字段依旧是公开的。既然内部类型的标识符提升到了外部类型,这些公开的字段也可以通过外部类型的字段的值来访问。

因此,在第20行和第21行,来自未公开的内部类型的字段NameEmail可以通过外部类型的变量a被访问并被初始化。因为user类型是未公开的,所以这里没有直接访问内部类型。

5.7 小结