diff --git a/README.md b/README.md index e4d4aff..8b8c917 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ## 翻译进度 -9.11 [在 Go 程序中使用外部库](eBook/09.11.md) +10.8 [垃圾回收和 SetFinalizer](eBook/10.8.md) ## 支持本书 diff --git a/README_gc.md b/README_gc.md index 1ea7791..9f15bf3 100644 --- a/README_gc.md +++ b/README_gc.md @@ -19,4 +19,4 @@ Golang 编程:245386165 |更新日期 |更新内容 |----------|------------------ -|2015-08-17|10.5 匿名字段和内嵌结构体 \ No newline at end of file +|2015-08-25|10.8 垃圾回收和 SetFinalizer \ No newline at end of file diff --git a/eBook/10.6.md b/eBook/10.6.md index d53982d..32def04 100644 --- a/eBook/10.6.md +++ b/eBook/10.6.md @@ -2,42 +2,47 @@ ## 10.6.1 方法是什么 -在Go中,结构体就像是类的一种简化形式,那么OO程序员可能会问:类的方法在哪里呢?在Go中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。 +在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。 -接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是int、bool、string或数组的alias类型。但是接收者不能是一个接口类型(参考 第11章),因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:*invalid receiver type…* +接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。但是接收者不能是一个接口类型(参考 第 11 章),因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:**invalid receiver type…**。 最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。 -一个类型加上它的方法等价于OO中的一个类。一个重要的区别是:在Go中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。 +一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。 -类型T(或*T)上的所有方法的集合叫做类型T(或*T)的方法集。 +类型 T(或 \*T)上的所有方法的集合叫做类型 T(或 \*T)的方法集。 -因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在2个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的: +因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的: ```go func (a *denseMatrix) Add(b Matrix) Matrix func (a *sparseMatrix) Add(b Matrix) Matrix ``` -alias类型不能有它原始类型上已经定义过的方法。 +别名类型不能有它原始类型上已经定义过的方法。 定义方法的一般格式如下: - func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... } +```go +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 会自动解引用。 -如果方法不需要使用recv的值,可以用*_*替换它,比如: +如果方法不需要使用 `recv` 的值,可以用 **_** 替换它,比如: - func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... } +```go +func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... } +``` -recv就像是OO语言中的this或self,但是Go中并没有这两个关键字。随个人喜好,你可以使用this或self作为receiver的名字。下面是一个结构体上的简单方法的例子: +`recv` 就像是面向对象语言中的 `this` 或 `self`,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 `this` 或 `self` 作为 receiver 的名字。下面是一个结构体上的简单方法的例子: + +示例 10.10 method .go: -Listing 10.10—method .go ```go package main @@ -77,10 +82,10 @@ func (tn *TwoInts) AddToParam(param int) int { 下面是非结构体类型上方法的例子: - Listing 10.11—method2.go: +示例 10.11 method2.go: - ```go - package main +```go +package main import "fmt" @@ -96,13 +101,13 @@ func (v IntVector) Sum() (s int) { func main() { fmt.Println(IntVector{1, 2, 3}.Sum()) // 输出是6 } - ``` +``` -练习 10.6:employee_salary.go +**练习 10.6** employee_salary.go -定义结构体employee,它有一个salary字段,给这个结构体定义一个方法giveRaise来按照指定的百分比增加薪水。 +定义结构体 `employee`,它有一个 `salary` 字段,给这个结构体定义一个方法 `giveRaise` 来按照指定的百分比增加薪水。 -练习 10.7:iteration_list.go +**练习 10.7** iteration_list.go 下面这段代码有什么错? @@ -122,11 +127,11 @@ func main() { } ``` -类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在int、float或类似这些的类型上定义方法。试图在int类型上定义方法会得到一个编译错误: +类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 int、float 或类似这些的类型上定义方法。试图在 int 类型上定义方法会得到一个编译错误: cannot define new methods on non-local type int -比如想在time.Time上定义如下方法: +比如想在 `time.Time` 上定义如下方法: ```go func (t time.Time) first3Chars() string { @@ -136,9 +141,9 @@ func (t time.Time) first3Chars() string { 类型在在其他的,或是非本地的包里定义,在它上面定义方法都会得到和上面同样的错误。 -但是有一个绕点的方式:可以先定义该类型(比如int,float)的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。 +但是有一个绕点的方式:可以先定义该类型(比如:int 或 float)的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。 -Listing 10.12—method_on_time.go: +示例 10.12 method_on_time.go: ```go package main @@ -171,31 +176,31 @@ First 3 chars: Mon ## 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 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。 -*方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。* +**方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。** ## 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 是指针,方法都支持运行了。 -Listing 10.13—pointer_value.go: +示例 10.13 pointer_value.go: ```go package main @@ -228,9 +233,9 @@ func main() { */ ``` -试着在write()中改变接收者b的值:将会看到它可以正常编译,但是开始的b没有被改变。 +试着在 `write()` 中改变接收者b的值:将会看到它可以正常编译,但是开始的 b 没有被改变。 -我们知道方法不需要指针作为接收者,如下面的例子,我们只是需要Point3的值来做计算: +我们知道方法不需要指针作为接收者,如下面的例子,我们只是需要 `Point3` 的值来做计算: ```go type Point3 struct { x, y, z float } @@ -240,25 +245,25 @@ func (p Point3) Abs float { } ``` -这样做稍微有点昂贵,因为Point3是作为值传递给方法的,因此传递的是它的拷贝,这在Go中合法的。也可以在指向这个类型的指针上调用此方法(会自动解引用)。 +这样做稍微有点昂贵,因为 `Point3` 是作为值传递给方法的,因此传递的是它的拷贝,这在 Go 中合法的。也可以在指向这个类型的指针上调用此方法(会自动解引用)。 -假设p3定义为一个指针:* p3 := &Point{ 3, 4, 5}* +假设 `p3` 定义为一个指针:`p3 := &Point{ 3, 4, 5}`。 -可以这样写: * p3.Abs() 来替代 (*p3).Abs() * +可以使用 `p3.Abs()` 来替代 `(*p3).Abs()`。 -像例子10.11(method1.go)中接收者类型是*TwoInts的方法AddThem(),它能在类型TwoInts的值上被调用,这是自动间接发生的。 +像例子 10.11(method1.go)中接收者类型是 `*TwoInts` 的方法 `AddThem()`,它能在类型 `TwoInts` 的值上被调用,这是自动间接发生的。 -因此two2.AddThem可以替代(&two2).AddThem()。 +因此 `two2.AddThem` 可以替代 `(&two2).AddThem()`。 在值和指针上调用方法: 可以有连接到类型的方法,也可以有连接到类型指针的方法。 -*但是这没关系:对于类型T,如果在*T上存在方法Meth(),并且t是这个类型的变量,那么t.Meth()会被自动转换为(&t).Meth().* +但是这没关系:对于类型 T,如果在 \*T 上存在方法 `Meth()`,并且 `t` 是这个类型的变量,那么 `t.Meth()` 会被自动转换为 `(&t).Meth()`。 -*指针方法和值方法都可以在指针或非指针上被调用*,如下面程序所示,类型List在值上有一个方法Len(),在指针上有一个方法Append(),但是可以看到两个方法都可以在两种类型的变量上被调用。 +**指针方法和值方法都可以在指针或非指针上被调用**,如下面程序所示,类型 `List` 在值上有一个方法 `Len()`,在指针上有一个方法 `Append()`,但是可以看到两个方法都可以在两种类型的变量上被调用。 -Listing 10.14—methodset1.go: +示例 10.14 methodset1.go: ```go package main @@ -287,11 +292,11 @@ func main() { ## 10.6.4 方法和未导出字段 -考虑person2.go中的person包:类型Person被明确的导出了,但是它的字段没有被导出。例如在use_person2.go中p.firsetname就是错误的。该如何在另一个程序中修改或者只是读取一个Person的名字呢? +考虑 `person2.go` 中的 `person` 包:类型 `Person` 被明确的导出了,但是它的字段没有被导出。例如在 `use_person2.go` 中 `p.firsetname` 就是错误的。该如何在另一个程序中修改或者只是读取一个 `Person` 的名字呢? -这可以通过OO语言一个众所周知的技术来完成:提供getter和setter方法。对于setter方法使用Set前缀,对于getter方法只适用成员名。 +这可以通过面向对象语言一个众所周知的技术来完成:提供 getter 和 setter 方法。对于 setter 方法使用 Set 前缀,对于 getter 方法只适用成员名。 -Listing 10.15—person2.go: +示例 10.15 person2.go: ```go package person @@ -310,7 +315,7 @@ func (p *Person) SetFirstName(newName string) { } ``` -Listing 10.16—use_person2.go: +示例 10.16—use_person2.go: ```go package main @@ -329,16 +334,16 @@ func main() { fmt.Println(p.FirstName()) // Output: Eric } ``` -*并发访问对象:* +**并发访问对象** -对象的字段(属性)不应该由2个或2个以上的不同线程在同一时间去改变。如果在程序发生这种情况,为了安全并发访问,可以使用包sync(参考9.3)中的方法。在14.17我们会通过goroutines和channels探索另一种方式。 +对象的字段(属性)不应该由 2 个或 2 个以上的不同线程在同一时间去改变。如果在程序发生这种情况,为了安全并发访问,可以使用包 `sync`(参考第 9.3 节)中的方法。在第 14.17 节中我们会通过 goroutines 和 channels 探索另一种方式。 ## 10.6.5 内嵌类型的方法和继承 -当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌---在效果上等同于外层类型*继承*了这些方法:*将父类型放在子类型中来实现亚型*。这个机制提供了一种简单的方式来模拟经典OO语言中的子类和继承相关的效果,也类似Ruby中的混入(mixin)。 +当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型 **继承** 了这些方法:**将父类型放在子类型中来实现亚型**。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果,也类似 Ruby 中的混入(mixin)。 -下面是一个示例(可以在练习 10.8中进一步学习):假定有一个Engine接口类型,一个Car结构体类型,它包含一个Engine类型的匿名字段: +下面是一个示例(可以在练习 10.8 中进一步学习):假定有一个 `Engine` 接口类型,一个 `Car` 结构体类型,它包含一个 `Engine` 类型的匿名字段: ```go type Engine interface { @@ -363,7 +368,7 @@ func (c *Car) GoToWorkIn() { } ``` -下面是method3.go的完整例子,它展示了内嵌结构体上的方法可以直接在外层类型的实例上调用: +下面是 `method3.go` 的完整例子,它展示了内嵌结构体上的方法可以直接在外层类型的实例上调用: ```go package main @@ -394,7 +399,9 @@ func main() { 内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法“晋升”成为了外层类型的方法。当然类型可以有只作用于本身实例而不作用于内嵌“父”类型上的方法, -可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。在Listing 10.18—method4.go中添加: +可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。 + +在示例 10.18 method4.go 中添加: ```go func (n *NamedPoint) Abs() float64 { @@ -402,32 +409,33 @@ 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.8:inheritance_car.go +**练习 10.8** inheritance_car.go -创建一个上面Car和Engine可运行的例子,并且给Car类型一个wheelCount字段和一个numberOfWheels()方法。 +创建一个上面 `Car` 和 `Engine` 可运行的例子,并且给 `Car` 类型一个 `wheelCount` 字段和一个 `numberOfWheels()` 方法。 -创建一个Mercedes类型,它内嵌Car,并新建Mercedes的一个实例,然后调用它的方法。 +创建一个 `Mercedes` 类型,它内嵌 `Car`,并新建 `Mercedes` 的一个实例,然后调用它的方法。 -然后仅在Mercedes类型上创建方法sayHiToMerkel()并调用它。 +然后仅在 `Mercedes` 类型上创建方法 `sayHiToMerkel()` 并调用它。 ## 10.6.6 如何在类型中嵌入功能 主要有两种方法来实现在类型中嵌入功能: -A. 聚合(或组合):包含一个所需功能类型的具名字段 -B. 内嵌:内嵌(匿名地)所需功能类型,像前一节10.6.5 所示的那样 +A:聚合(或组合):包含一个所需功能类型的具名字段。 +B:内嵌:内嵌(匿名地)所需功能类型,像前一节 10.6.5 所演示的那样。 -为了使这些概念具体化,假设有一个Customer类型,我们想让它通过Log类型来包含日志功能,Log类型只是简单地包含一个累积的消息(当然它可以是复杂的)。如果想让特定类型都具备日志功能,你可以实现一个这样的Log类型,然后将它作为特定类型的一个字段,并提供Log(),它返回这个日志的引用。 +为了使这些概念具体化,假设有一个 `Customer` 类型,我们想让它通过 `Log` 类型来包含日志功能,`Log` 类型只是简单地包含一个累积的消息(当然它可以是复杂的)。如果想让特定类型都具备日志功能,你可以实现一个这样的 `Log` 类型,然后将它作为特定类型的一个字段,并提供 `Log()`,它返回这个日志的引用。 -方式A可以通过如下方法实现(使用了10.7中的String()功能): +方式 A 可以通过如下方法实现(使用了第 10.7 节中的 `String()` 功能): + +示例 10.19 embed_func1.go: -Listing 10.19—embed_func1.go: ```go package main @@ -476,7 +484,7 @@ func (c *Customer) Log() *Log { 1 - Yes we can! 2 - After me the world will be a better place! -相对的方式B可能会像这样: +相对的方式 B 可能会像这样: ```go package main @@ -520,7 +528,7 @@ func (c *Customer) String() string { Log:{1 - Yes we can! 2 - After me the world will be a better place!} -内嵌的类型不需要指针,Customer也不需要Add方法,它使用Log的Add方法,Customer有自己的String方法,并且在它里面调用了Log的String方法。 +内嵌的类型不需要指针,`Customer` 也不需要 `Add` 方法,它使用 `Log` 的 `Add` 方法,`Customer` 有自己的 `String` 方法,并且在它里面调用了 `Log` 的 `String` 方法。 如果内嵌类型嵌入了其他类型,也是可以的,那些类型的方法可以直接在外层类型中使用。 @@ -528,11 +536,12 @@ func (c *Customer) String() string { ## 10.6.7 多重继承 -多重继承指的是类型获得多个父类型行为的能力,它在传统的面向对象语言中通常是不被实现的(C++和Python例外)。因为在类继承层次中,多重继承会给编译器引入额外的复杂度。但是Go语言中,通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承。 +多重继承指的是类型获得多个父类型行为的能力,它在传统的面向对象语言中通常是不被实现的(C++ 和 Python 例外)。因为在类继承层次中,多重继承会给编译器引入额外的复杂度。但是在 Go 语言中,通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承。 -作为一个例子,假设有一个类型CameraPhone,通过它可以Call(),也可以TakeAPicture(),但是第一个方法属于类型Phone,第二个方法属于类型Camera。 +作为一个例子,假设有一个类型 `CameraPhone`,通过它可以 `Call()`,也可以 `TakeAPicture()`,但是第一个方法属于类型 `Phone`,第二个方法属于类型 `Camera`。 只要嵌入这两个类型就可以解个问题,如下所示: + ```go package main @@ -571,17 +580,17 @@ func main() { It exhibits behavior of a Camera: Click It works like a Phone too: Ring Ring -练习 10.9:point_methods.go: +**练习 10.9** point_methods.go: -从point.go开始(10.1的联系):使用方法来实现Abs()和Scale()函数,Point作为方法的接收者类型。也为Point3和Polar实现Abs()方法。做point.go中同样的事情,只是这次通过方法。 +从 `point.go` 开始(第 10.1 节的联系):使用方法来实现 `Abs()` 和 `Scale()`函数,`Point` 作为方法的接收者类型。也为 `Point3` 和 `Polar` 实现 `Abs()` 方法。完成了 `point.go` 中同样的事情,只是这次通过方法。 -练习 10.10:inherit_methods.go: +**练习 10.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: 首先预测一下下面程序的结果,然后动手实验下: @@ -620,21 +629,21 @@ func main() { ## 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 章),标准库广泛的应用了这些规则,在标准库中这些通用方法都有一致的名字,比如 `Open()`、`Read()`、`Write()`等。想写规范的 Go 程序,就应该遵守这些约定,给方法合适的名字和签名,就像那些通用方法那样。这样做会使 Go 开发的软件更加具有一致性和可读性。比如:如果需要一个 convert-to-string 方法,应该命名为 `String()`,而不是 `ToString()`(参考第 10.7 节)。 -## 10.6.9 和其他面向对象语言比较Go的类型和方法 +## 10.6.9 和其他面向对象语言比较 Go 的类型和方法 -在如C++、Java、C#和Ruby这样的面向对象语言中,方法在类的上下文中被定义和继承:在一个对象上调用方法时,运行时会检测类以及它的超类中是否有此方法的定义,如果没有会导致异常发生。 +在如 C++、Java、C# 和 Ruby 这样的面向对象语言中,方法在类的上下文中被定义和继承:在一个对象上调用方法时,运行时会检测类以及它的超类中是否有此方法的定义,如果没有会导致异常发生。 -在Go中,这样的继承层次是完全没必要的:如果方法在此类型定义了,就可以调用它,和其他类型上是否存在这个方法没有关系。在这个意义上,Go具有更大的灵活性。 +在 Go 语言中,这样的继承层次是完全没必要的:如果方法在此类型定义了,就可以调用它,和其他类型上是否存在这个方法没有关系。在这个意义上,Go 具有更大的灵活性。 下面的模式就很好的说明了这个问题: ![](images/10.6.9_fig10.4.jpg?raw=true) -Go不需要一个显式的类定义,如同Java、C++、C#等那样,相反地,,“类”是通过提供一组作用于一个共同类型的方法集来隐式定义的。类型可以是结构体或者任何用户自定义类型。 +Go 不需要一个显式的类定义,如同 Java、C++、C# 等那样,相反地,“类”是通过提供一组作用于一个共同类型的方法集来隐式定义的。类型可以是结构体或者任何用户自定义类型。 -比如:我们想定义自己的Integer类型,并添加一些类似转换成字符串的方法,在Go中可以如下定义: +比如:我们想定义自己的 `Integer` 类型,并添加一些类似转换成字符串的方法,在 Go 中可以如下定义: ```go type Integer int @@ -643,35 +652,36 @@ func (i *Integer) String() string { } ``` -在Java或C#中,这个方法需要和类Integer的定义放在一起,在Ruby中可以直接在基本类型int上定义这个方法。 +在 Java 或 C# 中,这个方法需要和类 `Integer` 的定义放在一起,在 Ruby 中可以直接在基本类型 int 上定义这个方法。 -**总结:** +**总结** -在Go中,类型就是类(数据和关联的方法)。Go不知道类似OO语言的类继承的概念。继承有两个好处:代码复用和多态。 +在 Go 中,类型就是类(数据和关联的方法)。Go 不知道类似面向对象语言的类继承的概念。继承有两个好处:代码复用和多态。 -在Go中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫*组件编程*。 +在 Go 中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫 **组件编程**。 -许多开发者说相比于类继承,Go的接口提供了更强大、却更简单的多态行为。 +许多开发者说相比于类继承,Go 的接口提供了更强大、却更简单的多态行为。 -**备注**: +**备注** -如果真的需要更多OO的能力,看一下goop包(Go Object-Oriented Programming),它来自与Scott Pakin[(https://github.com/losalamos/goop]: 它给Go提供了JavaScript风格的对象(基于原型的对象),并且支持多重继承和类型独立分派,通过它可以实现你喜欢的其他编程语言里的一些结构。 +如果真的需要更多面向对象的能力,看一下 [`goop`](https://github.com/losalamos/goop) 包(Go Object-Oriented Programming),它由 Scott Pakin 编写: 它给 Go 提供了 JavaScript 风格的对象(基于原型的对象),并且支持多重继承和类型独立分派,通过它可以实现你喜欢的其他编程语言里的一些结构。 -问题 10.1: +**问题 10.1** -我们在某个类型的变量上使用点号调用一个方法:variable.method(),在使用Go以前,在哪儿碰到过OO的点号? +我们在某个类型的变量上使用点号调用一个方法:`variable.method()`,在使用 Go 以前,在哪儿碰到过面向对象的点号? -问题 10.2: +**问题 10.2** -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 { ... }`。 -d) 对于新定义的Integer,和b)中同样的问题 +d)对于新定义的 `Integer`,和 b)中同样的问题。 ## 链接 + - [目录](directory.md) -- 上一节:[10.5 匿名字段和内嵌结构体](10.5.md) -- 下一节:[10.7 TODO](10.7.md) +- 上一节:[匿名字段和内嵌结构体](10.5.md) +- 下一节:[类型的 String() 方法和格式化描述符](10.7.md) diff --git a/eBook/10.7.md b/eBook/10.7.md index 7361953..b70fcd9 100644 --- a/eBook/10.7.md +++ b/eBook/10.7.md @@ -1,10 +1,11 @@ -# 10.7 类型的String()方法和格式化描述符 +# 10.7 类型的 String() 方法和格式化描述符 -当定义一个了有很多方法的类型时,十之八九你会使用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.22 method_string.go: -Listing 10.22—method_string.go: ```go package main @@ -40,11 +41,11 @@ func (tn *TwoInts) String() string { two1 is: *main.TwoInts two1 is: &main.TwoInts{a:12, b:10} -当你广泛使用一个自定义类型时,最好为它定义String()方法。从上面的例子也可以看到,格式化描述符%T会给出类型的完全规格,%#v会给出实例的完整输出,包括它的字段(在程序自动生成Go代码时也很有用)。 +当你广泛使用一个自定义类型时,最好为它定义 `String()`方法。从上面的例子也可以看到,格式化描述符 `%T` 会给出类型的完全规格,`%#v` 会给出实例的完整输出,包括它的字段(在程序自动生成 `Go` 代码时也很有用)。 -**备注:** +**备注** -不要在String()方法里面调用涉及String()方法的方法,它会导致意料之外的错误,比如下面的例子,它导致了一个无限迭代调用(TT.String()调用fmt.Sprintf,而fmt.Sprintf又会反过来调用TT.String()...),很快就会导致内存溢出: +不要在 `String()` 方法里面调用涉及 `String()` 方法的方法,它会导致意料之外的错误,比如下面的例子,它导致了一个无限迭代调用(`TT.String()` 调用 `fmt.Sprintf`,而 `fmt.Sprintf` 又会反过来调用 `TT.String()`...),很快就会导致内存溢出: ```go type TT float64 @@ -55,11 +56,9 @@ func (t TT) String() string { t. String() ``` -**练习**: +**练习 10.12** type_string.go -练习 10.12:type_string.go - -给定结构体类型T: +给定结构体类型 T: ```go type T struct { @@ -69,42 +68,42 @@ type T struct { } ``` -值t: `t := &{7, -2.35, "abc\tdef"}`。给T定义String(),使得`fmt.Printf("%v\n", t)`输出:`7 / -2.350000 / "abc\tdef"` +值 `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 -为float64定义一个别名类型Celsius,并给它定义String(),它输出一个十进制数和°C表示的温度值。 +为 float64 定义一个别名类型 `Celsius`,并给它定义 `String()`,它输出一个十进制数和 °C 表示的温度值。 +**练习 10.14** days.go -练习 10.14:days.go +为 int 定义一个别名类型 `Day`,定义一个字符串数组它包含一周七天的名字,为类型 `Day` 定义 `String()` 方法,它输出星期几的名字。使用 `iota` 定义一个枚举常量用于表示一周的中每天(MO、TU...)。 -为int定义一个别名类型Day,定义一个字符串数组它包含一周七天的名字,为类型Day定义String()方法,它输出星期几的名字。使用iota定义一个枚举常量用于表示一周的中每天(MO,TU,...) +**练习 10.15** timezones.go -练习 10.15: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 / stack_struct.go - -实现栈(stack)数据结构: +实现栈(stack)数据结构: ![](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获取栈的最顶部一个非空(非零)的格子的值。现在可以理解为什么栈是一个后进先出(LIFO)的结构了吧。 +pop 获取栈的最顶部一个非空(非零)的格子的值。现在可以理解为什么栈是一个后进先出(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]`。 -(1). stack_arr.go:使用长度为4的int数据作为底层数据结构 -(2). stack_struct.go:使用包含一个索引和一个int数组的结构体作为底层数据结构,所以表示第一个空闲的位置。 -(3). 使用常量LIMIT代替上面表示元素个数的4重新实现上面的(1)和(2),是它们更具有一般性。 +1)stack_arr.go:使用长度为 4 的 int 数据作为底层数据结构。 +2)stack_struct.go:使用包含一个索引和一个 int 数组的结构体作为底层数据结构,所以表示第一个空闲的位置。 +3)使用常量 LIMIT 代替上面表示元素个数的 4 重新实现上面的 1)和 2),使它们更具有一般性。 ## 链接 + - [目录](directory.md) -- 上一节:[10.6 方法](10.6.md) -- 下一节:[10.8 垃圾回收和SetFinalizer](10.8.md) +- 上一节:[方法](10.6.md) +- 下一节:[垃圾回收和 SetFinalizer](10.8.md) diff --git a/eBook/10.8.md b/eBook/10.8.md index 21a8df0..d8e0829 100644 --- a/eBook/10.8.md +++ b/eBook/10.8.md @@ -1,29 +1,33 @@ -# 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` 进程在执行)。 如果想知道当前的内存状态,可以使用: ```go fmt.Printf(“%d\n”, runtime.MemStats.Alloc/1024) ``` -上面的程序会给出已分配内存的总量,单位是Kb。进一步的测量参考:http://golang.org/pkg/runtime/#MemStatsType。 -如果需要在一个对象obj被从内存移除前执行一些特殊操作,比如写到日志文件中,可以通过如下方式调用函数来实现: +上面的程序会给出已分配内存的总量,单位是 Kb。进一步的测量参考 [文档页面](http://golang.org/pkg/runtime/#MemStatsType)。 + +如果需要在一个对象 obj 被从内存移除前执行一些特殊操作,比如写到日志文件中,可以通过如下方式调用函数来实现: ```go runtime.SetFinalizer(obj, func(obj *typeObj)) ``` -func(obj *typeObj)需要一个typeObj类型的指针参数obj,特殊操作会在它上面执行。func也可以是一个匿名函数。 +`func(obj *typeObj)` 需要一个 `typeObj` 类型的指针参数 `obj`,特殊操作会在它上面执行。`func` 也可以是一个匿名函数。 -在对象被GC进程选中并从内存中移除以前,SetFinalizer都不会执行,即使程序正常结束或者发生错误。 +在对象被 GC 进程选中并从内存中移除以前,`SetFinalizer` 都不会执行,即使程序正常结束或者发生错误。 -练习 10.17:从练习10.16开始(它基于结构体实现了一个栈结构),为栈的实现(stack_struct.go)创建一个单独的包stack,并从main包main.stack.go中调用它。 +**练习 10.17** + +从练习 10.16 开始(它基于结构体实现了一个栈结构),为栈的实现(stack_struct.go)创建一个单独的包 `stack`,并从 `main` 包 `main.stack.go` 中调用它。 ## 链接 + - [目录](directory.md) -- 上一节:[10.7 类型的String()方法和格式化描述符](10.7.md) -- 下一节:[11.1 什么是接口](11.1.md) \ No newline at end of file +- 上一节:[类型的 String() 方法和格式化描述符](10.7.md) +- 下一章:[什么是接口?](11.1.md) \ No newline at end of file diff --git a/eBook/11.0.md b/eBook/11.0.md deleted file mode 100644 index e69de29..0000000 diff --git a/eBook/11.1.md b/eBook/11.1.md new file mode 100644 index 0000000..50dbd8f --- /dev/null +++ b/eBook/11.1.md @@ -0,0 +1 @@ +# 11.1 什么是接口? \ No newline at end of file diff --git a/eBook/directory.md b/eBook/directory.md index 5c1501e..e055756 100644 --- a/eBook/directory.md +++ b/eBook/directory.md @@ -94,6 +94,7 @@ - 10.7 [类型的 String() 方法和格式化描述符](10.7.md) - 10.8 [垃圾回收和 SetFinalizer](10.8.md) - 第11章:接口(interface)与反射(reflection) + - 11.1 [什么是接口?](11.1.md) ## 第三部分:Go 高级编程