上周末翻完了《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")
}
数组, 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
, b
和 c
类型相同,都是“包含 4 个 int 元素的数组”。其中 a
中的四个元素被初始化了四个不同的值,b
和 c
中的元素都是零值。
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
时,整个数组都被复制了一遍,之后 x
和 c
就没关联了,修改 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]
d
和 y
的地址不同,不是同一个 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: "[email protected]"}, role: "admin"}
fmt.Println(larry)
Post(&larry)
}
输出结果是
{{Larry [email protected]} admin}
Author Larry write an article
由于内部类型的提升,内部类型实现的接口会自动提升到外部类型。这意味着由于内部类型的实现,外部类型也同样实现了这个接口。
具体到上面代码中,外部类型是 admin
,内部类型是 user
。admin
并没有实现 write
方法,调用了 use
的 write
方法。如果外部类型实现了 write
方法,就不会去调用内部类型的 write
方法了。
func (a *admin) write() {
fmt.Printf("Admin %s write an article\n", a.name, a.email)
}
输出就变成了
{{Larry [email protected]} admin}
Admin Larry write an article
关键字 iota
这是个有点古怪的关键字。乍一看我以为是 itoa
, atoi
之类的函数,其实它是在生命 const
变量时用的,itoa
起到自增的作用。
In some programming languages (e.g., A+, APL, C++, Go[6]) iota (either as a lowercase symbol
ι
or as a keywordiota
) is used to represent and generate an array of consecutive integers. For example, in APLι4
gives1 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
)
每种编程语法都可能有些坑,但是通常来说,刻意去制造出现“坑”的上下文,刻意纠结语法细节, 是没有必要的,甚至是有害的。在实际工作中,这是种要尽量避免的情况。
对于编程语言的学习,最好的教程还是官方的各种文档,不是要一气看完,边写边查也未尝不可。写得多了,自然水到渠成。