当前位置: 首页 > 文档资料 > Go 中文文档 >

5.8. 数据

优质
小牛编辑
130浏览
2023-12-01

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 类型。