在上一节中,我们了解到了“零值可用”对于编写出符合 Go 惯用法的代码是大有裨益的。但有些时候,零值并非是最好的选择,我们有必要要为变量赋予适当的初值以保证其后续以正确的“状态”参与到业务流程计算中去,尤其是 Go 语言中的一些类型为复合类型的变量。Go 语言中的复合类型包括结构体、数组、切片和 map。
对于复合类型变量,最常见的值构造方式就是对其内部元素做逐个赋值,比如:
var s myStruct
s.name = "tony"
s.age = 23
var a [5]int
a[0] = 13
a[1] = 14
... ...
a[4] = 17
sl := make([]int, 5, 5)
sl[0] = 23
sl[1] = 24
... ...
sl[4] = 27
m := make(map[int]string)
m[1] = "hello"
m[2] = "gopher"
m[3] = "!"
但这样的值构造方式让代码显得有些“繁琐”,尤其是在构造组成较为复杂的复合类型变量初值的时候。Go 提供的复合字面值(composite literals)语法可以作为复合类型变量的初值构造器。使用复合字面值上述代码可以改写成下面这样:
s := myStruct {"tony", 23}
a := [5]int{13, 14, 15, 16, 17}
sl := []int{23, 24, 25, 26, 27}
m := map[int]string {1:"hello", 2:"gopher", 3:"!"}
显然,代码得到了很大的简化。
我们看到复合字面值由两部分组成,一部分是类型,比如上述例子中赋值操作符右侧的 myStruct、[5]int、[]int、map[int]string,另外一部分是由大括号 {} 包裹的字面值。这里的字面值形式还仅仅是 Go 复合字面值作为值构造器的基本用法。下面我们来分别看看复合字面值对于不同复合类型的“高级用法”。
1. 结构体复合字面值
使用 go vet 工具对 Go 源码进行过静态代码分析的童鞋们可能会知道,go vet 工具中默认内置了一条检查规则:“composites”。下面是源码中对该规则的描述:
此分析器对源码中使用复合字面值对struct变量赋值的行为进行诊断:
如果源码使用从另一个包(package)中导入的struct类型,但不使用field:value语法形式进行值构造的,分析器认为这样的复合字面值是脆弱的。因为一旦结构体增加了一个新的字段(即使未导出),这种值构造方式也将导致编译失败。
举个例子:
err = &net.DNSConfigError{err}
应该替换为:
err = &net.DNSConfigError{Err: err}
Go 推荐使用 “field:value” 格式的复合字面值形式对 struct 类型变量进行值构造,这种值构造方式可以降低结构体类型使用者与结构体类型设计者之间的耦合。这也是 Go 语言的惯用法,在 Go 标准库中,通过"field:value"格式复合字面值进行结构体类型变量初值构造的例子比比皆是:
// $GOROOT/src/net/http/transport.go
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
// $GOROOT/src/io/pipe.go
type pipe struct {
wrMu sync.Mutex // Serializes Write operations
wrCh chan []byte
rdCh chan int
once sync.Once // Protects closing done
done chan struct{}
rerr atomicError
werr atomicError
}
func Pipe() (*PipeReader, *PipeWriter) {
p := &pipe{
wrCh: make(chan []byte),
rdCh: make(chan int),
done: make(chan struct{}),
}
return &PipeReader{p}, &PipeWriter{p}
}
我们看到这种 “field:value” 形式的复合字面值初值构造器颇为强大。和之前普通复合字面值形式相比,“field:value” 形式字面值中的字段可以以任意次序出现,未显式出现在字面值的结构体中的字段将采用其对应类型的零值。以上面的 pipe 为例,Pipe 函数在使用复合字面值对其类型变量进行初值构造时仅对 wrCh、rdCh 和 done 做了 “field:value” 形式的显式赋值,这样 pipe 结构体中的其他变量的值将为其类型的初值,比如 wrMu。
从上面例子我们还可以看到:通过在复合字面值值构造器的类型前面增加&,我们可以得到对应类型的指针类型变量,如上面例子中的 p 的类型为 Pipe 类型指针。
复合字面值作为结构体值构造器的大量使用,使得即便采用类型零值时我们也会使用字面值构造器形式:
s := myStruct{}
而较少使用 new 这一个 Go 预定义的函数来创建结构体变量实例:
s := new(myStruct)
值得注意的是:使用从其他包导入的结构体中的未导出字段作为复合字面值中的 field 是不被允许的,会导致编译错误。
2. 数组/切片复合字面值
和结构体类型不同,数组/切片使用下标(index)作为 “field:value” 形式中 “field”,从而实现数组/切片初始元素值的高级构造形式:
numbers := [256]int{'a': 8, 'b': 7, 'c': 4, 'd': 3, 'e': 2, 'y': 1, 'x': 5}
// [10]float{-1, 0, 0, 0, -0.1, -0.1, 0, 0.1, 0, -1}
fnumbers := [...]float{-1, 4: -0.1, -0.1, 7:0.1, 9: -1}
// $GOROOT/src/sort/search_test.go
var data = []int{0: -10, 1: -5, 2: 0, 3: 1, 4: 2, 5: 3, 6: 5, 7: 7, 8: 11, 9: 100, 10: 100, 11: 100, 12: 1000, 13: 10000}
var sdata = []string{0: "f", 1: "foo", 2: "foobar", 3: "x"}
和结构体复合字面值较多采用 “field:value” 形式作为值构造器不同的是,数组/切片由于其固有的特性,采用 “index:value” 为其构造初值主要应用在少数场合,比如:为非连续(稀疏)元素构造初值(上面的 numbers、fnumbers)、让编译器根据最大元素下标值推导数组的 Size(如上面的 fnumbers)等。
另外在编写单元测试时,为了更显著体现元素对应的下标值,可能会使用 “index:value” 形式来为数组/切片进行值构造,如上面标准库单元测试源码中的 data、sdata。
3. map 复合字面值
和结构体、数组/切片相比,map 类型变量使用复合字面值作为初值构造器就显得自然了许多,因为 map 类型具有原生的 “key:value” 抽象形式:
// $GOROOT/src/time/format.go
var unitMap = map[string]int64{
"ns": int64(Nanosecond),
"us": int64(Microsecond),
"µs": int64(Microsecond), // U+00B5 = micro symbol
"μs": int64(Microsecond), // U+03BC = Greek letter mu
"ms": int64(Millisecond),
"s": int64(Second),
"m": int64(Minute),
"h": int64(Hour),
}
// $GOROOT/src/net/http/server.go
type ConnState int
const (
StateNew ConnState = iota
StateActive
StateIdle
StateHijacked
StateClosed
)
var stateName = map[ConnState]string{
StateNew: "new",
StateActive: "active",
StateIdle: "idle",
StateHijacked: "hijacked",
StateClosed: "closed",
}
对于数组/切片类型而言,当元素的类型为复合类型时,我们可以省去元素复合字面量中的类型,比如:
type Point struct {
x float64
y float64
}
sl := []Point{
{1.2345, 6.2789}, // Point{1.2345, 6.2789}
{2.2345, 19.2789}, // Point{2.2345, 19.2789}
}
但是对于 map 类型而言,这一优化在 Go 1.5 中才得以引入。引入后,当 key 或 value 的类型为复合类型时,我们可以省去 key 或 value 中的复合字面量中的类型:
// Go 1.5版本之前:
m := map[Point]string{
Point{29.935523, 52.891566}: "Persepolis",
Point{-25.352594, 131.034361}: "Uluru",
Point{37.422455, -122.084306}: "Googleplex",
}
vs.
// Go 1.5版本及之后
m := map[Point]string{
{29.935523, 52.891566}: "Persepolis",
{-25.352594, 131.034361}: "Uluru",
{37.422455, -122.084306}: "Googleplex",
}
m1 := map[string]Point{
"Persepolis": {29.935523, 52.891566},
"Uluru": {-25.352594, 131.034361},
"Googleplex": {37.422455, -122.084306},
}
对于 key 或 value 为指针类型的情况,我们也可以省略 “&T”:
m2 := map[string]*Point{
"Persepolis": {29.935523, 52.891566}, // &Point {29.935523, 52.891566}
"Uluru": {-25.352594, 131.034361}, // &Point{-25.352594, 131.034361}
"Googleplex": {37.422455, -122.084306}, // &Point{37.422455, -122.084306}
}
fmt.Println(m2) // map[Googleplex:0xc0000ae050 Persepolis:0xc0000ae030 Uluru:0xc0000ae040]
4. 小结
对于零值不适用的场景,我们要为变量赋予一定的初值。对于复合类型而言,我们应该首选 Go 提供的复合字面值作为初值构造器。对于不同复合类型,我们要记住下面几点:
- 使用 “field:value” 形式的复合字面值为结构体类型的变量赋初值;
- 在为稀疏元素赋值或让编译器推导数组 Size 的时候,多使用 “index:value” 的形式为数组/切片类型变量赋初值;
- 使用 “key:value” 形式的复合字面值为 map 类型的变量赋初值。