购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第12条
使用复合字面值作为初值构造器

在上一条中,我们了解到零值可用对于编写出符合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 literal)语法可以作为复合类型变量的初值构造器。上述代码可以使用复合字面值改写成下面这样:


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复合字面值作为值构造器的基本用法。下面来分别看看复合字面值对于不同复合类型的高级用法。

12.1 结构体复合字面值

使用go vet工具对Go源码进行过静态代码分析的读者可能会知道,go vet工具中内置了一条检查规则:composites。此规则用于检查源码中使用复合字面值对结构体类型变量赋值的行为。如果源码中使用了从另一个包中导入的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
wrCh chan []byte
rdCh chan int

once sync.Once
done chan struct{}
rerr onceError
werr onceError
}

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函数在使用复合字面值对pipe类型变量进行初值构造时仅对wrCh、rdCh和done进行了field:value形式的显式赋值,这样pipe结构体中的其他变量的值将为其类型的初值,如wrMu。

从上面例子中还可以看到,通过在复合字面值构造器的类型前面增加&,可以得到对应类型的指针类型变量,如上面例子中的变量p的类型即为Pipe类型指针。

复合字面值作为结构体值构造器的大量使用,使得即便采用类型零值时我们也会使用字面值构造器形式:


s := myStruct{} // 常用

而较少使用new这一个Go预定义的函数来创建结构体变量实例:


s := new(myStruct) // 较少使用

值得注意的是,不允许将从其他包导入的结构体中的未导出字段作为复合字面值中的field,这会导致编译错误。

12.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)、让编译器根据最大元素下标值推导数组的大小(如上面示例中的fnumbers)等。

另外在编写单元测试时,为了更显著地体现元素对应的下标值,可能会使用index:value形式来为数组/切片进行值构造,如上面标准库单元测试源码中的data和sdata。

12.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 = 微符号
"μs": int64(Microsecond), // U+03BC = 希腊字母μ
"ms": int64(Millisecond),
...
}


// $GOROOT/src/net/http/server.go
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",
}


// 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},   // 相当于value为&Point{29.935523, 52.891566}
"Uluru":      {-25.352594, 131.034361}, // 相当于value为&Point{-25.352594, 131.034361}
"Googleplex": {37.422455, -122.084306}, // 相当于value为&Point{37.422455, -122.084306}
}

fmt.Println(m2) // map[Googleplex:0xc0000ae050 Persepolis:0xc0000ae030 Uluru:0xc0000ae040]

小结

对于零值不适用的场景,我们要为变量赋予一定的初值。对于复合类型,我们应该首选Go提供的复合字面值作为初值构造器。对于不同复合类型,我们要记住下面几点:

■使用field:value形式的复合字面值为结构体类型的变量赋初值;

■在为稀疏元素赋值或让编译器推导数组大小的时候,多使用index:value的形式为数组/切片类型变量赋初值;

■使用key:value形式的复合字面值为map类型的变量赋初值。(Go 1.5版本后,复合字面值中的key和value类型均可以省略不写。) oZzbKmWZeRJ8jh8QGXx2U7IA88P81TSDh6KxToIEf4ABvzftyj6Dbo7bX5dyWM/1

点击中间区域
呼出菜单
上一章
目录
下一章
×