第十章修改 (#840)

Co-authored-by: Joe Chen <jc@unknwon.io>
This commit is contained in:
Haigang Zhou
2022-05-11 22:58:19 +08:00
committed by GitHub
parent 471d59af32
commit d3979794f4
9 changed files with 108 additions and 106 deletions

View File

@@ -1,10 +1,10 @@
# 10.0 结构struct与方法method # 10.0 结构 (struct) 与方法 (method)
Go 通过类型别名alias types和结构体的形式支持用户自定义类型,或者叫定制类型。一个带属性的结构体试图表示一个现实世界中的实体。结构体是复合类型composite types,当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。然后可以访问这些数据,就好像它是一个独立实体的一部分。结构体也是值类型,因此可以通过 **new** 函数来创建。 Go 通过类型别名 (alias types) 和结构体的形式支持用户自定义类型,或者叫定制类型。一个带属性的结构体试图表示一个现实世界中的实体。结构体是复合类型 (composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。然后可以访问这些数据,就好像它是一个独立实体的一部分。结构体也是值类型,因此可以通过 **new** 函数来创建。
组成结构体类型的那些数据称为 **字段fields**。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。 组成结构体类型的那些数据称为 **字段 (fields)**。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。
结构体的概念在软件工程上旧的术语叫 ADT抽象数据类型Abstract Data Type在一些老的编程语言中叫 **记录Record**,比如 Cobol在 C 家族的编程语言中它也存在,并且名字也是 **struct**,在面向对象的编程语言中,跟一个无方法的轻量级类一样。不过因为 Go 语言中没有类的概念,因此在 Go 中结构体有着更为重要的地位。 结构体的概念在软件工程上旧的术语叫 ADT抽象数据类型Abstract Data Type在一些老的编程语言中叫 **记录 (Record)**,比如 Cobol在 C 家族的编程语言中它也存在,并且名字也是 **struct**,在面向对象的编程语言中,跟一个无方法的轻量级类一样。不过因为 Go 语言中没有类的概念,因此在 Go 中结构体有着更为重要的地位。
## 链接 ## 链接

View File

@@ -12,9 +12,9 @@ type identifier struct {
`type T struct {a, b int}` 也是合法的语法,它更适用于简单的结构体。 `type T struct {a, b int}` 也是合法的语法,它更适用于简单的结构体。
结构体里的字段都有 **名字**,像 field1field2 等,如果字段在代码中从来也不会被用到,那么可以命名它为 **_** 结构体里的字段都有 **名字**,像 `field1``field2` 等,如果字段在代码中从来也不会被用到,那么可以命名它为 `_`
结构体的字段可以是任何类型,甚至是结构体本身(参考第 [10.5](10.5.md) 节),也可以是函数或者接口(参考第 11 章)。可以声明结构体类型的一个变量,然后像下面这样给它的字段赋值: 结构体的字段可以是任何类型,甚至是结构体本身(参考第 [10.5](10.5.md) 节),也可以是函数或者接口(参考[第 11 章](11.0.md))。可以声明结构体类型的一个变量,然后像下面这样给它的字段赋值:
```go ```go
var s T var s T
@@ -24,9 +24,9 @@ s.b = 8
数组可以看作是一种结构体类型,不过它使用下标而不是具名的字段。 数组可以看作是一种结构体类型,不过它使用下标而不是具名的字段。
**使用 new** **使用 `new()`**
使用 **new** 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:`var t *T = new(T)`,如果需要可以把这条语句放在不同的行(比如定义是包范围的,但是分配却没有必要在开始就做)。 使用 `new()` 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:`var t *T = new(T)`,如果需要可以把这条语句放在不同的行(比如定义是包范围的,但是分配却没有必要在开始就做)。
```go ```go
var t *T var t *T
@@ -35,7 +35,7 @@ t = new(T)
写这条语句的惯用方法是:`t := new(T)`,变量 `t` 是一个指向 `T` 的指针,此时结构体字段的值是它们所属类型的零值。 写这条语句的惯用方法是:`t := new(T)`,变量 `t` 是一个指向 `T` 的指针,此时结构体字段的值是它们所属类型的零值。
声明 `var t T` 也会给 `t` 分配内存,并零值化内存,但是这个时候 `t` 是类型 T 。在这两种方式中,`t` 通常被称做类型 T 的一个实例instance或对象object 声明 `var t T` 也会给 `t` 分配内存,并零值化内存,但是这个时候 `t` 是类型 `T` 。在这两种方式中,`t` 通常被称做类型 T 的一个实例 (instance) 或对象 (object)
示例 10.1 [structs_fields.go](examples/chapter_10/structs_fields.go) 给出了一个非常简单的例子: 示例 10.1 [structs_fields.go](examples/chapter_10/structs_fields.go) 给出了一个非常简单的例子:
@@ -69,13 +69,13 @@ func main() {
The string is: Chris The string is: Chris
&{10 15.5 Chris} &{10 15.5 Chris}
使用 `fmt.Println` 打印一个结构体的默认输出可以很好的显示它的内容,类似使用 **%v** 选项。 使用 `fmt.Println()` 打印一个结构体的默认输出可以很好的显示它的内容,类似使用 `%v` 选项。
就像在面向对象语言所作的那样,可以使用点号符给字段赋值:`structname.fieldname = value` 就像在面向对象语言所作的那样,可以使用点号符给字段赋值:`structname.fieldname = value`
同样的,使用点号符可以获取结构体字段的值:`structname.fieldname` 同样的,使用点号符可以获取结构体字段的值:`structname.fieldname`
在 Go 语言中这叫 **选择器selector**。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 **选择器符selector-notation** 来引用结构体的字段: 在 Go 语言中这叫 **选择器 (selector)**。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 **选择器符 (selector-notation)** 来引用结构体的字段:
```go ```go
type myStruct struct { i int } type myStruct struct { i int }
@@ -99,7 +99,7 @@ p.i
ms = struct1{10, 15.5, "Chris"} ms = struct1{10, 15.5, "Chris"}
``` ```
混合字面量语法composite literal syntax`&struct1{a, b, c}` 是一种简写,底层仍然会调用 `new ()`,这里值的顺序必须按照字段顺序来写。在下面的例子中能看到可以通过在值的前面放上字段名来初始化字段的方式。表达式 `new(Type)``&Type{}` 是等价的。 混合字面量语法 (composite literal syntax) `&struct1{a, b, c}` 是一种简写,底层仍然会调用 `new()`,这里值的顺序必须按照字段顺序来写。在下面的例子中能看到可以通过在值的前面放上字段名来初始化字段的方式。表达式 `new(Type)``&Type{}` 是等价的。
时间间隔(开始和结束时间以秒为单位)是使用结构体的一个典型例子: 时间间隔(开始和结束时间以秒为单位)是使用结构体的一个典型例子:
@@ -118,7 +118,7 @@ intr := Interval{end:5, start:1} (B)
intr := Interval{end:5} (C) intr := Interval{end:5} (C)
``` ```
A中,值必须以字段在结构体定义时的顺序给出,**&** 不是必须的。B显示了另一种方式,字段名加一个冒号放在值的前面,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉,就像C中那样。 (A) 中,值必须以字段在结构体定义时的顺序给出,`&` 不是必须的。(B) 显示了另一种方式,字段名加一个冒号放在值的前面,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉,就像 (C) 中那样。
结构体类型和字段的命名遵循可见性规则(第 [4.2](04.2.md) 节),一个导出的结构体类型中有些字段是导出的,另一些不是,这是可能的。 结构体类型和字段的命名遵循可见性规则(第 [4.2](04.2.md) 节),一个导出的结构体类型中有些字段是导出的,另一些不是,这是可能的。
@@ -128,7 +128,7 @@ intr := Interval{end:5} (C)
type Point struct { x, y int } type Point struct { x, y int }
``` ```
使用 new 初始化: 使用 `new()` 初始化:
![](images/10.1_fig10.1-1.jpg?raw=true) ![](images/10.1_fig10.1-1.jpg?raw=true)
@@ -136,9 +136,9 @@ type Point struct { x, y int }
![](images/10.1_fig10.1-2.jpg?raw=true) ![](images/10.1_fig10.1-2.jpg?raw=true)
类型 struct1 在定义它的包 pack1 中必须是唯一的,它的完全类型名是:`pack1.struct1` 类型 `struct1` 在定义它的包 `pack1` 中必须是唯一的,它的完全类型名是:`pack1.struct1`
下面的例子 [Listing 10.2—person.go](examples/chapter_10/person.go) 显示了一个结构体 Person一个方法方法有一个类型为 `*Person` 的参数(因此对象本身是可以被改变的),以及三种调用这个方法的不同方式: 下面的例子 [Listing 10.2—person.go](examples/chapter_10/person.go) 显示了一个结构体 `Person`,一个方法 `upPerson()`,方法有一个类型为 `*Person` 的参数(因此对象本身是可以被改变的),以及三种调用这个方法的不同方式:
```go ```go
package main package main
@@ -209,7 +209,7 @@ type Rect2 struct {Min, Max *Point }
![](images/10.1_fig10.3.jpg?raw=true) ![](images/10.1_fig10.3.jpg?raw=true)
这块的 `data` 字段用于存放有效数据(比如 float64`su` 指针指向后继节点。 这块的 `data` 字段用于存放有效数据(比如 `float64``su` 指针指向后继节点。
Go 代码: Go 代码:
@@ -220,7 +220,7 @@ type Node struct {
} }
``` ```
链表中的第一个元素叫 `head`,它指向第二个元素;最后一个元素叫 `tail`,它没有后继元素,所以它的 `su` 为 nil 值。当然真实的链接会有很多数据节点,并且链表可以动态增长或收缩。 链表中的第一个元素叫 `head`,它指向第二个元素;最后一个元素叫 `tail`,它没有后继元素,所以它的 `su``nil` 值。当然真实的链接会有很多数据节点,并且链表可以动态增长或收缩。
同样地可以定义一个双向链表,它有一个前趋节点 `pr` 和一个后继节点 `su` 同样地可以定义一个双向链表,它有一个前趋节点 `pr` 和一个后继节点 `su`
@@ -234,9 +234,9 @@ type Node struct {
二叉树: 二叉树:
![](images/10.1_fig10.4.jpg?raw=true) <img src="images/10.1_fig10.4.jpg?raw=true" style="zoom: 80%;" />
二叉树中每个节点最多能链接至两个节点:左节点le和右节点ri,这两个节点本身又可以有左右节点,依次类推。树的顶层节点叫根节点**root**,底层没有子节点的节点叫叶子节点**leaves**,叶子节点的 `le``ri` 指针为 nil 值。在 Go 中可以如下定义二叉树: 二叉树中每个节点最多能链接至两个节点:左节点 (`le`) 和右节点 (`ri`),这两个节点本身又可以有左右节点,依次类推。树的顶层节点叫根节点 (**root**),底层没有子节点的节点叫叶子节点 (**leaves**),叶子节点的 `le``ri` 指针为 `nil` 值。在 Go 中可以如下定义二叉树:
```go ```go
type Tree struct { type Tree struct {
@@ -248,7 +248,7 @@ type Tree struct {
**结构体转换** **结构体转换**
Go 中的类型转换遵循严格的规则。当为结构体定义了一个 alias 类型时,此结构体类型和它的 alias 类型都有相同的底层类型,它们可以如示例 10.3 那样互相转换,同时需要注意其中非法赋值或转换引起的编译错误。 Go 中的类型转换遵循严格的规则。当为结构体定义了一个 `alias` 类型时,此结构体类型和它的 `alias` 类型都有相同的底层类型,它们可以如示例 10.3 那样互相转换,同时需要注意其中非法赋值或转换引起的编译错误。
示例 10.3 示例 10.3
@@ -278,30 +278,29 @@ func main() {
{5} {5} {5} {5} {5} {5}
**练习 10.1** vcard.go **练习 10.1** [vcard.go](exercises\chapter_10\vcard.go)
定义结构体 Address 和 VCard后者包含一个人的名字、地址编号、出生日期和图像试着选择正确的数据类型。构建一个自己的 vcard 并打印它的内容。 定义结构体 `Address``VCard`,后者包含一个人的名字、地址编号、出生日期和图像,试着选择正确的数据类型。构建一个自己的 `vcard` 并打印它的内容。
提示: 提示:
VCard 必须包含住址,它应该以值类型还是以指针类型放在 VCard 中呢? VCard 必须包含住址,它应该以值类型还是以指针类型放在 VCard 中呢?
第二种会好点,因为它占用内存少。包含一个名字和两个指向地址的指针的 Address 结构体可以使用 %v 打印: 第二种会好点,因为它占用内存少。包含一个名字和两个指向地址的指针的 Address 结构体可以使用 %v 打印:
{Kersschot 0x126d2b80 0x126d2be0} {Kersschot 0x126d2b80 0x126d2be0}
**练习 10.2** personex1.go **练习 10.2** [personex1.go](exercises\chapter_10\personex1.go)
修改 personex1.go使它的参数 upPerson 不是一个指针,解释下二者的区别。 修改 personex1.go使它的参数 `upPerson` 不是一个指针,解释下二者的区别。
**练习 10.3** point.go **练习 10.3** [point.go](exercises\chapter_10\point.go)
使用坐标 X、Y 定义一个二维 Point 结构体。同样地,对一个三维点使用它的极坐标定义一个 Polar 结构体。实现一个 `Abs()` 方法来计算一个 Point 表示的向量的长度,实现一个 `Scale` 方法,它将点的坐标乘以一个尺度因子(提示:使用 `math` 包里的 `Sqrt` 函数)function Scale that multiplies the coordinates of a point with a scale 使用坐标 `X``Y` 定义一个二维 `Point` 结构体。同样地,对一个三维点使用它的极坐标定义一个 `Polar` 结构体。实现一个 `Abs()` 方法来计算一个 `Point` 表示的向量的长度,实现一个 `Scale()` 方法,它将点的坐标乘以一个尺度因子(提示:使用 `math` 包里的 `Sqrt()` 函数)(function Scale that multiplies the coordinates of a point with a scale factor)。
factor
**练习 10.4** rectangle.go **练习 10.4** [rectangle.go](exercises\chapter_10\rectangle.go)
定义一个 Rectangle 结构体,它的长和宽是 int 类型,并定义方法 `Area()``Perimeter()`,然后进行测试。 定义一个 `Rectangle` 结构体,它的长和宽是 `int` 类型,并定义方法 `Area()``Perimeter()`,然后进行测试。
## 链接 ## 链接
- [目录](directory.md) - [目录](directory.md)
- 上一节:[结构struct与方法method](10.0.md) - 上一节:[结构 (struct) 与方法 (method)](10.0.md)
- 下一节:[使用工厂方法创建结构体](10.2.md) - 下一节:[使用工厂方法创建结构体](10.2.md)

View File

@@ -2,7 +2,7 @@
## 10.2.1 结构体工厂 ## 10.2.1 结构体工厂
Go 语言不支持面向对象编程语言中那样的构造子方法,但是可以很容易的在 Go 中实现 “构造子工厂”方法。为了方便通常会为类型定义一个工厂,按惯例,工厂的名字以 new 或 New 开头。假设定义了如下的 File 结构体类型: Go 语言不支持面向对象编程语言中那样的构造子方法,但是可以很容易的在 Go 中实现 “构造子工厂”方法。为了方便通常会为类型定义一个工厂,按惯例,工厂的名字以 `new...``New...` 开头。假设定义了如下的 `File` 结构体类型:
```go ```go
type File struct { type File struct {
@@ -37,11 +37,11 @@ f := NewFile(10, "./test.txt")
我们可以说是工厂实例化了类型的一个对象,就像在基于类的 OO 语言中那样。 我们可以说是工厂实例化了类型的一个对象,就像在基于类的 OO 语言中那样。
如果想知道结构体类型 T 的一个实例占用了多少内存,可以使用:`size := unsafe.Sizeof(T{})` 如果想知道结构体类型 `T` 的一个实例占用了多少内存,可以使用:`size := unsafe.Sizeof(T{})`
**如何强制使用工厂方法** **如何强制使用工厂方法**
通过应用可见性规则参考[4.2.1节](04.2.md)、[9.5 节](09.5.md)就可以禁止使用 new 函数,强制用户使用工厂方法,从而使类型变成私有的,就像在面向对象语言中那样。 通过应用可见性规则参考 [4.2.1节](04.2.md)、[9.5 节](09.5.md) 就可以禁止使用 `new()` 函数,强制用户使用工厂方法,从而使类型变成私有的,就像在面向对象语言中那样。
```go ```go
type matrix struct { type matrix struct {
@@ -66,13 +66,13 @@ right := matrix.NewMatrix(...) // 实例化 matrix 的唯一方式
## 10.2.2 map 和 struct vs new() 和 make() ## 10.2.2 map 和 struct vs new() 和 make()
new 和 make 这两个内置函数已经在第 [7.2.4](07.2.md) 节通过切片的例子说明过一次。 `new()``make()` 这两个内置函数已经在第 [7.2.4](07.2.md) 节通过切片的例子说明过一次。
现在为止我们已经见到了可以使用 `make()` 的三种类型中的其中两个: 现在为止我们已经见到了可以使用 `make()` 的三种类型中的其中两个:
slices / maps / channels见第 14 章) slices / maps / channels见第 14 章)
下面的例子说明了在映射上使用 new 和 make 的区别以及可能发生的错误: 下面的例子说明了在映射上使用 `new()``make()` 的区别以及可能发生的错误:
示例 10.4 [new_make.go](examples/chapter_10/new_make.go)(不能编译) 示例 10.4 [new_make.go](examples/chapter_10/new_make.go)(不能编译)
@@ -108,7 +108,7 @@ func main() {
} }
``` ```
试图 `make()` 一个结构体变量,会引发一个编译错误,这还不是太糟糕,但是 `new()` 一个 map 并试图向其填充数据,将会引发运行时错误! 因为 `new(Foo)` 返回的是一个指向 `nil` 的指针,它尚未被分配内存。所以在使用 `map` 时要特别谨慎。 试图 `make()` 一个结构体变量,会引发一个编译错误,这还不是太糟糕,但是 `new()` 一个 `map` 并试图向其填充数据,将会引发运行时错误! 因为 `new(Foo)` 返回的是一个指向 `nil` 的指针,它尚未被分配内存。所以在使用 `map` 时要特别谨慎。
## 链接 ## 链接

View File

@@ -1,6 +1,6 @@
# 10.3 使用自定义包中的结构体 # 10.3 使用自定义包中的结构体
下面的例子中main.go 使用了一个结构体,它来自 struct_pack 下的包 structPack。 下面的例子中main.go 使用了一个结构体,它来自 struct_pack 下的包 `structPack`
示例 10.5 [structPack.go](examples/chapter_10/struct_pack/structPack.go) 示例 10.5 [structPack.go](examples/chapter_10/struct_pack/structPack.go)

View File

@@ -1,6 +1,7 @@
# 10.4 带标签的结构体 # 10.4 带标签的结构体
结构体中的字段除了有名字和类型外,还可以有一个可选的标签tag:它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 `reflect` 能获取它。我们将在下一章([11.10 节](11.10.md)中深入的探讨 `reflect` 包,它可以在运行时自省类型、属性和方法,比如:在一个变量上调用 `reflect.TypeOf()` 可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。 结构体中的字段除了有名字和类型外,还可以有一个可选的标签 (tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 `reflect` 能获取它。我们将在下一章([11.10 节](11.10.md)中深入的探讨 `reflect` 包,它可以在运行时自省类型、属性和方法,比如:在一个变量上调用 `reflect.TypeOf()` 可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。
示例 10.7 [struct_tag.go](examples/chapter_10/struct_tag.go) 示例 10.7 [struct_tag.go](examples/chapter_10/struct_tag.go)

View File

@@ -92,9 +92,9 @@ func main() {
1 2 3 4 1 2 3 4
{1 2} {1 2}
**练习 10.5** anonymous_struct.go **练习 10.5** [anonymous_struct.go](exercises\chapter_10\anonymous_struct.go)
创建一个结构体,它有一个具名的 float 字段2 个匿名字段,类型分别是 int 和 string。通过结构体字面量新建一个结构体实例并打印它的内容。 创建一个结构体,它有一个具名的 `float32` 字段2 个匿名字段,类型分别是 `int``string`。通过结构体字面量新建一个结构体实例并打印它的内容。
## 10.5.3 命名冲突 ## 10.5.3 命名冲突
@@ -113,14 +113,14 @@ type C struct {A; B}
var c C var c C
``` ```
规则 2使用 `c.a` 是错误的,到底是 `c.A.a` 还是 `c.B.a` 呢?会导致编译器错误:**ambiguous DOT reference c.a disambiguate with either c.A.a or c.B.a**。 规则 2使用 `c.a` 是错误的,到底是 `c.A.a` 还是 `c.B.a` 呢?会导致编译器错误:**`ambiguous DOT reference c.a disambiguate with either c.A.a or c.B.a`**。
```go ```go
type D struct {B; b float32} type D struct {B; b float32}
var d D var d D
``` ```
规则1使用 `d.b` 是没问题的:它是 float32而不是 `B``b`。如果想要内层的 `b` 可以通过 `d.B.b` 得到。 规则1使用 `d.b` 是没问题的:它是 `float32`,而不是 `B``b`。如果想要内层的 `b` 可以通过 `d.B.b` 得到。
## 链接 ## 链接

View File

@@ -2,15 +2,15 @@
## 10.6.1 方法是什么 ## 10.6.1 方法是什么
在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 中有一个概念它和方法有着同样的名字并且大体上意思相同Go 方法是作用在接收者receiver上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。 在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 中有一个概念它和方法有着同样的名字并且大体上意思相同Go 方法是作用在接收者 (receiver) 上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。
接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。但是接收者不能是一个接口类型(参考 第 11 章),因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:**invalid receiver type…** 接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 `int``bool``string` 或数组的别名类型。但是接收者不能是一个接口类型(参考[第 11 章](11.0.md)),因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:`invalid receiver type...`
最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。 最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。
一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。 一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。
类型 T或 \*T)上的所有方法的集合叫做类型 T或 \*T)的方法集method set 类型 `T`(或 `*T`)上的所有方法的集合叫做类型 `T`(或 `*T`)的方法集 (method set)
因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的: 因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的:
@@ -29,7 +29,7 @@ func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
在方法名之前,`func` 关键字之后的括号中指定 receiver。 在方法名之前,`func` 关键字之后的括号中指定 receiver。
如果 `recv` 是 receiver 的实例Method1 是它的方法名,那么方法调用遵循传统的 `object.name` 选择器符号:**recv.Method1()** 如果 `recv` 是 receiver 的实例,`Method1` 是它的方法名,那么方法调用遵循传统的 `object.name` 选择器符号:`recv.Method1()`
如果 `recv` 是一个指针Go 会自动解引用。 如果 `recv` 是一个指针Go 会自动解引用。
@@ -103,11 +103,11 @@ func main() {
} }
``` ```
**练习 10.6** employee_salary.go **练习 10.6** [employee_salary.go](exercises\chapter_10\employee_salary.go)
定义结构体 `employee`,它有一个 `salary` 字段,给这个结构体定义一个方法 `giveRaise` 来按照指定的百分比增加薪水。 定义结构体 `employee`,它有一个 `salary` 字段,给这个结构体定义一个方法 `giveRaise` 来按照指定的百分比增加薪水。
**练习 10.7** iteration_list.go **练习 10.7** [iteration_list.go](exercises\chapter_10\iteration_list.go)
下面这段代码有什么错? 下面这段代码有什么错?
@@ -127,7 +127,7 @@ func main() {
} }
``` ```
类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 int、float 或类似这些的类型上定义方法。试图在 int 类型上定义方法会得到一个编译错误: 类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 `int``float32(64)` 或类似这些的类型上定义方法。试图在 `int` 类型上定义方法会得到一个编译错误:
cannot define new methods on non-local type int cannot define new methods on non-local type int
@@ -141,7 +141,7 @@ func (t time.Time) first3Chars() string {
类型在其他的,或是非本地的包里定义,在它上面定义方法都会得到和上面同样的错误。 类型在其他的,或是非本地的包里定义,在它上面定义方法都会得到和上面同样的错误。
但是有一个间接的方式可以先定义该类型比如int 或 float的别名类型然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。 但是有一个间接的方式:可以先定义该类型(比如:`int``float32(64)`)的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。
示例 10.12 [method_on_time.go](examples/chapter_10/method_on_time.go) 示例 10.12 [method_on_time.go](examples/chapter_10/method_on_time.go)
@@ -176,17 +176,17 @@ First 3 chars: Mon
## 10.6.2 函数和方法的区别 ## 10.6.2 函数和方法的区别
函数将变量作为参数:**Function1(recv)** 函数将变量作为参数:`Function1(recv)`
方法在变量上被调用:**recv.Method1()** 方法在变量上被调用:`recv.Method1()`
在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。 在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。
**不要忘记 Method1 后边的括号 (),否则会引发编译器错误:`method recv.Method1 is not an expression, must be called`** **不要忘记 `Method1()` 后边的括号 `()`,否则会引发编译器错误:`method recv.Method1 is not an expression, must be called`**
接收者必须有一个显式的名字,这个名字必须在方法中被使用。 接收者必须有一个显式的名字,这个名字必须在方法中被使用。
**receiver_type** 叫做 **(接收者)基本类型**,这个类型必须在和方法同样的包中被声明。 `receiver_type` 叫做 **(接收者)基本类型**,这个类型必须在和方法同样的包中被声明。
在 Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。 在 Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。
@@ -194,11 +194,11 @@ First 3 chars: Mon
## 10.6.3 指针或值作为接收者 ## 10.6.3 指针或值作为接收者
鉴于性能的原因,`recv` 最常见的是一个指向 receiver_type 的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver 类型是结构体时,就更是如此了。 鉴于性能的原因,`recv` 最常见的是一个指向 `receiver_type` 的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver 类型是结构体时,就更是如此了。
如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。 如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。
下面的例子 `pointer_value.go` 作了说明:`change()`接受一个指向 B 的指针,并改变它内部的成员;`write()` 通过拷贝接受 B 的值并只输出 B 的内容。注意 Go 为我们做了探测工作我们自己并没有指出是否在指针上调用方法Go 替我们做了这些事情。b1 是值而 b2 是指针,方法都支持运行了。 下面的例子 `pointer_value.go` 作了说明:`change()`接受一个指向 `B` 的指针,并改变它内部的成员;`write()` 通过拷贝接受 `B` 的值并只输出 `B` 的内容。注意 Go 为我们做了探测工作我们自己并没有指出是否在指针上调用方法Go 替我们做了这些事情。`b1` 是值而 `b2` 是指针,方法都支持运行了。
示例 10.13 [pointer_value.go](examples/chapter_10/pointer_value.go) 示例 10.13 [pointer_value.go](examples/chapter_10/pointer_value.go)
@@ -233,7 +233,7 @@ func main() {
*/ */
``` ```
试着在 `write()` 中改变接收者 b 的值:将会看到它可以正常编译,但是开始的 b 没有被改变。 试着在 `write()` 中改变接收者 `b` 的值:将会看到它可以正常编译,但是开始的 `b` 没有被改变。
我们知道方法将指针作为接收者不是必须的,如下面的例子,我们只是需要 `Point3` 的值来做计算: 我们知道方法将指针作为接收者不是必须的,如下面的例子,我们只是需要 `Point3` 的值来做计算:
@@ -251,7 +251,7 @@ func (p Point3) Abs() float64 {
可以使用 `p3.Abs()` 来替代 `(*p3).Abs()` 可以使用 `p3.Abs()` 来替代 `(*p3).Abs()`
像例子 10.10[method1.go](examples/chapter_10/method1.go)中接收者类型是 `*TwoInts` 的方法 `AddThem()`,它能在类型 `TwoInts` 的值上被调用,这是自动间接发生的。 像例子 10.10 ([method1.go](examples/chapter_10/method1.go)) 中接收者类型是 `*TwoInts` 的方法 `AddThem()`,它能在类型 `TwoInts` 的值上被调用,这是自动间接发生的。
因此 `two2.AddThem` 可以替代 `(&two2).AddThem()` 因此 `two2.AddThem` 可以替代 `(&two2).AddThem()`
@@ -259,7 +259,7 @@ func (p Point3) Abs() float64 {
可以有连接到类型的方法,也可以有连接到类型指针的方法。 可以有连接到类型的方法,也可以有连接到类型指针的方法。
但是这没关系:对于类型 T,如果在 \*T 上存在方法 `Meth()`,并且 `t` 是这个类型的变量,那么 `t.Meth()` 会被自动转换为 `(&t).Meth()` 但是这没关系:对于类型 `T`,如果在 `\*T` 上存在方法 `Meth()`,并且 `t` 是这个类型的变量,那么 `t.Meth()` 会被自动转换为 `(&t).Meth()`
**指针方法和值方法都可以在指针或非指针上被调用**,如下面程序所示,类型 `List` 在值上有一个方法 `Len()`,在指针上有一个方法 `Append()`,但是可以看到两个方法都可以在两种类型的变量上被调用。 **指针方法和值方法都可以在指针或非指针上被调用**,如下面程序所示,类型 `List` 在值上有一个方法 `Len()`,在指针上有一个方法 `Append()`,但是可以看到两个方法都可以在两种类型的变量上被调用。
@@ -294,7 +294,7 @@ func main() {
考虑 `person2.go` 中的 `person` 包:类型 `Person` 被明确的导出了,但是它的字段没有被导出。例如在 `use_person2.go``p.firstName` 就是错误的。该如何在另一个程序中修改或者只是读取一个 `Person` 的名字呢? 考虑 `person2.go` 中的 `person` 包:类型 `Person` 被明确的导出了,但是它的字段没有被导出。例如在 `use_person2.go``p.firstName` 就是错误的。该如何在另一个程序中修改或者只是读取一个 `Person` 的名字呢?
这可以通过面向对象语言一个众所周知的技术来完成:提供 getter 和 setter 方法。对于 setter 方法使用 Set 前缀,对于 getter 方法只使用成员名。 这可以通过面向对象语言一个众所周知的技术来完成:提供 `getter()``setter()` 方法。对于 `setter()` 方法使用 `Set...` 前缀,对于 `getter()` 方法只使用成员名。
示例 10.15 [person2.go](examples/chapter_10/person2.go) 示例 10.15 [person2.go](examples/chapter_10/person2.go)
@@ -315,7 +315,7 @@ func (p *Person) SetFirstName(newName string) {
} }
``` ```
示例 10.16[use_person2.go](examples/chapter_10/use_person2.go) 示例 10.16 [use_person2.go](examples/chapter_10/use_person2.go)
```go ```go
package main package main
@@ -337,11 +337,11 @@ func main() {
**并发访问对象** **并发访问对象**
对象的字段(属性)不应该由 2 个或 2 个以上的不同线程在同一时间去改变。如果在程序发生这种情况,为了安全并发访问,可以使用包 `sync`(参考第 9.3 节中的方法。在第 14.17 节中我们会通过 goroutines 和 channels 探索另一种方式。 对象的字段(属性)不应该由 2 个或 2 个以上的不同线程在同一时间去改变。如果在程序发生这种情况,为了安全并发访问,可以使用包 `sync`(参考[第 9.3 节](09.3.md)中的方法。在[第 14.17 节](14.17)中我们会通过 goroutines 和 channels 探索另一种方式。
## 10.6.5 内嵌类型的方法和继承 ## 10.6.5 内嵌类型的方法和继承
当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型 **继承** 了这些方法:**将父类型放在子类型中来实现亚型**。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果,也类似 Ruby 中的混入mixin 当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型 **继承** 了这些方法:**将父类型放在子类型中来实现亚型**。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果,也类似 Ruby 中的混入 (mixin)
下面是一个示例(可以在练习 10.8 中进一步学习):假定有一个 `Engine` 接口类型,一个 `Car` 结构体类型,它包含一个 `Engine` 类型的匿名字段: 下面是一个示例(可以在练习 10.8 中进一步学习):假定有一个 `Engine` 接口类型,一个 `Car` 结构体类型,它包含一个 `Engine` 类型的匿名字段:
@@ -411,11 +411,11 @@ func (n *NamedPoint) Abs() float64 {
现在 `fmt.Println(n.Abs())` 会打印 `500` 现在 `fmt.Println(n.Abs())` 会打印 `500`
因为一个结构体可以嵌入多个匿名类型,所以实际上我们可以有一个简单版本的多重继承,就像:`type Child struct { Father; Mother}`。在第 10.6.7 节中会进一步讨论这个问题。 因为一个结构体可以嵌入多个匿名类型,所以实际上我们可以有一个简单版本的多重继承,就像:`type Child struct { Father; Mother}`。在[第 10.6.7 节](10.6.md)中会进一步讨论这个问题。
结构体内嵌和自己在同一个包中的结构体时,可以彼此访问对方所有的字段和方法。 结构体内嵌和自己在同一个包中的结构体时,可以彼此访问对方所有的字段和方法。
**练习 10.8** inheritance_car.go **练习 10.8** [inheritance_car.go](exercises\chapter_10\inheritance_car.go)
创建一个上面 `Car``Engine` 可运行的例子,并且给 `Car` 类型一个 `wheelCount` 字段和一个 `numberOfWheels()` 方法。 创建一个上面 `Car``Engine` 可运行的例子,并且给 `Car` 类型一个 `wheelCount` 字段和一个 `numberOfWheels()` 方法。
@@ -433,7 +433,7 @@ B内嵌内嵌匿名地所需功能类型像前一节 10.6.5 所
为了使这些概念具体化,假设有一个 `Customer` 类型,我们想让它通过 `Log` 类型来包含日志功能,`Log` 类型只是简单地包含一个累积的消息(当然它可以是复杂的)。如果想让特定类型都具备日志功能,你可以实现一个这样的 `Log` 类型,然后将它作为特定类型的一个字段,并提供 `Log()`,它返回这个日志的引用。 为了使这些概念具体化,假设有一个 `Customer` 类型,我们想让它通过 `Log` 类型来包含日志功能,`Log` 类型只是简单地包含一个累积的消息(当然它可以是复杂的)。如果想让特定类型都具备日志功能,你可以实现一个这样的 `Log` 类型,然后将它作为特定类型的一个字段,并提供 `Log()`,它返回这个日志的引用。
方式 A 可以通过如下方法实现(使用了第 10.7 节中的 `String()` 功能): 方式 A 可以通过如下方法实现(使用了[第 10.7 节](10.7.md)中的 `String()` 功能):
示例 10.19 [embed_func1.go](examples/chapter_10/embed_func1.go) 示例 10.19 [embed_func1.go](examples/chapter_10/embed_func1.go)
@@ -485,7 +485,7 @@ func (c *Customer) Log() *Log {
1 - Yes we can! 1 - Yes we can!
2 - After me the world will be a better place! 2 - After me the world will be a better place!
相对的方式 B 可能会像这样[embed_func2.go](examples/chapter_10/embed_func2.go) 相对的方式 B 可能会像这样 ([embed_func2.go](examples/chapter_10/embed_func2.go))
```go ```go
package main package main
@@ -541,7 +541,7 @@ func (c *Customer) String() string {
作为一个例子,假设有一个类型 `CameraPhone`,通过它可以 `Call()`,也可以 `TakeAPicture()`,但是第一个方法属于类型 `Phone`,第二个方法属于类型 `Camera` 作为一个例子,假设有一个类型 `CameraPhone`,通过它可以 `Call()`,也可以 `TakeAPicture()`,但是第一个方法属于类型 `Phone`,第二个方法属于类型 `Camera`
只要嵌入这两个类型就可以解决这个问题,如下所示[mult_inheritance.go](examples/chapter_10/mult_inheritance.go) 只要嵌入这两个类型就可以解决这个问题,如下所示 ([mult_inheritance.go](examples/chapter_10/mult_inheritance.go))
```go ```go
package main package main
@@ -581,17 +581,17 @@ func main() {
It exhibits behavior of a Camera: Click It exhibits behavior of a Camera: Click
It works like a Phone too: Ring Ring It works like a Phone too: Ring Ring
**练习 10.9** point_methods.go **练习 10.9** [point_methods.go](exercises\chapter_10\point_methods.go)
`point.go` 开始(第 10.1 节的练习):使用方法来实现 `Abs()``Scale()`函数,`Point` 作为方法的接收者类型。也为 `Point3``Polar` 实现 `Abs()` 方法。完成了 `point.go` 中同样的事情,只是这次通过方法。 `point.go` 开始([第 10.1 节](10.1)的练习):使用方法来实现 `Abs()``Scale()`函数,`Point` 作为方法的接收者类型。也为 `Point3``Polar` 实现 `Abs()` 方法。完成了 `point.go` 中同样的事情,只是这次通过方法。
**练习 10.10** inherit_methods.go **练习 10.10** [inherit_methods.go](exercises\chapter_10\inherit_methods.go)
定义一个结构体类型 `Base`,它包含一个字段 `id`,方法 `Id()` 返回 `id`,方法 `SetId()` 修改 `id`。结构体类型 `Person` 包含 `Base`,及 `FirstName``LastName` 字段。结构体类型 `Employee` 包含一个 `Person``salary` 字段。 定义一个结构体类型 `Base`,它包含一个字段 `id`,方法 `Id()` 返回 `id`,方法 `SetId()` 修改 `id`。结构体类型 `Person` 包含 `Base`,及 `FirstName``LastName` 字段。结构体类型 `Employee` 包含一个 `Person``salary` 字段。
创建一个 `employee` 实例,然后显示它的 `id` 创建一个 `employee` 实例,然后显示它的 `id`
**练习 10.11** magic.go **练习 10.11** [magic.go](exercises\chapter_10\magic.go)
首先预测一下下面程序的结果,然后动手实验下: 首先预测一下下面程序的结果,然后动手实验下:
@@ -630,7 +630,7 @@ func main() {
## 10.6.8 通用方法和方法命名 ## 10.6.8 通用方法和方法命名
在编程中一些基本操作会一遍又一遍的出现,比如打开Open、关闭Close、读Read、写Write、排序Sort等等,并且它们都有一个大致的意思:打开Open可以作用于一个文件、一个网络连接、一个数据库连接等等。具体的实现可能千差万别,但是基本的概念是一致的。在 Go 语言中,通过使用接口(参考 第 11 章),标准库广泛的应用了这些规则,在标准库中这些通用方法都有一致的名字,比如 `Open()``Read()``Write()`等。想写规范的 Go 程序,就应该遵守这些约定,给方法合适的名字和签名,就像那些通用方法那样。这样做会使 Go 开发的软件更加具有一致性和可读性。比如:如果需要一个 convert-to-string 方法,应该命名为 `String()`,而不是 `ToString()`(参考第 10.7 节)。 在编程中一些基本操作会一遍又一遍的出现,比如打开 (Open)、关闭 (Close)、读 (Read)、写 (Write)、排序(Sort) 等等,并且它们都有一个大致的意思:打开 (Open)可以作用于一个文件、一个网络连接、一个数据库连接等等。具体的实现可能千差万别,但是基本的概念是一致的。在 Go 语言中,通过使用接口(参考[第 11 章](11.0.md)),标准库广泛的应用了这些规则,在标准库中这些通用方法都有一致的名字,比如 `Open()``Read()``Write()`等。想写规范的 Go 程序,就应该遵守这些约定,给方法合适的名字和签名,就像那些通用方法那样。这样做会使 Go 开发的软件更加具有一致性和可读性。比如:如果需要一个 `convert-to-string()` 方法,应该命名为 `String()`,而不是 `ToString()`(参考[第 10.7 节](10.7.md))。
## 10.6.9 和其他面向对象语言比较 Go 的类型和方法 ## 10.6.9 和其他面向对象语言比较 Go 的类型和方法
@@ -640,7 +640,7 @@ func main() {
下面的模式就很好的说明了这个问题: 下面的模式就很好的说明了这个问题:
![](images/10.6.9_fig10.4.jpg?raw=true) <img src="images/10.6.9_fig10.4.jpg?raw=true" style="zoom:80%;" />
Go 不需要一个显式的类定义,如同 Java、C++、C# 等那样,相反地,“类”是通过提供一组作用于一个共同类型的方法集来隐式定义的。类型可以是结构体或者任何用户自定义类型。 Go 不需要一个显式的类定义,如同 Java、C++、C# 等那样,相反地,“类”是通过提供一组作用于一个共同类型的方法集来隐式定义的。类型可以是结构体或者任何用户自定义类型。
@@ -653,19 +653,19 @@ func (i *Integer) String() string {
} }
``` ```
在 Java 或 C# 中,这个方法需要和类 `Integer` 的定义放在一起,在 Ruby 中可以直接在基本类型 int 上定义这个方法。 在 Java 或 C# 中,这个方法需要和类 `Integer` 的定义放在一起,在 Ruby 中可以直接在基本类型 `int` 上定义这个方法。
**总结** **总结**
在 Go 中类型就是类数据和关联的方法。Go 不知道类似面向对象语言的类继承的概念。继承有两个好处:代码复用和多态。 在 Go 中类型就是类数据和关联的方法。Go 不知道类似面向对象语言的类继承的概念。继承有两个好处:代码复用和多态。
在 Go 中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫 **组件编程Component Programming** 在 Go 中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫 **组件编程 (Component Programming)**
许多开发者说相比于类继承Go 的接口提供了更强大、却更简单的多态行为。 许多开发者说相比于类继承Go 的接口提供了更强大、却更简单的多态行为。
**备注** **备注**
如果真的需要更多面向对象的能力,看一下 [`goop`](https://github.com/losalamos/goop) 包Go Object-Oriented Programming,它由 Scott Pakin 编写: 它给 Go 提供了 JavaScript 风格的对象(基于原型的对象),并且支持多重继承和类型独立分派,通过它可以实现你喜欢的其他编程语言里的一些结构。 如果真的需要更多面向对象的能力,看一下 [`goop`](https://github.com/losalamos/goop) 包 (Go Object-Oriented Programming),它由 Scott Pakin 编写: 它给 Go 提供了 JavaScript 风格的对象(基于原型的对象),并且支持多重继承和类型独立分派,通过它可以实现你喜欢的其他编程语言里的一些结构。
**问题 10.1** **问题 10.1**
@@ -675,7 +675,7 @@ func (i *Integer) String() string {
a假设定义 `type Integer int`,完成 `get()` 方法的方法体: `func (p Integer) get() int { ... }` a假设定义 `type Integer int`,完成 `get()` 方法的方法体: `func (p Integer) get() int { ... }`
b定义 `func f(i int) {}; var v Integer` ,如何就 v 作为参数调用f b定义 `func f(i int) {}; var v Integer` ,如何就 `v` 作为参数调用f
c假设 `Integer` 定义为 `type Integer struct {n int}`,完成 `get()` 方法的方法体:`func (p Integer) get() int { ... }` c假设 `Integer` 定义为 `type Integer struct {n int}`,完成 `get()` 方法的方法体:`func (p Integer) get() int { ... }`

View File

@@ -2,7 +2,8 @@
当定义了一个有很多方法的类型时,十之八九你会使用 `String()` 方法来定制类型的字符串形式的输出,换句话说:一种可阅读性和打印性的输出。如果类型定义了 `String()` 方法,它会被用在 `fmt.Printf()` 中生成默认的输出:等同于使用格式化描述符 `%v` 产生的输出。还有 `fmt.Print()``fmt.Println()` 也会自动使用 `String()` 方法。 当定义了一个有很多方法的类型时,十之八九你会使用 `String()` 方法来定制类型的字符串形式的输出,换句话说:一种可阅读性和打印性的输出。如果类型定义了 `String()` 方法,它会被用在 `fmt.Printf()` 中生成默认的输出:等同于使用格式化描述符 `%v` 产生的输出。还有 `fmt.Print()``fmt.Println()` 也会自动使用 `String()` 方法。
我们使用第 10.4 节中程序的类型来进行测试: 我们使用[第 10.4 节](10.4.md)中程序的类型来进行测试:
示例 10.22 [method_string.go](examples/chapter_10/method_string.go) 示例 10.22 [method_string.go](examples/chapter_10/method_string.go)
@@ -45,7 +46,8 @@ func (tn *TwoInts) String() string {
**备注** **备注**
不要在 `String()` 方法里面调用涉及 `String()` 方法的方法,它会导致意料之外的错误,比如下面的例子,它导致了一个无限递归调用(`TT.String()` 调用 `fmt.Sprintf`,而 `fmt.Sprintf` 又会反过来调用 `TT.String()`...),很快就会导致内存溢出: 不要在 `String()` 方法里面调用涉及 `String()` 方法的方法,它会导致意料之外的错误,比如下面的例子,它导致了一个无限递归调用(`TT.String()` 调用 `fmt.Sprintf`,而 `fmt.Sprintf` 又会反过来调用 `TT.String()`),很快就会导致内存溢出:
```go ```go
type TT float64 type TT float64
@@ -56,9 +58,9 @@ func (t TT) String() string {
t.String() t.String()
``` ```
**练习 10.12** type_string.go **练习 10.12** [type_string.go](exercises\chapter_10\type_string.go)
给定结构体类型 T: 给定结构体类型 `T`:
```go ```go
type T struct { type T struct {
@@ -70,40 +72,40 @@ type T struct {
`t`: `t := &T{7, -2.35, "abc\tdef"}`。给 T 定义 `String()`,使得 `fmt.Printf("%v\n", t)` 输出:`7 / -2.350000 / "abc\tdef"` `t`: `t := &T{7, -2.35, "abc\tdef"}`。给 T 定义 `String()`,使得 `fmt.Printf("%v\n", t)` 输出:`7 / -2.350000 / "abc\tdef"`
**练习 10.13** celsius.go **练习 10.13** [celsius.go](exercises\chapter_10\celsius.go)
为 float64 定义一个别名类型 `Celsius`,并给它定义 `String()`,它输出一个十进制数和 °C 表示的温度值。 `float64` 定义一个别名类型 `Celsius`,并给它定义 `String()`,它输出一个十进制数和 °C 表示的温度值。
**练习 10.14** days.go **练习 10.14** [days.go](exercises\chapter_10\days.go)
为 int 定义一个别名类型 `Day`,定义一个字符串数组它包含一周七天的名字,为类型 `Day` 定义 `String()` 方法,它输出星期几的名字。使用 `iota` 定义一个枚举常量用于表示一周的中每天MO、TU...)。 `int` 定义一个别名类型 `Day`,定义一个字符串数组它包含一周七天的名字,为类型 `Day` 定义 `String()` 方法,它输出星期几的名字。使用 `iota` 定义一个枚举常量用于表示一周的中每天MO、TU...)。
**练习 10.15** timezones.go **练习 10.15** [timezones.go](exercises\chapter_10\timezones.go)
为 int 定义别名类型 `TZ`,定义一些常量表示时区,比如 UTC定义一个 map它将时区的缩写映射为它的全称比如`UTC -> "Universal Greenwich time"`。为类型 `TZ` 定义 `String()` 方法,它输出时区的全称。 `int` 定义别名类型 `TZ`,定义一些常量表示时区,比如 UTC定义一个 `map`,它将时区的缩写映射为它的全称,比如:`UTC -> "Universal Greenwich time"`。为类型 `TZ` 定义 `String()` 方法,它输出时区的全称。
**练习 10.16** stack_arr.go/stack_struct.go **练习 10.16** [stack_arr.go](exercises\chapter_10\stack_arr.go) / [stack_struct.go](exercises\chapter_10\stack_struct.go)
实现栈stack数据结构: 实现栈 (stack) 数据结构:
![](images/10.7_fig.jpg?raw=true) ![](images/10.7_fig.jpg?raw=true)
它的格子包含数据,比如整数 i、j、k 和 l 等等,格子从底部(索引 0至顶部索引 n来索引。这个例子中假定 `n = 3`,那么一共有 4 个格子。 它的格子包含数据,比如整数 `i``j``k``l` 等等,格子从底部(索引 0至顶部索引 n来索引。这个例子中假定 `n = 3`,那么一共有 4 个格子。
一个新栈中所有格子的值都是 0 一个新栈中所有格子的值都是 `0`
将一个新值放到栈的最顶部一个空(包括零)的格子中,这叫做`push` 将一个新值放到栈的最顶部一个空(包括零)的格子中,这叫做 push。
获取栈的最顶部一个非空(非零)的格子的值,这叫做`pop` 获取栈的最顶部一个非空(非零)的格子的值,这叫做 pop。
现在可以理解为什么栈是一个后进先出LIFO的结构了吧。 现在可以理解为什么栈是一个后进先出 (LIFO) 的结构了吧。
为栈定义一个 `Stack` 类型,并为它定义 `Push``Pop` 方法,再为它定义 `String()` 方法(用于调试)输出栈的内容,比如:`[0:i] [1:j] [2:k] [3:l]` 为栈定义一个 `Stack` 类型,并为它定义 `Push``Pop` 方法,再为它定义 `String()` 方法(用于调试)输出栈的内容,比如:`[0:i] [1:j] [2:k] [3:l]`
1stack_arr.go使用长度为 4 的 int 数组作为底层数据结构。 1[stack_arr.go](exercises\chapter_10\stack_arr.go):使用长度为 4 的 int 数组作为底层数据结构。
2stack_struct.go使用包含一个索引和一个 int 数组的结构体作为底层数据结构,索引表示第一个空闲的位置。 2 [stack_struct.go](exercises\chapter_10\stack_struct.go):使用包含一个索引和一个 `int` 数组的结构体作为底层数据结构,索引表示第一个空闲的位置。
3使用常量 LIMIT 代替上面表示元素个数的 4 重新实现上面的 1和 2使它们更具有一般性。 3使用常量 `LIMIT` 代替上面表示元素个数的 4 重新实现上面的 1和 2使它们更具有一般性。
## 链接 ## 链接

View File

@@ -1,6 +1,6 @@
# 10.8 垃圾回收和 SetFinalizer # 10.8 垃圾回收和 SetFinalizer
Go 开发者不需要写代码来释放程序中不再使用的变量和结构占用的内存,在 Go 运行时中有一个独立的进程,即垃圾收集器GC,会处理这些事情,它搜索不再使用的变量然后释放它们的内存。可以通过 `runtime` 包访问 GC 进程。 Go 开发者不需要写代码来释放程序中不再使用的变量和结构占用的内存,在 Go 运行时中有一个独立的进程,即垃圾收集器 (GC),会处理这些事情,它搜索不再使用的变量然后释放它们的内存。可以通过 `runtime` 包访问 GC 进程。
通过调用 `runtime.GC()` 函数可以显式的触发 GC但这只在某些罕见的场景下才有用比如当内存资源不足时调用 `runtime.GC()`,它会在此函数执行的点上立即释放一大片内存,此时程序可能会有短时的性能下降(因为 `GC` 进程在执行)。 通过调用 `runtime.GC()` 函数可以显式的触发 GC但这只在某些罕见的场景下才有用比如当内存资源不足时调用 `runtime.GC()`,它会在此函数执行的点上立即释放一大片内存,此时程序可能会有短时的性能下降(因为 `GC` 进程在执行)。
@@ -16,7 +16,7 @@ fmt.Printf("%d Kb\n", m.Alloc / 1024)
上面的程序会给出已分配内存的总量,单位是 Kb。进一步的测量参考 [文档页面](http://golang.org/pkg/runtime/#MemStatsType)。 上面的程序会给出已分配内存的总量,单位是 Kb。进一步的测量参考 [文档页面](http://golang.org/pkg/runtime/#MemStatsType)。
如果需要在一个对象 obj 被从内存移除前执行一些特殊操作,比如写到日志文件中,可以通过如下方式调用函数来实现: 如果需要在一个对象 `obj` 被从内存移除前执行一些特殊操作,比如写到日志文件中,可以通过如下方式调用函数来实现:
```go ```go
runtime.SetFinalizer(obj, func(obj *typeObj)) runtime.SetFinalizer(obj, func(obj *typeObj))
@@ -26,12 +26,12 @@ runtime.SetFinalizer(obj, func(obj *typeObj))
在对象被 GC 进程选中并从内存中移除以前,`SetFinalizer` 都不会执行,即使程序正常结束或者发生错误。 在对象被 GC 进程选中并从内存中移除以前,`SetFinalizer` 都不会执行,即使程序正常结束或者发生错误。
**练习 10.17** **练习 10.17** [main_stack.go](exercises\chapter_10\main_stack.go)
从练习 10.16 开始(它基于结构体实现了一个栈结构),为栈的实现stack_struct.go创建一个单独的包 `stack`,并从 `main``main.stack.go` 中调用它。 从练习 10.16 开始(它基于结构体实现了一个栈结构),为栈的实现 ([stack_struct.go](exercises\chapter_10\stack_struct.go)) 创建一个单独的包 `stack`,并从 `main``main.stack.go` 中调用它。
## 链接 ## 链接
- [目录](directory.md) - [目录](directory.md)
- 上一节:[类型的 String() 方法和格式化描述符](10.7.md) - 上一节:[类型的 String() 方法和格式化描述符](10.7.md)
- 下一章:[接口Interfaces与反射reflection](11.0.md) - 下一章:[接口 (Interfaces) 与反射 (reflection)](11.0.md)