专栏目录
11 理解包导入路径的含义 44 利其器!Go常用工具大检阅 22 Go 并发模型和常见并发模式 35 告别乱码!GO语言字符集编码方案间转换 12 go语言 init 函数的妙用 41 与时俱进!使用module管理依赖包 1 参考 Go 项目布局设计你的项目结构 16 方法集合决定接口实现 17 go变长参数函数的妙用 7 go语言定义“零值可用”的类型 30 Go 惯例:将测试依赖的外部数据文件放在 testdata 下面 34 一文告诉你如何在 Go 中实现 HTTPS 通信 24 sync 包的正确使用姿势 6 Go“枚举常量”的惯用实现方法 4 变量声明形式尽量保持一致 2 gofmt:Go代码风格的唯一标准 3 Go 标识符的命名惯例 9 深入理解和高效运用切片 10 Go 字符串是原生类型 8 用复合字面值作初值构造器 13 Go 函数是“一等公民”的理解 14 defer 让你的代码更清晰 23 Go channel 的常见使用模式 18 定义小接口是 Go 的惯例 19 不要在函数参数中使用空接口(interface{}) 15 Go 方法的本质 21 面试必考!掌握 goroutine 的调度原理 29 Go 单元测试惯例:表驱动 26 if err != nil 重复太多可以这么办 27 不要让 panic 掺和到正常错误处理中 32 掌握 Go 代码性能剖析神器:pprof 31 为被测对象建立性能基准 25 别笑!这就是 Go 的错误处理哲学 39 慎用reflect包提供的反射能力 38 小心被kill!不要忽略对系统信号的处理 36 像极!bytes包和strings包的那些相似操作 33 掌握 Go 代码调试利器:delve 40 与C互操作不是免费的!疑问了解cgo的使用成本 43 让你的Go包拥有个性化的导入路径 37 time包,你用对了吗 42 小即是美?构建最小Go程序容器镜像 20 要提高代码可测试性,请使用接口 45 未雨绸缪!Go语言常见“坑”大汇 28 一文告诉你测试包的包名要不要带“\_test”后缀 5 无类型常量让代码更简化

8 用复合字面值作初值构造器

九路
• 阅读 597

在上一节中,我们了解到了“零值可用”对于编写出符合 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 类型的变量赋初值。
点赞
收藏
评论区
推荐文章

暂无数据