上周末翻完了《Go 语言实战》这本书,还不错,篇幅不大,内容实用。书中有很多内容是这样写的:先给出一大段代码,然后一点一点拆解分析,稍微有些啰嗦。我先把 A Tour of Go 过了一遍,再看这本书算是更深入了一点。

看书过程中,标注了一些之前忽略或者生僻或者需要刻意重复练习的——或者是有趣的——知识点(自然是不全面的),整理在这里。

init 函数

程序中每个代码文件里的 init 函数都会在 main 函数执行前调用。

一个常见的用途:在 init 函数里配置日志参数,让程序一开始就能用 log 包按格式进行输出。

强制风格

为了让程序的可读性更强,Go 编译器不允许声明导入某个包却不使用。下划线 _ 让编译器接受这类导入,并且调用对应包内的所有代码文件里定义的 init 函数。

不仅不允许导入包却不使用,也不允许声明了变量后不使用。

Go 在语法格式上也有一些区别于其他语言的强制规定

  • 行尾不能加分号
  • 左大括号 { 不能另起一行
  • if 判断式 和 for 循环不用小括号括起来

与其让编译器告警,不如直接失败更有意义。

违法了上面提到的各项规则,都会让代码编译失败。

命名导入

如果导入的包有重名的,或者说是要导入两个一样名字的包(我觉得这通常不是什么好事),可以给导入的包另起一个名字,来解决命名冲突。

package main

import (
    "fmt"
    myfmt "mylib/fmt"
)

func main() {
    fmt.Println("Standard Library")
    myfmt.Println("mylib/fmt")
}

Go

数组, slice, map

数组和 slice

一旦声明,数组里存储的数据类型和数组长度就不能改变了。

同样类型的数组可以赋值给另一个数组。

这里说的同样类型,是指数组长度和每个元素的类型都相同。例如下面例子中的数组 a,它的类型是“拥有 4 个 int 元素的数组”。

根据内存和性能来看,在函数间传递数组是一个开销很大的操作。

如果函数的参数是数组,在函数间传递数组时,整个数组都会被复制一遍。

slice 作为函数参数时,不用担心像数组那样的开销。

由于与 slice 关联的数据包含在底层数组里,不属于 slice 本身,所以将 slice 复制到任意函数的时候,对底层数组大小都不会有影响。

一个例子

package main

import "fmt"

func main() {
    a := [4]int{1, 2, 3, 4}
    fmt.Printf("type of a, %T\n", a)
    fmt.Println("a", a)

    b := [4]int{}
    fmt.Printf("type of b, %T\n", b)
    fmt.Println("b", b)

    var c [4]int
    fmt.Printf("type of c, %T\n", c)
    fmt.Println("c", c)

    d := []int{1, 2, 3, 4}
    fmt.Printf("type of d, %T\n", d)
    fmt.Println("d", d)

    x := c
    x[2] = 100
    fmt.Printf("address of c, %p\n", &c)
    fmt.Printf("address of x, %p\n", &x)
    fmt.Println("c", c)
    fmt.Println("x", x)

    y := d
    y[2] = 100
    fmt.Printf("address of d, %p\n", &d)
    fmt.Printf("address of y, %p\n", &y)
    fmt.Printf("address of d[0], %p\n", &d[0])
    fmt.Printf("address of y[0], %p\n", &y[0])
    fmt.Println("d", d)
    fmt.Println("y", y)
}

a, bc 类型相同,都是“包含 4 个 int 元素的数组”。其中 a 中的四个元素被初始化了四个不同的值,bc 中的元素都是零值。

d 是一个 []int 类型的 slice

x := c
x[2] = 100
fmt.Printf("address of c, %p\n", &c)
fmt.Printf("address of x, %p\n", &x)
fmt.Println("c", c)
fmt.Println("x", x)

这一段代码的输出是

address of c, 0xc4200a80a0
address of x, 0xc4200a8120
c [0 0 0 0]
x [0 0 100 0]

可以看出把数组 c 复制给 x 时,整个数组都被复制了一遍,之后 xc 就没关联了,修改 x 中元素的值,不影响 c

y := d
y[2] = 100
fmt.Printf("address of d, %p\n", &d)
fmt.Printf("address of y, %p\n", &y)
fmt.Printf("address of d[0], %p\n", &d[0])
fmt.Printf("address of y[0], %p\n", &y[0])
fmt.Println("d", d)
fmt.Println("y", y)

这段代码的输出是

address of d, 0xc4200a6020
address of y, 0xc4200a6080
address of d[0], 0xc4200a8100
address of y[0], 0xc4200a8100
d [1 2 100 4]
y [1 2 100 4]

dy 的地址不同,不是同一个 slice,但是它们的底层数据是一样的,所以 d[0]y[0] 的地址相同,修改 y 中元素的值,也修改了 b 中元素的值,反之亦然。

map

map 的键可以是内置类型,也可以是结构体,只要这个值能用 == 运算符做比较。slice、函数以及包含 slice 的结构体,具有引用语义,不能作为 map 的键。

map 的值可以是任意类型,任意的值。

range 返回的不是对元素的引用

range 迭代(遍历)slice 时,range 创建了每个元素的副本,而不是直接返回对该元素的引用。

package main

import "fmt"

func main() {
    x := make(map[string]string)

    x["hello1"] = "world1"
    x["hello2"] = "world2"
    x["hello3"] = "world3"

    var y []*string

    for _, v := range x {
        y = append(y, &v)
    }

    fmt.Println(y)

    var sy []*string
    sx := []string{"world1", "world2", "world3"}
    for _, v := range sx {
        sy = append(sy, &v)
    }
    fmt.Println(sy)
}

下面是我电脑上的一次输出结果,可以看到每次迭代的 v 的地址是一样的。

[0xc42009a030 0xc42009a030 0xc42009a030]
[0xc42009a050 0xc42009a050 0xc42009a050]

golang 面试题这里的第二个题目与这个点相关。

嵌入类型

嵌入类型是将已有的类型直接声明在新的结构类型里

package main

import "fmt"

type writer interface {
        write()
}

type author struct {
        name  string
        email string
}

type admin struct {
        // 如果写成下面一行这样,就不是“嵌入类型”了,代码执行结果跟预期必然不一致
        // author author
        author
        role string
}

func (a *author) write() {
        fmt.Printf("Author %s write an article\n", a.name, a.email)
}

func Post(w writer) {
        w.write()
}

func main() {
        larry := admin{author: author{name: "Larry", email: "larry@example.com"}, role: "admin"}
        fmt.Println(larry)
        Post(&larry)
}

输出结果是

{{Larry larry@example.com} admin}
Author Larry write an article

由于内部类型的提升,内部类型实现的接口会自动提升到外部类型。这意味着由于内部类型的实现,外部类型也同样实现了这个接口。

具体到上面代码中,外部类型是 admin,内部类型是 useradmin 并没有实现 write 方法,调用了 usewrite 方法。如果外部类型实现了 write 方法,就不会去调用内部类型的 write 方法了。

func (a *admin) write() {
   fmt.Printf("Admin %s write an article\n", a.name, a.email)
}

输出就变成了

{{Larry larry@example.com} admin}
Admin Larry write an article

关键字 iota

这是个有点古怪的关键字。乍一看我以为是 itoaatoi 之类的函数,其实它是在生命 const 变量时用的,itoa 起到自增的作用。

In some programming languages (e.g., A+, APL, C++, Go[6]) iota (either as a lowercase symbol ι or as a keyword iota) is used to represent and generate an array of consecutive integers. For example, in APL ι4 gives 1 2 3 4.

Ultimate Visual Guide to Go Enums 这篇文章有很详细的讲述。

Golang WiKi 上提到的两个例子:

type ByteSize float64

const (
    _           = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)

Weekday enum example - How iota is calculated


每种编程语法都可能有些坑,但是通常来说,刻意去制造出现“坑”的上下文,刻意纠结语法细节, 是没有必要的,甚至是有害的。在实际工作中,这是种要尽量避免的情况。

对于编程语言的学习,最好的教程还是官方的各种文档,不是要一气看完,边写边查也未尝不可。写得多了,自然水到渠成。