翻译: Effective Go (5)

用户头像
申屠鹏会
关注
发布于: 2020 年 08 月 14 日

数据

new分配内存

Go有两个分配新内存的语法,内置函数new和make。它们执行不同的操作并应用于不同的类型,这虽然有点困惑,但是规则很简单。我们先来讨论new。这是一个内置的函数,可以申请内存空间,但与其他一些语言中的同名函数不同的是,它不对内存进行初始化,只是将内存给予零值。也就是说,new(T)为类型T分配零值内存,并返回它的地址,即*T类型的值。用Go的术语来说,它返回一个指针,指向新分配T类型的零值。

由于new返回的内存是零值化的,所以在设计数据结构时,每个类型的零值可以不需要初始化函数就可以使用。这意味着数据结构的用户可以用new创建一个数据结构,然后直接开始工作。例如,bytes.Buffer的文档中指出:Buffer的零值是一个准备使用的空缓冲区同样,syn.Mutex也没有一个显示的构造函数或init方法,想法,syn.Mutex的零值被定义为一个未锁定的锁。

零值可以被用来中间过度,考虑下面的类型声明:

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


SyncedBuffer类型的值可以通过分配或声明进行使用。下面,p和v不需要额外的处理就会同时正确的工作:

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语言不同的是,Go返回局部变量的地址是完全可以的。在函数返回后,该变量关联的内存会继续存在。事实上,每次获取一个复合字面量的地址时,都会分配一个新的实例, 因此我们可以将上面的最后两行代码进行合并。

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


复合字面量的字段是按顺序排列的,并且必须全部列出。但是,通过字段:值进行明确标注的时候,初始化可以以任意顺序出现,缺失的字段作为零值。因此,我们可以这么写。

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


在少数情况下,如果一个复合字面量不包含任何字段,那么它会创建该类型的零值。new(File)和&File{}是等价的。

复合字面量也可以创建数组,切片(slices),映射(map),字段自动变为索引或者键(key)。在这些例子中,无论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"}


make分配内存

回到内存分配上。内建函数make(T,args)的作用与new(T)不同。它值创建切片,映射和管道,并返回一个类型T(不是*T)初始化后的(不是零值)的值。不同的原因是,这三种类型本质上为引用数据类型,在使用前必须进行初始化。例如,切片是一个具有三项内容的描述符,包含指向数据(在数组内部)的指针,长度和容量。在这些项目被初始化之前,切片是nil。对于切片,映射,管道,make初始化了内部数据结构,并准备好了将要使用的值。例如,

make([]int, 10, 100)


这行代码分配了一个具有100个int的数组,然后创建一个长度为10,容量为100的并指向前10个元素的切片结构。(make切片时,可以省略容量,更多信息可以看切片这一节)。与make相反,new([]int)返回一个指向新分配的,零值的切片指针,即指向nil的指针。

var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v []int = make([]int, 100) // the slice 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分配或明确的获取变量的地址。

数组

数组在规划详细的内存布局时候很有用,又是还能避免进行内存分配。但主要的是,内存是切片的构件。这是下一节的主题,不过还是要说几句。

下面是数组在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的风格,通常会使用切片(slices)

切片(slices)

切片封装了数组,为序列化数据提供了一个更通用,强大和方便的接口。除了具有显式唯独的项目,如矩阵变换等,Go中的大多数数组编程都是用切片而不是简单的数组来完成的。

切片保存了对底层数组的引用,如果你把一个切片赋给另一个切片,那么两个切片都会指向同一个数组。如果一个函数接受一个切片,那么它对切片中元素的修改对调用方也是可见的,这就类似传递一个指向底层数组的指针。因此,Read函数可以接受一个切片参数,而不是一个指针和一个计数count。切片中的长度设置了一个读取数据的上限,下面是os包中的File类型Read方法的签名。

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


该方法返回读取的字节数和一个错误值(如果有的话)。要从一个较大的缓冲区buf读取前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访问,它表示了切片的最大长度。又一个函数可以将数据追加到切片中,如果数据超过了容量,切片会被重新分配。所得到的切片会被返回。该函数利用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)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
return slice
}


最终我们必须返回slice,因为虽然append可以修改slice的元素,但slice本身(保存指针,长度和容量的运行时数据结构)是通过值传递的。

向切片追加数据的想法非常有用,所以有了内置的append函数。不过要理解这个函数的设计,需要更多的信息,所以稍后再进行讨论。

二维切片

Go的数组和切片是一维的,要创建一个相当于二维数组或二维切片的数组或切片,需要定义一个数组的数组或切片的切片,就像这样:

type Transform [3][3]float64 // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte // A slice of byte slices.


由于切片的长度是可变的,所以可以让每个内部的切片有不同的长度。这可能是一种常见的情况,就像我们的LinesOfText例子:每一行都有一个独立的长度。

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


有时需要分配一个二维的切片,比如在处理按行扫描像素时。有两种方式可以实现。一种是独立分配每个切片;另一种是分配一个单独的数组,并将单个切片指向其中。使用哪种方法取决于你的应用。如果切片可能会增长或缩小,就应该独立分配,以避免覆盖下一行;如果不是,用单个分配来构造会更有效率。下面是两种方式的大概代码,仅供参考。首先,一次一行:

// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
picture[i] = make([]uint8, XSize)
}


然后是一次分配,按行切片

// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}


映射

映射是一种方便且强大的内建数据结构,可以将一种类型的值(key)与另一种类型的值(value)关联起来。键(key)可以是定义了相等运算符的任意类型,例如整数,浮点数和复数,字符串,指针,接口(只要动态类型支持相等),结构和数组。切片不能用作键,因为它没有定义相等性。和切片一样,映射保存对底层数据结构的引用,如果你把一个映射传递给一个函数并修改其内容,那么此修改对调用方也是可见的。

映射可以用通常的复合字面量语法进行构建,其key-value键值对使用冒号分隔,因此,在初始化过程中很容易创建映射。

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.可以用值为布尔类型的映射实现集合。将映射值设为true,将键设为要放入的值,然后通过简单的索引进行测试。

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

if attended[person] { // will be false if person is not in the map
fmt.Println(person, "was at the meeting")
}


有时需要区分缺失的值和零值。是有一个“UTC”的value为0,还是它根本不在映射中才返回0?这时可以用一种多重赋值的方式来区分。

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


出于明显的原因,这被称为“逗号ok”用法,在这个例子中,如果tz存在,秒数会被设置为true,如果不存在秒数会被设置为0,同时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") // Now on Standard Time


打印输出(printing)

Go的格式化打印使用了类似C语言printf系列风格,但是更丰富和通用。这些函数在fmt包中,并首字母大写:fmt.Printf, fmt.Fprintf, fmt.Sprintf等。字符串函数(Stringf等)返回一个字符串,而不是填入提供的缓冲区。

你不需要提供一个格式化的字符,对于每个Printf, Fprintf和Springf都有另外一对函数,例如Print和Println,这些函数不接受格式字符串,而是为每个参数生成一个默认格式。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.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 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}


(注意 &号)当遇到字符串类型或[]byte类型的值时,可以用%q获取带引号的字符串。%#q会尽可能使用反引号。(%q适用于整数和rune,产生单引号的字符常量)此外,%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的值以及指向T的指针,则String的接受者必须为值类型;这个例子使用了一个指针,因为它对于结构类型更加有效且更惯用。请参见下面的指针 vs 值接受者以获得更多的信息)

我们的string方法可以调用Sprintf,因为打印程序是完全可以重入的,并且可以用这种方式封装。关于此方法,又一个重要的细节要理解:不要通过Sprintf来构造String方法,因为会造成无限递归String。如果Sprintf调用试图将接收方直接打印为字符串,则可能会发生这种情况,,而String又会在此调用该方法。如本例所示,是一个常见且容易犯的错误。

type MyString string

func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}


不过这很容易修复:将参数变为基本的字符串类型,它就没有String方法了。

type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}


在初始化部分,我们可以看到另一种避免无限递归的方式。

另一种打印技巧是传递一个打印函数给另一个打印函数,Printf使用interface{}作为最后一个参数,以指定可以在格式之后显示任意数量的参数(任意类型)。

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

在函数Printf中,v的作用类似于[]interface{}类型的变量,但如果将其传递给另一个可变参数的函数,则其作用类似于常规的参数列表。这是我们上面使用的功能log.Println的实现。它将其参数直接传递给fmt.Sprintln进行实际的格式化。

// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...)) // Output takes parameters (int, string)
}


v后面的...以告诉编译器将v视为参数列表,否则它将v作为单个slice参数进行传递。除了这里介绍的内容外,还有更多有关print的内容,有关详细信息可以参见fmt包的godoc文档。

顺便提一下,一个...参数可以是具体的类型,例如...int可以用于选择最小整数列表的mini函数。

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


附加(append)

现在我们还没解释内建函数append的设计。append的签名与上面自定义的Append函数不同,大致来说,它是这样的:

func append(slice []T, elements ...T) []T

其中T是给任何给定类型的占位符,实际上你无法在Go中编写一个由调用者确定类型T的函数。这就是内置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另一个切片该怎么办?很简单:用...进行传参。下面的代码会和上面的输出一样:

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


如果没有...,将会因为类型错误而无法变异,因为y不是int类型



发布于: 2020 年 08 月 14 日 阅读数: 33
用户头像

申屠鹏会

关注

enjoy~ 2018.11.08 加入

https://xabc.site

评论

发布
暂无评论
翻译: Effective Go (5)