数据

未匹配的标注

使用 new 关键字分配内存

Go 提供了两种分配原语,即内建函数 newmake。 它们所做的事情各不相同,所应用的类型也不同。因为它们的使用规则很简单,用法也很相似所以很容易引起混淆。 让我们先来看看 new。这是个用来分配内存的内建函数, 但与其它语言中的同名函数不同,它不会 初始化 内存,只会将内存 置零。 也就是说,new(T) 会为类型为 T 的新项分配已置零的内存空间, 并返回它的地址,也就是一个类型为 *T 的值。用 Go 的术语来说,它返回一个指针, 该指针指向新分配的,类型为 T 的零值。

既然 new 返回的内存已置零,那么当你设计数据结构时, 每种类型的零值就不必进一步初始化了,这意味着该数据结构的使用者只需用 new 创建一个新的对象就能正常工作。例如,bytes.Buffer 的文档中提到“零值的 Buffer 就是已准备就绪的缓冲区。” 同样,sync.Mutex 并没有显式的构造函数或 Init 方法, 而是零值的 sync.Mutex 就已经被定义为已解锁的互斥锁了。

“零值属性”可以带来各种好处。考虑以下类型声明。

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

SyncedBuffer 类型的值也是在声明时就分配好内存就绪了。后续代码中, pv 无需进一步处理即可正确工作。

p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer      // type  SyncedBuffer

构造函数和复合字面量

有时零值还不够好,这时就需要一个初始化构造函数,如来自 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
}

请注意,返回一个局部变量的地址完全没有问题,这点与C不同。该局部变量对应的数据 在函数返回后依然有效。实际上,每当获取一个复合字面的地址时,都将为一个新的实例分配内存, 因此我们可以将上面的最后两行代码合并:

    return &File{fd, name, nil, 0}

复合字面的字段必须按顺序全部列出。但如果以 字段: 对的形式明确地标出元素,初始化字段时就可以按任何顺序出现,未给出的字段值将赋予零值。 因此,我们可以用如下形式:

    return &File{fd: fd, name: name}

少数情况下,若复合字面不包括任何字段,它将创建该类型的零值。表达式 new(File)&File{} 是等价的。

复合字面同样可用于创建数组、切片以及映射,字段标签是索引还是映射键则视情况而定。 在下例初始化过程中,无论 EnoneEioEinval 的值是什么,只要它们的标签不同就行。

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"}

使用make分配

再回到内存分配上来。内建函数 make(T, args) 的目的不同于 new(T)。它只用于创建slicemapchannel,并返回类型为 T(而非 *T)的一个已初始化 (而非置零)的值。 出现这种用差异的原因在于,这三种类型本质上为引用数据类型,它们在使用前必须初始化。 例如,切片是一个具有三项内容的描述符,包含一个指向(数组内部)数据的指针、长度以及容量, 在这三项被初始化之前,该切片为 nil。对于slicemapchannelmake 用于初始化其内部的数据结构并准备好将要使用的值。例如,

make([]int, 10, 100)

会分配一个具有100个 int 的数组空间,接着创建一个长度为10, 容量为100并指向该数组中前10个元素的切片结构。(生成切片时,其容量可以省略,更多信息见切片一节。) 与此相反,new([]int) 会返回一个指向新分配的,已置零的切片结构, 即一个指向 nil 切片值的指针。

下面的例子阐明了 newmake 之间的区别:

var p *[]int = new([]int)       // 分配切片结构;*p == nil;很少用到
var v  []int = make([]int, 100) // 切片 v 现在引用了一个具有 100 个 int 元素的新数组

// 没必要的复杂用法:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

// 常规用法:
v := make([]int, 100)

请记住,make 只适用于map切片channel且不返回指针。若要获得明确的指针, 请使用 new 分配内存。

数组

在详细规划内存布局时,数组是非常有用的,有时还能避免过多的内存分配, 但它们主要用作切片的构件。这是下一节的主题了,不过要先说上几句来为它做铺垫。

以下为数组在Go和C中的主要区别。在Go中,

  • 数组是值。将一个数组赋予另一个数组会复制其所有元素。
  • 特别地,若将某个数组传入某个函数,它将接收到该数组的一份副本而非指针。
  • 数组的大小是其类型的一部分。类型 [10]int[20]int 是不同的。

数组为值的属性很有用,但代价高昂;若你想要C那样的行为和效率,你可以传递一个指向该数组的指针。

func Sum(a *[3]float64) (sum float64) {
    for _, v := range *a {
        sum += v
    }
    return
}

array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array)  // Note the explicit address-of operator

但这并不是Go的习惯用法,切片才是。

切片

切片通过对数组进行封装,为数据序列提供了更通用、强大而方便的接口。 除了矩阵变换这类需要明确维度的情况外,Go中的大部分数组编程都是通过切片来完成的。

切片保存了对底层数组的引用,若你将某个切片赋予另一个切片,它们会引用同一个数组。 若某个函数将一个切片作为参数传入,则它对该切片元素的修改对调用者而言同样可见, 这可以理解为传递了底层数组的指针。因此,Read 函数可接受一个切片实参 而非一个指针和一个计数;切片的长度决定了可读取数据的上限。以下为 os 包中 File 类型的 Read 方法签名:

func (f *File) Read(buf []byte) (n int, err error)

该方法返回读取的字节数和一个错误值(若有的话)。若要从更大的缓冲区 b 中读取前32个字节,只需对其进行切片即可。

    n, err := f.Read(buf[0:32])

这种切片的方法常用且高效。若不谈效率,以下片段同样能读取该缓冲区的前32个字节。

    var n int
    var err error
    for i := 0; i < 32; i++ {
        nbytes, e := f.Read(buf[i:i+1])  // Read one byte.
        n += nbytes
        if nbytes == 0 || e != nil {
            err = e
            break
        }
    }

只要切片不超出底层数组的限制,它的长度就是可变的,只需将它赋予其自身的切片即可。 切片的容量可通过内建函数 cap 获得,它将给出该切片可取得的最大长度。 以下是将数据追加到切片的函数。若数据超出其容量,则会重新分配该切片。返回值即为所得的切片。 该函数中所使用的 lencap 在应用于 nil 切片时是合法的,它会返回 0。

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // 重新分配
        // 为未来的增长,双重分配所需的内容.
        newSlice := make([]byte, (l+len(data))*2)
        // copy函数是预先声明的,适用于任何切片类型。
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

最终我们必须返回切片,因为尽管 Append 可修改 slice 的元素,但切片自身(其运行时数据结构包含指针、长度和容量)是通过值传递的。

向切片追加东西的想法非常有用,因此有专门的内建函数 append。 要理解该函数的设计,我们还需要一些额外的信息,我们将稍后再介绍它。

二维切片

Go的数组和切片都是一维的。要创建等价的二维数组或切片,就必须定义一个数组的数组, 或切片的切片,就像这样:

type Transform [3][3]float64  // 一个 3x3 的数组,其实是包含多个数组的一个数组。
type LinesOfText [][]byte     // 包含多个字节切片的一个切片。

由于切片长度是可变的,因此其内部可能拥有多个不同长度的切片。在我们的 LinesOfText 例子中,这是种常见的情况:每行都有其自己的长度。

text := LinesOfText{
    []byte("Now is the time"),
    []byte("for all good gophers"),
    []byte("to bring some fun to the party."),
}

有时必须分配一个二维数组,例如在处理像素的扫描行时,这种情况就会发生。 我们有两种方式来达到这个目的。一种就是独立地分配每一个切片;而另一种就是只分配一个数组, 将各个切片都指向它。采用哪种方式取决于你的应用。若切片会增长或收缩, 就应该通过独立分配来避免覆盖下一行;若不会,用单次分配来构造对象会更加高效。 以下是这两种方法的大概代码,仅供参考。首先是一次一行的:

// 分配底层切片.
picture := make([][]uint8, YSize) // y每一行的大小
//循环遍历每一行
for i := range picture {
    picture[i] = make([]uint8, XSize)
}

现在是一次分配,对行进行切片:

// 分配底层切片
picture := make([][]uint8, YSize) //  每 y 个单元一行。
// 分配一个大一些的切片以容纳所有的元素
pixels := make([]uint8, XSize*YSize) // 指定类型[]uint8, 即便图片是 [][]uint8.
//循环遍历图片所有行,从剩余像素切片的前面对每一行进行切片。
for i := range picture {
    picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

映射

映射是方便而强大的内建数据结构,它可以关联不同类型的值。其键可以是任何相等性操作符支持的类型, 如整数、浮点数、复数、字符串、指针、接口(只要其动态类型支持相等性判断)、结构以及数组。 切片不能用作映射键,因为它们的相等性还未定义。与切片一样,映射也是引用类型。 若将映射传入函数中,并更改了该映射的内容,则此修改对调用者同样可见。

映射可使用一般的复合字面语法进行构建,其键-值对使用冒号分隔,因此可在初始化时很容易地构建它们。

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。 集合可实现成一个值类型为 bool 的映射。将该映射中的项置为 true 可将该值放入集合中,此后通过简单的索引操作即可判断是否存在。

attended := map[string]bool{
    "Ann": true,
    "Joe": true,
    ...
}

if attended[person] { // person不在集合中,返回 false
    fmt.Println(person, "was at the meeting")
}

有时你需要区分某项是不存在还是其值为零值。如对于一个值本应为零的 "UTC" 条目,也可能是由于不存在该项而得到零值。你可以使用多重赋值的形式来分辨这种情况。

var seconds int
var ok bool
seconds, ok = timeZone[tz]

显然,我们可称之为“逗号 ok”惯用法。在下面的例子中,若 tz 存在, seconds 就会被赋予适当的值,且 ok 会被置为 true; 若不存在,seconds 则会被置为零,而 ok 会被置为 false

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}

若仅需判断映射中是否存在某项而不关心实际的值,可使用空白标识符_)来代替该值的一般变量。

_, present := timeZone[tz]

要删除映射中的某项,可使用内建函数 delete,它以映射及要被删除的键为实参。 即便对应的键不在该映射中,此操作也是安全的。

delete(timeZone, "PDT")  // 现在是标准时间

打印

Go采用的格式化打印风格和C的 printf 族类似,但却更加丰富而通用。 这些函数位于 fmt 包中,且函数名首字母均为大写:如 fmt.Printffmt.Fprintffmt.Sprintf 等。 字符串函数(Sprintf 等)会返回一个字符串,而非填充给定的缓冲区。

你无需提供一个格式字符串。每个 PrintfFprintfSprintf 都分别对应另外的函数,如 PrintPrintln。 这些函数并不接受格式字符串,而是为每个实参生成一种默认格式。Println 系列的函数还会在实参中插入空格,并在输出时追加一个换行符,而 Print 版本仅在操作数两侧都没有字符串时才添加空白。以下示例中各行产生的输出都是一样的。

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))

fmt.Fprint 一类的格式化打印函数可接受任何实现了 io.Writer 接口的对象作为第一个实参;变量os.Stdoutos.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(对应“值”);其结果与 PrintPrintln 的输出完全相同。此外,这种格式还能打印任意值,甚至包括数组、结构体和映射。 以下是打印上一节中定义的时区映射的语句。

fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)

打印结果:

map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]

对于映射, Printf 会自动对映射值按照键的字典顺序排序。

当然,映射中的键可能按任意顺序输出。当打印结构体时,改进的格式 %+v 会为结构体的每个字段添上字段名,而另一种格式 %#v 将完全按照Go的语法打印值。

type T struct {
    a int
    b float64
    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, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}

(请注意其中的&符号)当遇到 string[]byte 值时, 可使用 %q 产生带引号的字符串;而格式 %#q 会尽可能使用反引号。 (%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"

(如果你需要像指向 T 的指针那样打印类型 TString 的接收者就必须是值类型的;上面的例子中接收者是一个指针, 因为这对结构来说更高效而通用。更多详情见指针vs.值接收者一节)

我们的 String 方法也可调用 Sprintf, 因为打印例程可以完全重入并按这种方式封装。不过有一个重要的细节你需要知道: 请勿通过调用 Sprintf 来构造 String 方法,因为它会无限递归你的 String 方法。如果 Sprintf 调用试图将接收器直接打印为字符串,而该字符串又将再次调用该方法,则会发生这种情况。这是一个常见的错误,如本例所示。

type MyString string

func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // 错误:会无限递归
}

要解决这个问题也很简单:将该实参转换为基本的字符串类型,它没有这个方法。

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // 可以:注意转换
}

初始化 一节中,我们将看到避免这种递归的另一种技术。

另一种打印技术就是将打印例程的实参直接传入另一个这样的例程。Printf 的签名为其最后的实参使用了 ...interface{} 类型,这样格式的后面就能出现任意数量,任意类型的形参了。

func Printf(format string, v ...interface{}) (n int, err error) {

Printf 函数中,v 看起来更像是 []interface{} 类型的变量,但如果将它传递到另一个变参函数中,它就像是常规实参列表了。 以下是我们之前用过的 log.Println 的实现。它直接将其实参传递给 fmt.Sprintln 进行实际的格式化。

// Println 通过 fmt.Println 的方式将日志打印到标准记录器
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string)
}

在该 Sprintln 嵌套调用中,我们将 ... 写在 v 之后来告诉编译器将 v 视作一个实参列表,否则它会将 v 当做单一的切片实参来传递。

还有很多关于打印知识点没有提及。详情请参阅 godocfmt 包的说明文档。

顺便一提,... 形参可指定具体的类型,例如从整数列表中选出最小值的函数 min,其形参可为 ...int 类型。

func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // 最大的 int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}

追加

现在我们要对内建函数 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 6]。因此 append 有点像 Printf 那样,可接受任意数量的实参。

但如果我们要像 Append 那样将一个切片追加到另一个切片中呢? 很简单:在调用的地方使用 ...,就像我们在上面调用 Output 那样。以下代码片段的输出与上一个相同。

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

如果没有 ...,它就会由于类型错误而无法编译,因为 y 不是 int 类型的。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/effective-go/20...

译文地址:https://learnku.com/docs/effective-go/20...

上一篇 下一篇
贡献者:14
讨论数量: 0
发起讨论 只看当前版本


暂无话题~