翻译: Effective Go (5)
数据
new 分配内存
Go 有两个分配新内存的语法,内置函数 new 和 make。它们执行不同的操作并应用于不同的类型,这虽然有点困惑,但是规则很简单。我们先来讨论 new。这是一个内置的函数,可以申请内存空间,但与其他一些语言中的同名函数不同的是,它不对内存进行初始化,只是将内存给予零值。也就是说,new(T)为类型 T 分配零值内存,并返回它的地址,即*T 类型的值。用 Go 的术语来说,它返回一个指针,指向新分配 T 类型的零值。
由于 new 返回的内存是零值化的,所以在设计数据结构时,每个类型的零值可以不需要初始化函数就可以使用。这意味着数据结构的用户可以用 new 创建一个数据结构,然后直接开始工作。例如,bytes.Buffer 的文档中指出:Buffer 的零值是一个准备使用的空缓冲区同样,syn.Mutex 也没有一个显示的构造函数或 init 方法,想法,syn.Mutex 的零值被定义为一个未锁定的锁。
零值可以被用来中间过度,考虑下面的类型声明:
SyncedBuffer 类型的值可以通过分配或声明进行使用。下面,p 和 v 不需要额外的处理就会同时正确的工作:
构造函数和复合字面量
有时候零值并不够用,所以需要一个初始化构造函数,以 os 包中的一段代码为例:
这段代码非常啰嗦,我们可以用一个复合字面量来简化它,复合字面量是一个表达式,每次求值会创建一个新的实例。
注意,与 C 语言不同的是,Go 返回局部变量的地址是完全可以的。在函数返回后,该变量关联的内存会继续存在。事实上,每次获取一个复合字面量的地址时,都会分配一个新的实例, 因此我们可以将上面的最后两行代码进行合并。
复合字面量的字段是按顺序排列的,并且必须全部列出。但是,通过字段:值进行明确标注的时候,初始化可以以任意顺序出现,缺失的字段作为零值。因此,我们可以这么写。
在少数情况下,如果一个复合字面量不包含任何字段,那么它会创建该类型的零值。new(File)和 &File{}是等价的。
复合字面量也可以创建数组,切片(slices),映射(map),字段自动变为索引或者键(key)。在这些例子中,无论 Enone,Eio,Einval 的值是什么,只要它们的字段是不同的,都可以进行初始化。
make 分配内存
回到内存分配上。内建函数 make(T,args)的作用与 new(T)不同。它值创建切片,映射和管道,并返回一个类型 T(不是*T)初始化后的(不是零值)的值。不同的原因是,这三种类型本质上为引用数据类型,在使用前必须进行初始化。例如,切片是一个具有三项内容的描述符,包含指向数据(在数组内部)的指针,长度和容量。在这些项目被初始化之前,切片是 nil。对于切片,映射,管道,make 初始化了内部数据结构,并准备好了将要使用的值。例如,
这行代码分配了一个具有 100 个 int 的数组,然后创建一个长度为 10,容量为 100 的并指向前 10 个元素的切片结构。(make 切片时,可以省略容量,更多信息可以看切片这一节)。与 make 相反,new([]int)返回一个指向新分配的,零值的切片指针,即指向 nil 的指针。
记住,make 只适用于映射,切片和管道,并不返回指针。要获得一个显示的指针,请用 new 分配或明确的获取变量的地址。
数组
数组在规划详细的内存布局时候很有用,又是还能避免进行内存分配。但主要的是,内存是切片的构件。这是下一节的主题,不过还是要说几句。
下面是数组在 Go 和 C 中主要的不同点,在 Go 的数组中,
数组是值,将一个数组赋给另一个数组会复制所有的元素
当传递一个数组给函数时,函数会收到一个数组的副本,而不是一个指针
数组的大小是其类型的一部分。类型[10]int 和[20]int 是不同的
数组为值的属性很有用,但代价高昂;若需要 C 一样的高效,可以传递一个指向数组的指针。
但这不是 Go 的风格,通常会使用切片(slices)
切片(slices)
切片封装了数组,为序列化数据提供了一个更通用,强大和方便的接口。除了具有显式唯独的项目,如矩阵变换等,Go 中的大多数数组编程都是用切片而不是简单的数组来完成的。
切片保存了对底层数组的引用,如果你把一个切片赋给另一个切片,那么两个切片都会指向同一个数组。如果一个函数接受一个切片,那么它对切片中元素的修改对调用方也是可见的,这就类似传递一个指向底层数组的指针。因此,Read 函数可以接受一个切片参数,而不是一个指针和一个计数 count。切片中的长度设置了一个读取数据的上限,下面是 os 包中的 File 类型 Read 方法的签名。
该方法返回读取的字节数和一个错误值(如果有的话)。要从一个较大的缓冲区 buf 读取前 32 个字节,只需对其切片即可。
这样的做法很普遍,也非常高效。实际上,抛开效率不谈,下面的代码也能读取缓冲区的前 32 个字节。
切片的长度可以改变,只要它仍然适合底层的数组,只需将它分配给一个自身即可。切片的容量可以通过内置函数 cap 访问,它表示了切片的最大长度。又一个函数可以将数据追加到切片中,如果数据超过了容量,切片会被重新分配。所得到的切片会被返回。该函数利用 len 和 cap 在应用到 nil 切片是合法的事实,并返回 0.
最终我们必须返回 slice,因为虽然 append 可以修改 slice 的元素,但 slice 本身(保存指针,长度和容量的运行时数据结构)是通过值传递的。
向切片追加数据的想法非常有用,所以有了内置的 append 函数。不过要理解这个函数的设计,需要更多的信息,所以稍后再进行讨论。
二维切片
Go 的数组和切片是一维的,要创建一个相当于二维数组或二维切片的数组或切片,需要定义一个数组的数组或切片的切片,就像这样:
由于切片的长度是可变的,所以可以让每个内部的切片有不同的长度。这可能是一种常见的情况,就像我们的 LinesOfText 例子:每一行都有一个独立的长度。
有时需要分配一个二维的切片,比如在处理按行扫描像素时。有两种方式可以实现。一种是独立分配每个切片;另一种是分配一个单独的数组,并将单个切片指向其中。使用哪种方法取决于你的应用。如果切片可能会增长或缩小,就应该独立分配,以避免覆盖下一行;如果不是,用单个分配来构造会更有效率。下面是两种方式的大概代码,仅供参考。首先,一次一行:
然后是一次分配,按行切片
映射
映射是一种方便且强大的内建数据结构,可以将一种类型的值(key)与另一种类型的值(value)关联起来。键(key)可以是定义了相等运算符的任意类型,例如整数,浮点数和复数,字符串,指针,接口(只要动态类型支持相等),结构和数组。切片不能用作键,因为它没有定义相等性。和切片一样,映射保存对底层数据结构的引用,如果你把一个映射传递给一个函数并修改其内容,那么此修改对调用方也是可见的。
映射可以用通常的复合字面量语法进行构建,其 key-value 键值对使用冒号分隔,因此,在初始化过程中很容易创建映射。
映射的赋值和取值在语法上看起来就像对数组和切片差不多,只是索引不需要整数。
试图用不存在的键来获取映射的值将返回类型的零值。例如,如果映射是整数,那么查找一个不存在的键将返回 0.可以用值为布尔类型的映射实现集合。将映射值设为 true,将键设为要放入的值,然后通过简单的索引进行测试。
有时需要区分缺失的值和零值。是有一个“UTC”的 value 为 0,还是它根本不在映射中才返回 0?这时可以用一种多重赋值的方式来区分。
出于明显的原因,这被称为“逗号 ok”用法,在这个例子中,如果 tz 存在,秒数会被设置为 true,如果不存在秒数会被设置为 0,同时 ok 则为 false。这里有一个函数,将这个用法和错误报告结合在了一起。
如果只是测试映射中的值是否存在,而不在乎实际值,可以用空白标识符(_)来代替通常的变量。
要删除映射的条目,可以使用内建函数 delete,参数为映射和要删除的键。就算映射没有键也可以安全的进行删除。
打印输出(printing)
Go 的格式化打印使用了类似 C 语言 printf 系列风格,但是更丰富和通用。这些函数在 fmt 包中,并首字母大写:fmt.Printf, fmt.Fprintf, fmt.Sprintf 等。字符串函数(Stringf 等)返回一个字符串,而不是填入提供的缓冲区。
你不需要提供一个格式化的字符,对于每个 Printf, Fprintf 和 Springf 都有另外一对函数,例如 Print 和 Println,这些函数不接受格式字符串,而是为每个参数生成一个默认格式。Println 会在参数之间插入一个空格,并在输出中附加一个新行,而 Print 只在两边的操作符都不是字符串的情况下添加空格。在这个例子中,每一行都会产生相同的输出。
格式化打印函数 fmt.Fprint 一类接受任何实现了 io.Writer 接口的对象作为第一个参数;变量 os.Stdout 和 os.Stderr 都是熟知的例子。
和 C 不一样,像 %d 这样的数字格式不接受签名或大小标识;;相反,打印函数使用参数的类型来决定这些属性。
会打印
如果你只是想要默认的转换,例如整数的十进制转换,可以使用通用格式 %v(表示 value 的意思);结果和 Print 和 PrintLn 所产生的结果完全一样。此外,该格式还可以打印任何值,甚至是数组,切片,结构和映射。下面是上一节定义的时区映射的打印语句。
会输出
对于映射来说,Printf 类的函数会按照键进行词法排序。当打印一个结构时,修改后的格式 % +v 会在字段上标注它们的名称,而对于任何值,则用另一种格式 %#v 以 Go 的语法打印出该值。
输出
(注意 &号)当遇到字符串类型或[]byte 类型的值时,可以用 %q 获取带引号的字符串。%#q 会尽可能使用反引号。(%q 适用于整数和 rune,产生单引号的字符常量)此外,%x 适用于字符串,字节数组和字节切片以及整数,去生成长十六进制字符,带有空格格式的 %x 还会在字节中插入空格。另一个实用的是 %T,它会打印值的类型。
fmt.Printf("%T\n", timeZone)
输出
map[string]int
如果要自定义类型的默认格式,只需要定义该类型的String() string
方法。对于简单的类型 T,可能看起来就像这样:
会按格式这么输出
7/-2.35/"abc\tdef"
(如果你需要打印类型 T 的值以及指向 T 的指针,则 String 的接受者必须为值类型;这个例子使用了一个指针,因为它对于结构类型更加有效且更惯用。请参见下面的指针 vs 值接受者以获得更多的信息)
我们的 string 方法可以调用 Sprintf,因为打印程序是完全可以重入的,并且可以用这种方式封装。关于此方法,又一个重要的细节要理解:不要通过 Sprintf 来构造 String 方法,因为会造成无限递归 String。如果 Sprintf 调用试图将接收方直接打印为字符串,则可能会发生这种情况,,而 String 又会在此调用该方法。如本例所示,是一个常见且容易犯的错误。
不过这很容易修复:将参数变为基本的字符串类型,它就没有 String 方法了。
在初始化部分,我们可以看到另一种避免无限递归的方式。
另一种打印技巧是传递一个打印函数给另一个打印函数,Printf 使用 interface{}作为最后一个参数,以指定可以在格式之后显示任意数量的参数(任意类型)。
func Printf(format string, v ...interface{}) (n int, err error) {
在函数 Printf 中,v 的作用类似于[]interface{}类型的变量,但如果将其传递给另一个可变参数的函数,则其作用类似于常规的参数列表。这是我们上面使用的功能 log.Println 的实现。它将其参数直接传递给 fmt.Sprintln 进行实际的格式化。
v 后面的...以告诉编译器将 v 视为参数列表,否则它将 v 作为单个 slice 参数进行传递。除了这里介绍的内容外,还有更多有关 print 的内容,有关详细信息可以参见 fmt 包的 godoc 文档。
顺便提一下,一个...参数可以是具体的类型,例如...int 可以用于选择最小整数列表的 mini 函数。
附加(append)
现在我们还没解释内建函数 append 的设计。append 的签名与上面自定义的 Append 函数不同,大致来说,它是这样的:
func append(slice []T, elements ...T) []T
其中 T 是给任何给定类型的占位符,实际上你无法在 Go 中编写一个由调用者确定类型 T 的函数。这就是内置 append 的原因:它需要编译器的支持。
append 操作是将元素附加到切片的末尾并返回结果。因为底层数组会改变,所以需要返回结果。这是一个简单的例子:
打印[1 2 3 4 5 6]。所以,append 有点像 Printf,接受任意数量的参数。
但是如果我们要对一个切片执行 append 另一个切片该怎么办?很简单:用...进行传参。下面的代码会和上面的输出一样:
如果没有...,将会因为类型错误而无法变异,因为 y 不是 int 类型
版权声明: 本文为 InfoQ 作者【申屠鹏会】的原创文章。
原文链接:【http://xie.infoq.cn/article/df557d5227ca5e7947f6af5b2】。文章转载请联系作者。
评论