EffectiveGo-3-数据、初始化

Parts of Effective Go

Data

new

1
2
3
4
5
6
7
8
// new是用来分配内存的内建函数
// new不会初始化内存,只会将内存置零

// 为类型为T的新项分配已置零的内存空间
// 并返回它的指针
// ptrT 的类型为 *T
// ptrT指针指向新分配的,类型为T的零值
ptrT := new(T)

构造函数与复合字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 // f 的类型是 *File
}

func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0} // f 的类型是 File
return &f // 取址
}

func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
// 合并栗子2中两行代码
// 按顺序列出File中的全部字段
return &File{fd, name, nil, 0}
}

func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
// 以 (field:value) 的形式明确标出元素
// 初始化字段可以按任何顺序出现
// 未出现的field将被赋零值
return &File{fd: fd, name: name}
}

// 若复合字段不包括任何field
// 它将创建该类型的零值
// 以下两种是等价的
new(File)
&File{}

复合字段同样可用于创建数组、切片以及映射,field是索引还是映射键视具体情况而定。
Go中,返回一个局部变量的地址完全没有问题,局部变量的数据在函数返回后依然有效。

make

内建函数make(T, args)的目的不同于new(T)
make(T, args)只用于创建切片、映射和信道,并返回类型为T(非*T)的一个已初始化(非置零)的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 会分配一个具有100个int的数组空间
// 然后创建一个长度为10,容量为100并指向该数组的切片
make([]int, 10, 100)

// 会返回一个指向新分配的,已置零的切片结构的指针
// 即一个指向nil切片值的指针
new([]int)

// 分配切片结构;*p == nil;基本没用
var p *[]int = new([]int)
// 切片 v 现在引用了一个具有 100 个 int 元素的新数组
var v []int = make([]int, 100)
// 没必要的复杂
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// 习惯用法
v := make([]int, 100)

make只适用于映射、切片和信道且不返回指针;若要获得明确的指针,使用new

Arrays

数组是值。若将一个数组赋予另一个数组会复制其所有元素。
若将某个数组传入某个函数,它将接收到该数组的一份副本而非指针。
数组的大小是类型的一部分。类型[10]int[20]int是不同的。
数组并不是Go的习惯用法,切片才是。

Slices

1
2
3
4
5
6
7
8
9
10
var n int
var err error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i : i+1]) // 读取一个字节
if nbytes == 0 || e != nil {
err = e
break
}
n += nbytes
}

切片保存了对底层数组的引用,若将某个切片赋予另一个切片,它们会引用同一个数组
若某个函数将一个切片作为参数传入,则函数对切片元素的修改对调用者同样可见
切片的长度决定了可读取数据的上限
切片的长度不能超出底层数组的限制(切片的容量可通过内建函数cap获得)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 可以通过Append追加数据到切片
// 若数据超出切片容量,则会重新分配该切片
// 对nil切片使用len和cap是合法的,返回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)]
for i, c := range data {
slice[l+i] = c
}
return slice
}
// 尽管Append可以修改slice的元素
// 但Append必须返回切片
// 因为切片自身是通过值传递的

二维切片Two-dimensional slices

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// A 3*3 array
// 包含多个数组的一个数组
type Transform [3][3]float64
// a silce of byte slices
// 包含多个字节切片的一个切片
type LineOfText [][]byte

// 由于切片长度是可变的
// 因此其内部可能拥有多个不同长度的切片
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 常用分配二维切片的方法

// 1.独立分配每一个切片
// 顶层切片
picture := make([][]uint8, YSize)
// 遍历行,为每一行都分配切片
for i := range picture {
picture[i] = make([]uint8, XSize)
}

// 2.只分配一个数组,将每个切片都指向它
// 顶层切片
picture := make([][]uint8, YSize)
// 分配一个大的切片来保存所有数据
pixels := make([]uint8, XSize*YSize)
// 遍历行,从剩余大切片的前面切出每行
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

// 若切片会增长或收缩,选择1
// 2更高效

Maps

映射可以关联不同类型的值
任何相等性操作符支持的类型(整数/浮点数/复数/字符串/指针/接口/结构体/数组)都可以做映射的键
切片不能用作映射键(切片的相等性未定义)
若将映射传入函数中,映射修改对调用者同样可见

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 初始化
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["CST"]

// 取不存在的键,返回类型的零值
// value == 0
value := timeZone["HELLO"]

// 集合可实现一个值类型为bool的映射
// 将该映射中的项置为true可将该值放入集合中
// 通过简单的索引操作可判断是否存在
attended := map[string]bool{
"Ann": true,
"Joe": true,
}
person := "nobody"
if attended[person] { // person 不在映射中 => false
fmt.Println(person, "was at the meeting")
}

// 同样可使用多重赋值
var seconds int
var ok bool
seconds, ok = timeZone[tz]

// comma ok
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")

Printing

1
2
3
4
5
6
7
// 以下各行产生的输出是一样的
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
// fmt.Println 会默认在尾部添加一个换行符
fmt.Println("Hello ", 23)
// fmt.Sprint 会返回一个字符串,而非填充给定的缓冲区
fmt.Println(fmt.Sprint("Hello ", 23))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
var x uint64 = 1<<64 - 1
fmt.Printf("%d, %x, %d, %x\n", x, x, int64(x), int64(x))
// 18446744073709551615, ffffffffffffffff, -1, -1

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,
}
fmt.Printf("%v\n", timeZone)
// map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

type T struct {
a int
b float64
c string
}
t := &T{7, -2.35, "abc\tdef"}
fmt.Printf("%v\n", t)
// &{7 -2.35 abc def}
fmt.Printf("%+v\n", t)
// &{a:7 b:-2.35 c:abc def}
fmt.Printf("%#v\n", t)
// &main.T{a:7, b:-2.35, c:"abc\tdef"}
fmt.Printf("%#v\n", timeZone)
// map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}
// map[string] int
fmt.Printf("%T\n", timeZone)
// 控制自定义类型的默认格式
// 为类型定义一个 String() string 的方法
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
// 7/-2.35/"abc\tdef"
fmt.Print("%v\n", t)

type MyString string
// 不能通过调用Sprintf来构造String方法
// 会无限递归String方法
func (m MyString) String() string {
// Error: will recur forever.
return fmt.Sprintf("MyString=%s", m)
}
// 将该实参转换为基本的字符串类型
func (m MyString) String() string {
// OK
return fmt.Sprintf("MyString=%s", string(m))
}
// Pass a print routine's arguments directly to another such routine.
// The signature of Printf uses the type ...interface{}
// for its final argument to specify that an arbitrary
// number of parameters (of arbitrary type) can appear
// after the format.
func Printf(format string, v ...interface{}) (n int, err error) {}
// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
// Output takes parameters (int,string)
std.Output(2, fmt.Sprintln(v...))
}

// See the godoc documentation for package fmt for the details
func Min(a ...int) int {
min := int(^uint(0) >> 1) // largest int
for _, i := range a {
if i < min {
min = i
}
}
return min
}

Append

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func append(slice []T, elements ...T) []T
// T是为任意给定类型的占位符
// append会在切片末尾追加元素并返回结果
// 使用返回结果(即底层数组可能会被改变)

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

x := []int{1,2,3}
y := []int{4,5,6}
// compile error
// x is []int
// y's type is not int
x = append(x, y)
// success
x = append(x, y...)

初始化Initialization

Go在初始化过程中,不仅可以构建复杂的结构,还能正确处理不同包对象间的初始化顺序。

常量Constants

常量在编译时创建
常量只能是数字、字符、字符串或布尔值
定义常量的表达式必须是可以被编译器求值的常量表达式
1<<3是一个常量表达式
math.Sin(math.Pi/4)则不是
math.Sin的函数调用在运行时才会发生

枚举常量使用枚举器iota创建
iota可以是表达式的一部分
表达式可以被隐式地重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type ByteSize float64

const (
// 通过赋予空白标识符来忽略iota的第一个值
_ = iota
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)

// 可将 String 之类的方法附加在用户定义的类型上
// 自动格式化打印任意值
func (b ByteSize) String() string {
switch {
case b >= YB:
return fmt.Sprinf("%.2fYB", b/YB)
case b >= ZB:
return fmt.Sprinf("%.2fZB", b/ZB)
case b >= EB:
return fmt.Sprinf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprinf("%.2fPB", b/PB)
case b >= TB:
return fmt.Sprinf("%.2fTB", b/TB)
case b >= GB:
return fmt.Sprinf("%.2fGB", b/GB)
case b >= MB:
return fmt.Sprinf("%.2fMB", b/MB)
case b >= KB:
return fmt.Sprinf("%.2fKB", b/KB)
}
return fmt.Sprinf("%.2fB", b)
}

这里用Sprinf实现ByteSizeString方法很安全(不会无限递归),不是因为类型转换,而是因为它以%f调用了Sprintf,它并不是一种字符串格式:Sprinf只会在它需要字符串时才会调用String方法,而%f需要一个浮点数值。

变量Variables

1
2
3
4
5
6
7
// 变量的初始化与常量有区别
// 其初始值可以是在运行时才被计算的一般表达式
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)

init函数

每个包都可以通过定义自己的无参数init函数来设置一些必要的状态(每个包可以拥有多个init函数-顺序不可依赖)。
全部init函数结束就意味着初始化结束。
当该包中所有的变量声明都通过初始化器求值后,并且所有已导入的包都被初始化后,init才会被调用。
假设PackageA import PackageB import PackageC,则init执行顺序为1.PackageCInit 2.PackageBInit 3.PackageAInit
在一个应用的启动过程中,每个包的每个init函数只会被执行一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// init除了用于那些不能被表示成声明的初始化外
// 还可用于在程序真正开始执行前,检查或校验程序的状态
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath 可通过命令行中的 --gopath 标记覆盖掉
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}