5.8. 数据
5.8. 数据
5.8.1. new()分配
Go 有两个分配原语,new() 和 make() 。它们做法不同,也用作不同类型上。有点乱但规则简单。我们先谈谈 new() 。它是个内部函数,本质上和其它语言的同类一样:new(T)分配一块清零的存储空间给类型 T 的新项并返回其地址,一个类型 *T 的值。 用 Go 的术语,它返回一个类型 T 的新分配的零值。
因为 new() 返回的内存清零, 可以用来安排使用零值的物件而不需再初始化。亦即数据结构的用户可以直接用 new() 生成一个并马上使用。例如, bytes.Buffer 的文档指出“零值的 Buffer 为空并可用”。同http://code.google.com/p/ac-me/ 61理,sync.Mutex 没有明确的架构函数或 init 方法。 而是,一个sync.Mutex 的零值定义为开锁的互斥。
零值有用,这个特性可以顺延。考虑下面的声明。
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
类型 SyncBuffer 的值在分配或者声明后立即可用。下例,p 和 v 无需多余的安排已可以正确使用了。
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
5.8.2. 构造和结构初始化
有时零值不够好,有必要使用一个初始化架构函数,如下面从 os 包引出的例子。
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
这里有很多注模。我们可用组合字面简化之,它是个每次求值即生成新实例的表达式。
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}
注意返回局部变量的地址是完全 OK 的;变量对应的存储空间在函数返回后仍然存在。实际上,取一个组合字面的地址使每次它求值时都生成一个新实例,因此我们可以把最后两行合起来。
return &File{fd, name, nil, 0}
组合字面的域必须按顺序给出并全部出现。可是,明确的用域:值对儿标记元素,初始化可用任意顺序,未出现的对应着零值。所以我们可以讲
return &File{fd: fd, name: name}
特别的,如果一个组合字面一个域也没有,它生成此类型的零值。表达式 new(File) 和 &File{} 是等价的。
组合字面也可以生成数组、切片和映射,其域为合适的下标或映射键。下例中,无论 Enone Eio 和 Einval 是什么值都可以,只要它们是不同的。
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
5.8.3. make()分配
回到分配。内部函数 make(T, args) 的服务目的和 new(T) 不同。它只生成切片,映射和信道,并返回一个初始化的(不是零)的,type T的,不是 *T 的值。这种区分的原因是,这三种类型,揭开盖子,底下引用的数据结构必须在用前初始化。比如切片是一个三项的描述符,包含数据指针(数组内),长度,和容量;在这些项初始化前,切片为 nil 。对于切片、映射和信道,make 初始化内部数据结构,并准备要用的值。例如,
make([]int, 10, 100)
分配一个 100 个整数的数组,然后生成一个切片结构,长度为10,容量是100的指向此数组的首10项。(生成切片时,容量可以不写;详见切片一节。)对应的,new([]int) 返回一个新分配的,清零的切片结构,亦即,一个 nil 切片值的指针。
下面的例子展示了 new() 和 make() 的不同。
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v []int = make([]int, 100) // v now refers to a new array of 100 ints
// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:
v := make([]int, 100)
记住 make() 只用于映射、切片和信道,不返回指针。要明确的得到指针用 new() 分配。
5.8.4. 数组
数组用于安排详细的内存布局,还有助于避免分配,但其主要作为切片的构件,即下节的主题。这里先讲几句打个底儿。
Go 和 C 的数组的主要不同在于:
- 数组为值。数组赋值给另一数组拷贝其全部元素。
- 特别是,如果你传递数组给一个函数,它受到此数组的拷贝,不是指针。
- 数组的尺寸是其类型的一部分。[10]int 和 [20]int 是完全不同的类型。
值的属性可用但昂贵;如你所需的是类似 C 的行为和效率,你可以传递一个指针给数组。
func Sum(a *[3]float) (sum float) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
即便如此也不是地道的 Go 风格。切片才是。
5.8.5. Slices 切片
切片包装数组,给数据系列一个通用、强力、方便的界面。除了像变换矩阵那种要求明确尺寸的情况,绝大部分的数组编程在 Go 里使用切片、而不是简单的数组。
切片是引用类型,即如果赋值切片给另一个切片,它们都指向同一底层数组。例如,如果某函数取切片参量,对其元素的改动会显现在调用者中,类似于传递一个底层数组的指针。因此 Read 函数可以接受切片参量,而不需指针和计数;切片的长度决定了可读数据的上限。这里是 os 包的 File 型的 Read 方法的签名:
func (file *File) Read(buf []byte) (n int, err os.Error)
此方法返回读入字节数和可能的错误值。要读入一个大的缓冲 b 的首32字节, 切片(动词)缓冲。
n, err := f.Read(buf[0:32])
这种切片常用且高效。实际上,先不管效率,此片段也可读缓冲的首32字节。
var n int
var err os.Error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i:i+1]) // Read one byte.
if nbytes == 0 || e != nil {
err = e
break
}
n += nbytes
}
只要还在底层数组的限制内,切片的长度可以改变,只需赋值自己。切片的容量,可用内部函数 cap 取得,给出此切片可用的最大长度。下面的函数给切片添值。如果数据超过容量,切片重新分配,返回结果切片。此函数利用了 len 和 cap 对 nil 切片合法、返回0的事实。
func Append(slice, data[]byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2)
// Copy data (could use bytes.Copy()).
for i, c := range slice {
newSlice[i] = c
}
slice = newSlice
}
slice = slice[0:l+len(data)]
for i, c := range data {
slice[l+i] = c
}
return slice
}
我们必须返回切片,因为尽管 Append 可以改变 slice 的元素, 切片自身(持有指针、长度和容量的运行态数据结构)是值传递的。添加切片的主意很有用,因此由内置函数 append 实现。要理解此函数的设计,我们需要多一些信息,所以稍后再讲。
5.8.6. Maps 字典
映射提供了一个方便强力的内部数据结构,用来联合不同的类型。键可以是任何定义了相等操作符的类型,如整型,浮点型,字串,指针,界面(只要其动态类型支持相等)。结构,数组和切片不可用作映射键,因为其类型未定义相等。类似切片,映射是引用类型。如果你传递映射给某函数,对映射的内容的改动显现给调用者。
映射的生成使用平常的冒号隔开的键值伴组合字面句法,所以很容易初始化时建好它们。
var timeZone = map[string] int {
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
赋值和获取映射值语法上就像数组,只是下标不需是整型。
offset := timeZone["EST"]
试图获取不存在的键的映射值返回对应条目类型的零值。例如,如果映射包含整型数,查找不存在的键返回0。
有时你需区分不在键和零值。 是没有 “UTC” 的条目,还是因为其值为零?你可以用多值赋值的形式加以区分。
var seconds int
var ok bool
seconds, ok = timeZone[tz]
道理很明显,此习语称为“逗号ok”。此例中,如果 tz 存在,seconds 相应赋值,ok为真;否则,seconds 为0,ok为假。下面的函数加上了中意的出错报告:
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Stderr("unknown time zone", tz)
return 0
}
要检查映射的存在,又不想管实际值,你可以用空白标识,即下划线( _ )。空白标识可以赋值或声明为任意类型的任意值,会被无害的丢弃。如只要测试映射是否存在, 在平常变量的地方使用空白标识即可。
_, present := timeZone[tz]
要删除映射条目,翻转多值赋值,在右边多放个布尔;如果布尔为假,条目被删。即便键已经不再了,这样做也是安全的。
timeZone["PDT"] = 0, false // Now on Standard Time
5.8.7. 打印
Go 的排版打印风格类似 C 的 printf 族但更丰富更通用。这些函数活在 fmt 包里,叫大写的名字:fmt.Printf,fmt.Fprintf, fmt.Sprintf 等等。字串函数(Sprintf 等)返回字串,而不是填充给定的缓冲。
你不需给出排版字串。对应每个 Printf,Fprintf 和 Sprintf 都有另一对函数。例如 Print 和 Println。 它们不需排版字串,而是用每个参量默认的格式。Println 版本还会在参量间加入空格和输出新行,而 Print 版本只当操作数的两边都不是字串时才添加空格。下例每行的输出都是一样的:
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println(fmt.Sprint("Hello ", 23))
如《辅导》里所讲,fmt.Fprint 和伙伴们的第一个参量可以是任何实现 io.Writer 界面的物件。变量 os.Stdout 和 os.Stderr 是熟悉的实例。
从此事情开始偏离 C 了。首先,数字格式如 %d 没有正负和尺寸的标记;打印例程使用参量的类型决定这些属性。
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
打印出:
18446744073709551615 ffffffffffffffff; -1 -1
如果你只需默认的转换,例如整数用十进制,你可以用全拿格式 %v(代表 value);结果和Print 与 Println 打印的完全一样。再有,此格式可打印任意值,包括数组,结构和映射。这里是上节定义的时区映射的打印语句。
fmt.Printf("%v\n", timeZone) // or just fmt.Println(timeZone)
打印出:
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]
当然,映射的键会以任意顺序输出。打印结构时,改进的格式 %+v 用结构的域名注释,对任意值格式 %#v 打印出完整的 Go 句法。
type T struct {
a int
b float
c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)
打印出:
&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}
(注意和号&)。引号括起的字串也可以 %q 用在 string 或 []byte 类型的值上,对应的格式 %#q 如果可能则使用反引号。还有,%x 可用于字串、字节数组和整型,得到长的十六进制串,有空格的格式(% x)会在字节间加空格。
另一好用的格式是 %T,打印某值的类型。
fmt.Printf("%T\n", timeZone)
打印出:
map[string] int
如果你要控制某定制类型的默认格式, 只需在其类型上定义方法String() string。对我们简单的类型 T,可以是:
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)
来打印
7/-2.35/"abc\tdef"
我们的String() 方法可以调用 Sprint,因为打印例程是完全可以重入可以递归的。我们可以更进一步,把一个打印例程的参量直接传递给另一打印例程。 Printf 的签名的首参量使用类型 ...interface{},来指定任意数量任意类型的参量可以出现在格式字串的后面。
func Printf(format string, v ...) (n int, errno os.Error) {
Printf 函数中,v 像是一个 []interface{} 类的变量。但如果把它传递给另一个多维函数,它就像一列普通的参量。这里是我们上面用过的log.Println 的实现。它把自己的参量直接传递给 fmt.Sprintln 来实际打印。
// Stderr is a helper function for easy logging to stderr. It is analogous to Fprint(os.Stderr).
func Stderr(v ...) {
stderr.Output(2, fmt.Sprintln(v)) // Output takes parameters (int, string)
}
我们在 Sprintln 的调用的 v 后写 ... 告诉编译器把 v 作为一列参量;否则它只是传递一个单一的切片参量。
还有很多打印的内容我们还没讲,细节可参考 godoc 的 fmt 包的文档。
顺便提一句, ... 参量可以是任意给定的类型,例如,...int 在 min 函数里可以选一列整数的最小值。
func Min(a ...int) int {
min := int(^uint(0) >> 1) // largest int
for _, i := range a {
if i < min {
min = i
}
}
return min
}
5.8.8. Append
现在我们解释 append 的设计。append 的签名和上面我们定制的Append 函数不同。大体上是:
func append(slice []T, elements...T) []T
T 替代的是任意类型。 实际中你不能写 Go 的函数由调用者决定 T 的类型,所以 append 内置:它需要编译器的支持。
append 所做的是在切片尾添加元素并返回结果。结果需要返回因为,正如我们手写的 Append,底层的数组可能更改。下面简单的例子:
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
打印 1 2 3 4 5。所以 append 有点像 Printf 收集任意数量的参量。
但如何像我们 Append 一样给切片添加切片呢?容易:使用 ... 在调用的地方,正如我们上面我们调用 Output。 下例产生如上同样的输出:
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
没有 ... 将不能编译,因为类型错误; y 不是 int 类型。