通向Golang的捷径【10. 结构与方法】

Go 语言支持用户自定义类型, 即结构和类型假名, 结构可用于描述物体的诸多属性, 同时它也是一种组合类型, 当需要定义一个包含多个属性的类型时, 可使用结构, 而每个结构元素都有自己的类型和数值, 以实现数据的编组保存, 同时这些结构元素又可通过一个入口实现访问, 因此结构也是一种数值类型, 所以可使用 new 函数进行创建.

构成结构的数据元素又被称为数据域 (field), 每个数据域都包含了一个类型和一个名称, 在结构中, 每个数据域名都是唯一的. 同时结构的概念等同于软件工程中提到 ADT(抽象数据类型,Abstract Data Type), 也类似于 Cobol 语言的记录 (record), 同时在类 C 语言中, 也给出了结构 (struct) 类型, 在 OO 语言中, 结构将被视为一种无方法的轻量级类, 由于 Go 语言并未提供类的概念, 因此结构类型对于 Go 语言来说相当重要.

10.1 结构的定义

使用以下格式, 可实现结构的定义:
在这里插入图片描述
同时type T struct { a, b int } 也是合法定义, 可用于简单结构的定义, 结构的数据域会包含一个名称, 比如数据域名 1, 数据域名 2 等, 如果数据域名未在代码中使用, 可将其命名为 _.

数据域可以是任意类型, 甚至可以是结构 (参见 10.5 节), 函数或接口 (参见第 11 章), 由于结构是一个数值类型, 所以可将一个变量声明为结构类型, 之后可进行数据域的初始化, 如下:
在这里插入图片描述

new 函数的用法

使用 new 函数, 可为新的结构变量分配内存, 并返回一个指向存储区的指针, 即 var t *T = new(T), 如果处于包空间, 上述语句可能会分成不同行, 因为在入口处, 并不需要立即实现内存分配, 如:
在这里插入图片描述
同时也可简写为t := new(T), 变量 t 就是一个指向 T 的指针, 而数据域则包含了对应类型的默认值. 如果给出声明var t T, 也将为变量 t 分配内存, 同时自动实现数据域的默认值初始化, 同时 t 的类型为 T, 而 t 将被称为类型 T 的实例或对象.

例 10.1 pointer.go

在这里插入图片描述
在这里插入图片描述
使用 fmt.Println() 函数, 可实现结构的默认输出 (或使用%v), 使用点标记, 可为结构数据域赋值, 这与 OO 语言类似:
在这里插入图片描述
点标记在 Go 语言中, 被称为选取器 (selector), 为了访问结构的数据域, 无论它是结构变量, 还是结构指针, 都将使用相同的选取器:
在这里插入图片描述
使用以下语句, 可实现结构的初始化:
在这里插入图片描述
上述语句中, 后一种初始化是一种简写格式, 同时在创建结构的过程中, 也需要调用 new() 函数, 而且必须以数据域的次序, 来提供对应的数值, 在之后的示例中, 将给出基于数据域名的结构初始化, 因此new(Type) 和&Type{} 是等价表达式.

时间间隔是一种典型结构 (其中包含了起始时间和结束时间), 如下:
在这里插入图片描述
在 inter 初始化中, 数值的次序必须与数据域的次序相同, 同时 & 并不是强制符号, 在 inter2 初始化中, 给出了数据域名, 这时不再强制要求数值的次序, 在 inter3 初始化中, 有些数据域的初始化被忽略.

结构类型的命名, 以及结构的数据域也满足于可见性的规则, 参见 4.2 节, 所以一个导出的结构类型将包含不同属性的数据域, 即有些数据域被导出, 而另一些数据域未被导出.

下图给出了type Point struct { x, y int } 结构指针的存储格式和初始化:
在这里插入图片描述
在 pack1 包中,struct1 结构类型必须是唯一的, 同时它的完整类型名为 pack1.struct1, 在以下示例中, 给出了一个 Person 结构, 并包含了一个 upPerson 方法, 该方法又包含了一个 *Person 类型的形参 (传入的实参将被修改), 以及调用该方法的三种方式:

例 10.2 person.go

在这里插入图片描述
在这里插入图片描述
在结构指针的初始化中, 使用了pers2.lastName = ”Woodward”, 而没有采用 C++ 的-> 操作符, 因为 Go 语言可实现自动转换, 也可使用指针引用进行赋值, 如 (*pers2).lastName = ”Woodward”.

结构的存储格式

Go 语言的结构类型中, 可包含数据, 还可包含其他结构, 并能在内存的连续块中实现存储, 这可获得最佳的性能, 也不同于 Java 的引用类型 (引用对象和被引用对象将在内存的不同位置), 所以在这种情况下,Go 语言可使用指针, 如下:
在这里插入图片描述
在这里插入图片描述

递归结构

如果结构类型可引用自身, 这类结构变量可成为链表或二叉树的一个元素, 通常称为节点 (node), 在节点中,可包含后续节点的链接 (地址),su 是链表,le,ri 是二叉树对应的链接指针, 链表结构如下:
在这里插入图片描述
其中 data 将包含数据, 比如 float64, 而 su 指针将指向后续节点.
在这里插入图片描述
链表的首元素被称为表头 (head), 之后还将包含两个元素, 最后一个链表元素, 将被称为表尾 (tail), 它不会指向下一个节点, 而指向一个 nil 值 (空值), 当然实用的链接中, 将包含大量的数据节点, 同时链表的尺寸还可动态调整, 即增加或删除节点. 使用相同的方法, 还可定义一个双向链表,pr 域将指向前趋节点, 而 su 域将指向后续节点.
在这里插入图片描述

二叉树

在二叉树中, 大多数的节点都可链接到两个节点中, 一是左节点, 二是右节点, 还可基于这类形态进行扩展, 二叉树的顶层节点被称为根节点 (root), 未包含下层节点的节点被称为花节点 (leaves), 而花节点的 le 和 ri 指针将指向 nil 值, 因此一个节点就是一棵树, 如下:
在这里插入图片描述
在这里插入图片描述

结构的转换

Go 语言的转换必须遵守严格的规则, 如果定义了一个结构类型, 并为该结构类型配置了一个假名类型, 因此这两种类型实际上是一致的, 例 10.3 给出了类似的转换, 如果进行无法完成赋值或是转换时, 将产生一个编译错误.

例 10.3 struct_conversions.go

在这里插入图片描述

10.2 使用工厂方法, 创建结构变量

10.2.1 用于结构的工厂

Go 语言未提供 OO 语言的构造器, 但类似于构造器的工厂函数很容易实现, 为了简化应用, 通常会为类型, 定义一个工厂函数, 而函数名通常以 new 或 New 开头, 假定给出了以下的文件结构类型:
在这里插入图片描述
如果 File 是一个结构类型, 表达式new(File) 和&File{} 是等价的. 而大多数 OO 语言的初始化则相当笨拙
(File f = new File(…)), 在 Go 语言中, 可创建对应类型的工厂实例, 这与 OO 语言很相似. 如果 T 为结构类型, 使用size := unsafe.Sizeof(T{}), 可了解 T 类型变量所占用的内存大小.

如何强制使用工厂方法

利用可见性规则 (参见 4.2.1 节和 9.5 节), 可强制使用工厂方法, 并禁用 new 函数, 以便高效使用类型对象, 这类似于 OO 语言.
在这里插入图片描述
由于 m 是一个 matrix 类型, 因此可在另一个包中, 使用上述的工厂方法:
在这里插入图片描述

10.2.2 new 和 make 的用法 (map 和结构)

在 7.2.4 节的 slice 处理中, 给出两种构建函数的差异, 在 make() 函数中, 可使用 3 种类型:slice/map/channel(参见第 14 章), 在以下代码中, 给出 map 使用构建函数时, 可能出现的错误:

例 10.4 new_make.go(无法编译)

在这里插入图片描述
在这里插入图片描述
尝试使用 make() 创建一个结构变量, 并不造成太大的影响, 只是编译器会产生一个错误, 如果使用 new 创建一个 map, 并尝试进行赋值, 将会产生一个运行时错误,new(Foo) 将返回一个指向 nil 值的指针, 而 map 并未完成内存分配, 因此需要谨慎对待这类情况.

10.3 使用结构的自定义包

在 main.go 文件中将使用一个结构, 而这个结构将来自于 structPack 包 (它的存放目录名为 structpack):

例 10.5 structPack.go

在这里插入图片描述

例 10.6 main.go

在这里插入图片描述
在这里插入图片描述

10.4 包含标记 (tag) 的结构

结构的数据域除了名称和类型之外, 可包含一个标记 (tag), 也就是在数据域中, 附加一个字符串, 用于提供标识文本或是一些重要的标签, 而 tag 的内容, 无法在编程中使用, 只有 reflect 包可访问这些 tag, 参见第 11.10节, 该包提供了一些运行时状态下, 可用的属性和方法, 比如 reflect.TypeOf() 可使变量获取到正确的类型, 如果是一个结构类型, 可使用 Field 作为索引, 并能使用 Tag 属性.

例 10.7 struct_tag.go

在这里插入图片描述

10.5 在结构中嵌入匿名数据域

10.5.1 定义

有时在结构类型中, 需要包含一个或多个匿名 (嵌入) 数据域, 这类数据域无名称, 且包含类型, 同时匿名数据域也可包含一个结构, 即结构包含了一个嵌入式结构.

将匿名数据域与 OO 语言的继承, 进行一个大致的比较, 可发现两者较为相似, 基于嵌入和组合的功能, 可知Go 语言的组合功能优于继承功能 (OO 语言).

例 10.8 structs_anonymous_fields.go

在这里插入图片描述
在这里插入图片描述
可在匿名数据域保存数据, 并可使用数据类型名, 来访问匿名数据域的数据, 比如 outer.int, 因此在一个结构中, 每种数据类型只能有一个匿名数据域.

10.5.2 结构的嵌入

如上所述, 由于结构类型也是一种数值类型, 所以可以使用匿名数据域, 外层结构可直接访问内层结构的数据域, 如outer.in1, 即使嵌入 (内层) 结构来自于其他包, 内层结构只需简单插入或嵌入到外层结构中, 这就提供了一种简单的继承机制, 以便通过一种类型组合出其他的类型.

例 10.9 embedd_struct.go

在这里插入图片描述
在这里插入图片描述

10.5.3 名字冲突

如果两个数据域中出现相同的数据域名 (也可能是匿名数据域给出的相同类型名), 应当使用以下规则:
• 外层数据域名将覆盖内层数据域名, 这也提供了一种数据域名和方法名的覆盖机制.
• 如果在同一空间内出现同一数据域名, 同时程序中使用了该数据域名, 则会产生一个错误 ( 如果同名数
据域未在程序中使用, 则不会出现问题), 目前并无规则来解决这类二义性, 因此必须进行修改.
在这里插入图片描述
当程序使用 c.a 时, 将出现一个错误, 它无法确定应该使用 c.A.a 还是 c.B.a, 因此编译器将给出以下错误:
在这里插入图片描述
程序中可使用 d.b, 并且是一个 float32 类型, 而不会使用 B.b, 因为外层结构的数据域名, 可覆盖内层结构的同名数据域.

10.6 方法

10.6.1 介绍

首先结构很像是一种简单格式的类, 而 OO 编程者可能会询问,Go 语言中是否存在类方法? 在 Go 语言中包含了同名机制, 所以可实现简单的方法, 即某些类型的变量可包含一个方法 (也就是函数), 这类方法将被称为接收器 (receiver), 因此方法是一种特殊的函数.

接收器可支持大多数类型, 不光结构类型, 其他的类型也可包含方法, 甚至是函数类型以及 int,bool,string 或数组的假名类型, 但接收器不支持接口 (interface) 类型 (参见第 11 章), 因为接口类型是一个抽象定义, 并会在一个方法中实现接口类型, 如果强行在接收器中使用接口类型, 将产生一个编译器错误,invalid receiver type…,同时接收器也不支持指针类型, 但能够支持类型 (接收器支持的类型) 指针.

在 Go 语言中, 组合使用 (结构) 类型和类型方法, 等同于 OO 语言的类, 两者之间最大的差异在于, 类型定义和类型方法无法放置在一起, 它们可能存在于不同的源码文件中, 只要它们包含在同一个包中.

特定类型 T(或 *T) 的所有方法集合, 并称为 T(或 *T) 的方法集, 首先方法也是一种函数, 并且不存在方法的重载, 对于一个特定类型来说, 一个方法需给出一个方法名, 基于接收器的不同类型, 方法可实现重载, 即同名方法可出现在接收器的不同类型中, 同时还可包含在同一个包中, 如下:
在这里插入图片描述
因此有些假名类型不能包含方法, 而方法的定义格式如下:
在这里插入图片描述
在 func 和方法名之间, 必须指定一个接收器, 即 recv. 如果 recv 是一个接收器实例名, 而方法名为 Method1,之后方法的调用将使用传统的点标记,recv.Method1(). 如果 recv 是一个指针, 它可实现自动销毁, 如果方法无须使用 recv, 则可使用下划线 _ 忽略它, 如下:
在这里插入图片描述
recv 类似于 OO 语言的 this 指针或自引用, 但在 Go 语言中, 并无对应的关键字, 如果需要为接收器变量, 配置一个名称或自我标识, 可以自由选择, 以下示例将给出结构类型的一个简单方法:

例 10.10 method.go

在这里插入图片描述
以下示例将给出非结构类型的一个简单方法:

例 10.11 method2.go

在这里插入图片描述
在这里插入图片描述
类型和类型方法必须在同一个包中定义, 但是不能为 int,float 或相似类型 (即基础类型), 定义一些方法, 如果尝试为 int 类型, 定义一些方法, 将产生编译器错误:
在这里插入图片描述
这里给出一个变通的方法, 首先为基础类型 (int,float,…), 定义一个假名类型, 之后可为假名类型, 定义一些方法, 这与结构中使用匿名类型非常相似, 所以在这种情况下, 只有给出假名类型的方法才是有效的.

例 10.12 method_on_time.go

在这里插入图片描述

10.6.2 函数与方法的区别

函数会将变量视为实参, 如Function1(recv), 需基于变量的不同类型, 才可调用方法, 如recv.Method1(), 方法可修改接收器的变量 (基于指针引用的变量), 这与函数的用法相似, 函数也可修改引用传入 (指针) 的实参变量.

不要忘记在 Method1 后, 添加圆括号 (), 否则将出现一个编译器错误: method recv.Method1 is not an expression, must be called !!

接收器必须显式给出一个名称 (也可使用 _ 忽略), 而该名称也必须在应用程序中使用. 接收器类型也称为接收器的基本类型, 该基本类型必须在同一个包中进行声明, 也就是与所有方法的定义, 在同一个包中.

在 Go 语言中, 方法所附加的接收器类型, 不能在方法内部进行定义, 这与类的概念很相似, 同时也提供了更多的灵活性, 也就是通过接收器, 建立方法和类型的关联. 方法不能加入数据类型的定义 (比如结构), 因为需要基于不同的类型, 提供不同的方法, 因此可知数据描述 (类型) 和数据处理 (方法) 是独立的.

10.6.3 指针或数值的接收

recv 是一个常用的指向接收器类型的指针, 这也是出于性能的考虑, 因为无须创建一个实例副本 (也就是值传递), 当接收器类型是一个结构, 它对性能的影响更为突出.

如果需要方法可修改接收器指向的数据, 则应当基于一个指针类型来定义方法, 否则基于一个数值类型, 来定义方法, 会更加清晰.

在以下示例中,change() 可接收一个指向 B 的指针, 并能修改 B 的内部数据域, 而 write() 只能输出 B 的内容,同时接收 B 的数值副本, 在 main() 函数中,Go 代码会依照编程者的意图而执行, 而编程者无须考虑该提供何种方法 (基于指针类型, 或基于非指针类型), 因为 Go 可自动实现匹配, 比如 b1 是一个数值类型, 而 b2 是一个指针, 所以 Go 可自动匹配方法的调用.

例 10.13 pointer_value.go

在这里插入图片描述
即使在 write() 函数中, 需要对接收器变量进行修改, 也不会影响到 b 的原有值. 从方法中可知, 并未要求接收器提供一个指针, 在以下示例中, 需要使用 Point3 类型值进行计算:
在这里插入图片描述
上述代码的开销略大, 因为传递给方法的 Point3 类型, 需要创建一个副本, 因此这种情况下, 应将传入一个类型指针, 而传入的指针可实现自动销毁. 首先将 p3 定义成一个指针, 如 p3 := &Point3{ 3, 4, 5 }, 之后
可使用 (*p3).Abs() 替换 p3.Abs(), 在例 10.11(method1.go) 中, 如果方法包含了接收器类型 *TwoInts, 比如 AddThem(), 则可使用 TwoInts 类型值的存储地址, 从而实现自动化的间接引用 (即指针特性), 并可使
用(&two2).AddThem() 替换two2.AddThem(). 基于类型的数值或指针, 都可调用方法, 而有些方法将附带类型变量, 另一些方法则会附带类型指针, 这在调用过程中并不会造成问题, 如果一个 T 类型的方法 Meth() 使用了 *T 类型, 而 t 为 T 类型变量, 当调用为 t.Meth(), 将自动转换为(&t).Meth().

基于指针或数值的方法, 都可实现指针或非指针的调用, 因为存在 Go 语言实现的自动转换, 以下示例实现了之前的描述,List 类型给出了一个方法 Len(), 在 Len() 方法调用时, 需提供一个类型数值, 同时还给出了另一个方法 Append(), 在 Append() 方法调用时, 需提供一个 List 类型的指针, 但是我们看到, 两个方法在调用时,都可传入类型数值或类型指针.

例 10.14 methodset1.go

在这里插入图片描述

10.6.4 方法和无法导出的数据域

在 person2.go 文件中, 实现了 person 包, 同时 Person 类型可被导出, 但 Person 类型的数据域无法导出! 例如use_person2.go 文件的 p.firstname 语句是一个错误, 因此应当如何修改, 以使其他文件可读取到 Person 类型的名称 (数据域).

在 OO 语言中实现了类似的技术, 即实现 get-和 set-方法, 即获取方法和设定方法, 从而对私有对象实现完全的封闭,

例 10.15 person2.go

在这里插入图片描述
在这里插入图片描述

例 10.16 use_person2.go

在这里插入图片描述

并发访问

在同一时刻下, 有两个或多个线程需要修改, 结构变量的数据域, 为了保证并发访问的安全性, 需要使用 sync包的方法 (参见 9.3 节), 在 14.17 节中, 还将使用并发协程和并发通道, 来处理并发安全性.

10.6.5 嵌入式类型的方法和继承

在结构中嵌入匿名类型时, 该类型也可实现一些方法, 而事实上, 外层类型可继承这些方法, 即子类型的一些操作可在对应的父类型上完成, 这类机制可简单模拟,OO 语言的继承功能和子类功能, 这也与 Ruby 语言很相似.

假定给出一个 Engine 类型的接口, 在 Car 结构类型中, 包含了 Engine 类型的匿名数据域,
在这里插入图片描述
之后可在代码中, 实现以下的构造:
在这里插入图片描述
在以下示例中, 嵌入式结构类型的方法可基于嵌入式类型, 进行直接调用:

例 10.17 method3.go

在这里插入图片描述
在这里插入图片描述
将一种类型的数据域或方法, 嵌入到另一种类型中, 与匿名数据域相关的方法, 将变为外围类型的方法, 同时该类型的方法, 只能基于该类型变量而进行调用, 不能基于嵌入类型的变量, 而进行调用.

方法的覆盖也可实现, 嵌入式类型中包含的同名方法, 将被覆盖:

例 10.18 method4.go

在这里插入图片描述
由于结构类型中, 嵌入了多个匿名类型, 因此将给出一个简单的多重继承, 比如type Child struct { Father; Mother },10.6.7 节将给出更详细的描述, 来自同一个包的嵌入式结构, 可嵌入到其他结构中, 同时嵌入式结构可使用外层结构的所有数据域和方法.

10.6.6 类型的功能嵌入

• 聚合 (或组合): 将实现所需功能的类型, 作为一个命名数据域, 加入到上层类型.
• 嵌入: 在上层类型中, 添加嵌入式 (匿名) 类型.

以下将使用实例, 来说明上述机制, 假定存在一个 Customer 类型, 之后该类型需要增加一个日志功能 ( 由 Log类型实现), 它可保存所有出现的消息, 如果需要将日志功能, 添加到所有的类型中, 可实现一个 Log 类型, 并将其作为数据域, 加入到类型中, 同时能使用 Log() 方法, 返回日志的一个引用, 10.7 节将提供 String() 功能, 下例将实现聚合机制:

例 10.19 embed_func1.go

在这里插入图片描述
在这里插入图片描述
下例将实现嵌入机制:

例 10.20 embed_func2.go

在这里插入图片描述
在这里插入图片描述
嵌入式类型无须是一个指针,Customer 类型也需给出 Add 方法, 可使用 Log 类型的 Add 方法, 但Customer类型中, 提供了字符串处理的方法, 即 log String(), 如果嵌入式类型被添加到上层类型中, 嵌入式类型的方法,可被上层类型直接使用. 使用这类机制, 可实现一些小型的复用类型, 以便组合到其他的类型中.

10.6.7 多重继承

多重继承是指一个类可继承多个父类的行为, 在经典的 OO 语言中, 并未提供这类功能 ( 但 C++ 和 Python是例外), 因为基于类的继承机制, 将会提高编译器的复杂度, 所以在 Go 语言中, 能简单将所有类型的功能, 组合到一个上层类型中.

假定存在一个 CameraPhone 类型, 而 Phone 类型给出了 Call() 方法,Camera 类型给出了TakeAPicture() 方法, 同时 Phone 类型和 Camera 类型将嵌入到 CameraPhone 类型中, 以实现功能更强大的类型, 如下:

例 10.21 mult_inheritance.go

在这里插入图片描述

10.6.8 通用方法和方法命名

在编程过程中, 总有一些基础操作一再出现, 比如打开, 关闭, 读取, 写入, 排序等操作, 而这些操作还存在一些通用的意义, 比如打开操作的目标, 可能是文件, 网络链接, 数据库等, 当然这些目标的操作细节并不相同, 但具备通用的意义, 在 Go 语言中, 可通过接口 (参见第 11 章), 尽可能利用标准库, 而标准库也给出了标准命名的通用方法, 比如 Open(), Read(), Write() 等, 当我们在编写 Go 代码时, 也应当注意这类风格, 为自定义的方法, 设定一个标准名称, 以构建出一组标准方法, 同时可保证 Go 代码的一致性和可读性, 比如需要一个可实现字符串转换的方法, 应当命名为 String() 而不是 ToString(), 参见 10.7 节.

10.6.9 基于类型和方法, 进行 Go 与其他语言的比较

OO 语言 (比如 C++,Java,C#,Ruby 等) 的方法, 基于类空间和继承空间所定义的, 当对象调用一个方法时,必须清楚, 方法是类定义还是基类定义, 如果无法区分, 将出现一个异常.

在 Go 语言中, 并不需要一个继承层次, 如果类型中定义了一个方法, 将独立于其他类型所定义的方法, 因此可提供一个较大的灵活性, 如下:
在这里插入图片描述
Go 语言无须给出显式的类定义 (相似于 C++,C#,Java 的类定义),class 关键字也被隐藏, 同时将基于一个公共类型, 提供一组方法集合, 而公共类型可以是结构类型, 或是用户自定义类型. 比如可自定义一个 Integer 类型 (假名类型), 并为该类型, 添加一些方法, 比如字符串转换,
在这里插入图片描述
在 Java 或 C# 中, 将在一个类 Integer 中, 提供所需的类型和函数, 而 Ruby 中, 需要为基础类型 int, 编写对应的方法. 因此在 Go 语言中, 类型可视为简单类 (可提供数据, 以及关联所需的方法), 同时无法实现 OO 语言的类继承, 而类继承可提供两种重要优势, 即代码重用和多态性.

在 Go 语言中, 通过组合与委托(封装), 可实现代码重用, 通过接口可实现多态性, 所以 Go 语言开发有时被称为组件编程, 而大多数开发者认为 Go 接口能提供更强大的功能, 以及实现更简单的多态性 ( 相对于类继承).

如果你需要更多的 OO 特性, 可选择 goop(Go Object-Oriented Programming) 包, 并由 Scott Pakin
发布 (https://github.com/losalamos/goop), 它可为 Go 语言提供 JavaScript 风格的对象 ( 基于 prototype 的对象), 并支持多重继承和基于类型的派发, 因此你所熟悉的大多数特性 ( 来自于其他编程语言) 都能继续使用.

10.7 特定类型所需的 String 方法和格式标识符

当定义一个包含大量方法的类型时, 可创建一个自定义的字符串输出方法 String(), 以提供一个易于理解的打印输出, 因此为特定类型所定义的 String() 方法, 将被 fmt.Printf() 使用, 并会生成一个默认输出, 同时需给出一个格式标识符%v, 而 fmt.Print() 和 fmt.Println() 也将自动使用 String() 方法.

例 10.22 method_string.go

在这里插入图片描述
在使用自定义类型时, 创建一个 String() 方法, 可使处理更加简单, 其中的格式标识符%T, 用于输出完整的类型定义,%#v 用于给出完整实例的信息, 其中包含了数据域, 这在自动生成 Go 代码时, 相当有价值.

因此 String() 的定义不应出现二义性, 在以下代码中, 将出现一个无穷递归, 即 TT.String() 调用 fmt.Sprintf, 而 fmt.Sprintf 又将调用 TT.String(), 这将陷入一个死循环, 因此很快会出现一个内存不足的错误:
在这里插入图片描述

10.8 垃圾收集和 SetFinalizer

Go 开发者无法为应用程序中不再使用的变量和结构, 编写释放内存的代码, 因此在 Go 运行时状态下, 将存在一个单独进程, 即垃圾收集器, 它将与应用程序同时启动, 并会检索不再使用的变量, 并释放这些变量的内存,通过 runtime 包, 该进程可实现对应的功能.

调用 runtime.GC() 函数, 可显式启动垃圾收集操作, 并能避免竞争条件的出现, 比如当内存资源不足时, 启动垃圾收集操作, 可使大量内存立即释放, 而此时的应用程序将经历短暂的性能下降 (由于垃圾收集进程).
在这里插入图片描述
使用上述语句, 可了解当前内存的状态, 应用程序所分配的内存量, 将以 Kb 计算, 参考页面http://golang.org/pkg/runtime/#MemStatsType, 可进行更多的测试.

在 obj 对象从内存中销毁之前, 可能需要执行一些特殊任务, 这时可使用以下语句:
在这里插入图片描述
其中func(obj *typeObj) 是一个函数, 可传入一个 obj 类型的指针实参, 用于进行销毁前的清理任务, 同时该函数也可以是一个匿名函数. 当应用程序正常结束或出现错误时, SetFinalizer 并不会执行, 只有对象将被垃圾收集进程销毁之前, 它才会执行.

在这里插入图片描述