💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 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/](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](https://golang-china.googlecode.com/svn/trunk/Chinese/golang.org/6)。所以 append 有点像 Printf 收集任意数量的参量。 但如何像我们 Append 一样给切片添加切片呢?容易:使用 ... 在调用的地方,正如我们上面我们调用 Output。 下例产生如上同样的输出: ``` x := []int{1,2,3} y := []int{4,5,6} x = append(x, y...) fmt.Println(x) ``` 没有 ... 将不能编译,因为类型错误; y 不是 int 类型。