Coroutines for Go(草稿)

参考 Coroutines for GoCoroutine Wiki

什么是协程(coroutine)?通常使用的函数(function)也被称为子例程(subroutine),一系列调用会形成一个调用栈(call stack),调用者(caller)和被调用者(callee)是父子关系。而协程不同,协程之间是对等关系,每个协程都有一个调用栈。协程有非对称和对称两种实现,非对称协程使用 resumeyield 关键字,调用者使用 resume 恢复某个协程,被调用者使用 yield 暂停当前协程,然后控制会转移到调用者。对称协程只使用 yield 关键字,但是需要指定将控制转移给哪个协程。经典的例子,比较两个二叉树是否包含相同的序列(中序遍历),代码

协程的控制转移是主动的(非抢占式),不需要操作系统支持,也不需要使用锁和信号量等同步原语。线程的控制转移是被动的(抢占式),由操作系统调度,上下文切换更加昂贵,需要使用同步原语保护共享变量。协程只提供并发性,而线程可以利用多核 CPU 实现并行。切换的速度,协程最多 10 纳秒,线程几微秒,GoroutinesVirtual Threads(Java 21) 几百纳秒。

网络上各种术语的解释很混乱,根据多线程模型,我倾向于使用用户线程和内核线程的对应关系,来描述不同的实现。简单描述一下我的理解:平常使用的是一对一模型,Goroutines 和 Virtual Threads 使用的是多对多模型。协程不能简单的看作多对一模型,协程是非抢占式的用户线程,描述的是多个用户线程之间的协作关系,实际上可以在各个模型上实现协程。


关于 Goroutines 的实现,可以看 Dmitry Vyukov 的演讲 Go scheduler: Implementing language with lightweight concurrency。其他资源:Scalable Go Scheduler Design DocThe Go schedulerHACKING

简单的设计,使用一对一模型 + 线程池,缺点是线程的内存占用较大,Goroutine 阻塞会导致线程阻塞,没有“无限数量”的栈。所以,使用多对多模型,Goroutine 占用内存更小,可以被 Go runtime 完全控制。如果 Goroutine 因为锁/通道/网络 IO/计时器而阻塞,Goroutine 将会进入阻塞队列,运行此 Goroutine 的内核线程不会阻塞,Go runtime 可以从 Run Queue 中调度 Runnable 的 Goroutine 到该内核线程上(复用,Multiplex)。

但是,当 Goroutine 进行系统调用,控制将从 Goroutine 转移到系统调用处理程序,Go runtime 是无法感知该处理流程的,直到系统调用返回,所以此时运行 Goroutine 的内核线程是无法被复用的。有可能所有内核线程都阻塞在系统调用上,而该系统调用所需的资源被某个 Runnable 的 Goroutine 持有,从而发生死锁。所以在系统调用发生时总是会创建/唤醒一个内核线程,执行 Run Queue 中的 Goroutine。当内核线程从系统调用返回,Go runtime 将内核线程上的 Goroutine 放入 Run Queue,使该内核线程空闲从而保证指定的并行度(由 GOMAXPROCS 指定)。

关于 GOMAXPROCS,runtime 文档的描述如下:The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit.

该实现的瓶颈在全局的互斥锁(MUTEX),内核线程创建 Goroutine 以及获取 Goroutine 都需要操作共享的 Run Queue。解决方案很容易想到,就是为每个内核线程创建本地变量,从而避免频繁访问全局的共享变量。该方案会增加获取下一个 Goroutine 的复杂性,Go 调度器实现的获取顺序是, Local Run Queue、Global Run Queue、Network Poller、Work Stealing。

由于发生系统调用时会创建/唤醒内核线程,也就是说内核线程的数量会多于 CPU 的核心数量。新的调度器为每个内核线程分配本地资源,但是实际上执行 Go 代码的内核线程的数量是固定的(由 GOMAXPROCS 指定),所以空闲线程不应该持有资源,会造成资源浪费以及降低 Work Stealing 的效率。所以,设计上引入一个新的实体,也就是处理器(Processor),从而调度模型从 GM 变为 GMP。Go runtime 不会为每个内核线程分配资源,而是为 Processor 分配资源,Processor 的数量就是 CPU 的核心数量。在新的模型中,当 Goroutine 发生系统调用时,Goroutine 会创建/唤醒新的内核线程,然后将 Processor 对象交给新的内核线程。

目前,调度器已经足够好,不过可以更好。公平性(Fairness)和性能的权衡:设计者想要以最小的性能开销获得最小的公平性。FIFO 队列可以一定程度上保证公平性,但是如果当前 Goroutine 陷入无限循环,队列中的 Goroutine 将会饥饿,所以设计者使用 10 ms 的时间片轮转调度(时分共享,抢占式)。另一方面,FIFO 队列缺少局部性(影响性能),最后进入队列的 Goroutine 会在最后运行。常见的场景,当前 Goroutine 创建另一个 Goroutine,然后自身被阻塞等待另一个 Goroutine 执行。缺少局部性的 FIFO 会产生很大延迟,所以设计者在 Local Run Queue 的尾部添加一个单元素的 LIFO 缓冲区,每次获取 Goroutine 都会首先从缓冲区中获取(Direct Switch)。

该设计引入额外两个问题,一个是其他内核线程从当前内核线程 Work Stealing 时,将 LIFO 中的 Goroutine 窃取,影响 Direct Switch 的执行。解决方案是只有 Goroutine 被放入 LIFO 超过 3 μs 才能被窃取。另一个问题是,不断创建 Goroutine 会导致 LIFO 缓冲区总是有元素,从而 FIFO 队列中的 Goroutine 会饥饿,解决方案是当前 Goroutine 和 LIFO 中的 Goroutine 共享同一个 10 ms 的时间片。

如果 Local Run Queue 一直不为空,Global Run Queue 会饥饿。所以,假设当前是第 schedTick 次获取,设计者设置当 schedTick % 61 == 0 时,优先从 Global Run Queue 获取 Goroutine。为什么使用 61,因为 61 不大不小,太大会饥饿,太小会因为 Global Run Queue 的 MUTEX 限制性能,并且参考哈希表的设计,使用质数而不是 2 的幂会更随机/公平。

最后,Network Poller 可能会饥饿,解决方案是使用后台线程从中定期获取 Goroutine。之所以不像处理 Global Run Queue 饥饿一样在当前线程中获取,是因为从 Network Poller 获取 Goroutine 涉及到 epoll_wait() 系统调用(很慢)。


Go 基础

参考官方网站GOPLSTD源代码Go 语言设计与实现

Tutorial

Hello, World

使用 go run 编译运行程序,go build 编译程序,go doc 查看文档。

某些标记之后的换行符会被转换为分号,因此换行符的位置对于正确解析 Go 代码至关重要。例如,函数的左括号 { 必须与函数声明的结尾在同一行,否则会报错 unexpected semicolon or newline before {。而在表达式 x + y 中,可以在 + 运算符之后换行,但不能在之前换行。

1
2
3
func main() {
fmt.Println("Hello, 世界")
}

Command-Line Arguments

可以使用 os.Args 变量获取命令行参数,该变量是一个字符串切片。os.Args[0] 是命令本身,剩余元素是程序启动时用户传递的参数。使用 var 声明语句定义变量,变量可以在声明时进行初始化。如果未显式初始化,则隐式初始化为该类型的零值(zero value),数值类型为 0,字符串类型为空串 ""

for 语句是 Go 中唯一的循环语句,可以充当其他语言中常规的 forwhile 循环以及无限循环。Go 语言不允许未使用的局部变量,否则会报错 declared and not used。使用 += 在循环中拼接字符串的开销较大,每次都会生成新字符串,而旧字符串则不再使用等待 GC,可以使用 strings.Join 方法提升性能,一次性拼接所有字符串。

1
2
3
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}

以下几种声明变量的方式都是等价的。第一种简洁但只能在函数中使用,而不能用于包级变量,第二种使用默认初始化,第三种形式仅在声明多个变量时使用,第四种仅在声明类型和初始值类型不同时使用。

1
2
3
4
s := ""
var s string
var s = ""
var s string = ""

Finding Duplicate Lines

input.Scan() 读取下一行数据并移除行尾的换行符,可以调用 input.Text() 方法获取结果。map 的迭代顺序未明确指定,但在实际操作中是随机的,这种设计是有意为之,防止程序依赖特定的顺序。map 是一个由 make 创建的数据结构的引用(reference),当将 map 作为参数传递时,函数会收到引用的副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func main() {
counts := make(map[string]int)
files := os.Args[1:]
if len(files) == 0 {
countLines(os.Stdin, counts)
} else {
for _, arg := range files {
f, err := os.Open(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
continue
}
countLines(f, counts)
f.Close()
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}

func countLines(f *os.File, counts map[string]int) {
input := bufio.NewScanner(f)
for input.Scan() {
counts[input.Text()]++
}
// NOTE: ignoring potential errors from input.Err()
}

Animated GIFs

const 常量的值必须是数字、字符串或者布尔值。以下程序使用 web 方式可以正常显示图像,但是如果使用 ./main > out.gif 重定向输出的方式,则在 Windows 中不能正常显示。因为 Windows 标准输出默认以文本模式处理数据,会修改输出的数据从而导致图像损坏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
var palette = []color.Color{color.White, color.Black}

const (
whiteIndex = 0 // first color in palette
blackIndex = 1 // next color in palette
)

func main() {
//!-main
// The sequence of images is deterministic unless we seed
// the pseudo-random number generator using the current time.
// Thanks to Randall McPherson for pointing out the omission.
rand.Seed(time.Now().UTC().UnixNano())

if len(os.Args) > 1 && os.Args[1] == "web" {
//!+http
handler := func(w http.ResponseWriter, r *http.Request) {
lissajous(w)
}
http.HandleFunc("/", handler)
//!-http
log.Fatal(http.ListenAndServe("localhost:8000", nil))
return
}
//!+main
lissajous(os.Stdout)
}

func lissajous(out io.Writer) {
const (
cycles = 5 // number of complete x oscillator revolutions
res = 0.001 // angular resolution
size = 100 // image canvas covers [-size..+size]
nframes = 64 // number of animation frames
delay = 8 // delay between frames in 10ms units
)
freq := rand.Float64() * 3.0 // relative frequency of y oscillator
anim := gif.GIF{LoopCount: nframes}
phase := 0.0 // phase difference
for i := 0; i < nframes; i++ {
rect := image.Rect(0, 0, 2*size+1, 2*size+1)
img := image.NewPaletted(rect, palette)
for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
blackIndex)
}
phase += 0.1
anim.Delay = append(anim.Delay, delay)
anim.Image = append(anim.Image, img)
}
gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors
}

Fet ching URLs Concurrently

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func main() {
start := time.Now()
ch := make(chan string)
for _, url := range os.Args[1:] {
go fetch(url, ch) // start a goroutine
}
for range os.Args[1:] {
fmt.Println(<-ch) // receive from channel ch
}
fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}

func fetch(url string, ch chan<- string) {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprint(err) // send to channel ch
return
}

nbytes, err := io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close() // don't leak resources
if err != nil {
ch <- fmt.Sprintf("while reading %s: %v", url, err)
return
}
secs := time.Since(start).Seconds()
ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)
}

A Web Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

//!+handler
// handler echoes the HTTP request.
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
fmt.Fprintf(w, "Host = %q\n", r.Host)
fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
if err := r.ParseForm(); err != nil {
log.Print(err)
}
for k, v := range r.Form {
fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
}
}

Loose Ends

switch 语句的 case 不需要显式使用 break,默认不会 fallthrough 到下一个 case。可以省略 switch 之后的操作数,此时等价于 switch true。和 forif 一样,在 switch 之后可以跟一个简单语句。

1
2
3
4
5
6
7
8
9
10
func Signum(x int) int {
switch {
case x > 0:
return +1
case x < 0:
return -1
default:
return 0
}
}

Program Structure

Names

名称以字母(Unicode 字母)或下划线开头,可以包含任意数量的字母、数字和下划线,大小写敏感。关键字(keywords)不可作为名称被使用,而预声明(predeclared)的名称(内置常量、类型和函数)可以重新声明,但是存在混淆的风险。在函数内声明的实体仅在函数内可见,在函数外声明的实体包可见,名称的首字母大写则在包外可见。包名总是小写的,实体命名使用驼峰命名法。

Declarations

声明(declaration)命名一个程序实体,有四种主要的声明类型,varconsttypefunc。Go 程序存储在以 .go 为后缀的文件中,每个文件开头都有包声明,之后是导入声明,然后是以任意顺序排列的包级(package-level)的类型、变量、常量和函数声明。函数的返回值列表是可选的(多个返回值构成列表),如果不返回任何值则可以省略。

Variables

变量声明的通用形式为 var name type = expression。如果省略 type 则类型由表达式推断,如果省略 = expression 则必须显式指定类型,初始值为该类型的零值。数值类型为 0,字符串类型为空串 "",布尔类型为 false,接口和引用类型(切片、指针、哈希表、通道和函数)为 nil。像数组或者结构体聚合类型的元素或字段的零值就是自身的零值。

零值机制确保变量始终有其类型所定义的明确值,Go 语言中不存在未初始化变量的概念。可以同时声明一组变量,如果省略类型则可以同时声明不同类型的变量。包级变量会在 main 函数开始之前初始化,局部变量在声明时初始化。

1
2
3
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
var f, err = os.Open(name) // os.Open returns a file and an error

Short Variable Declarations

在函数中可以使用简短变量声明(short variable declaration)的形式声明和初始化局部变量,形式为 name := expression。简短变量声明常用于声明和初始化大多数局部变量,而 var 声明常用于变量类型和表达式类型不同、或者稍后赋值且初始值不重要的局部变量。

1
2
3
i := 100 // an int
var boiling float64 = 100 // a float64
i, j = j, i // swap values of i and j

区分 := 是声明而 = 是赋值。需要注意,如果简短变量声明中的变量已经在相同词法块(lexical block)中被声明过,那么该声明相当于对该变量赋值。而且简短变量声明必须至少声明一个新变量,否则代码将无法通过编译。

1
2
3
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables

Pointers

指针(pointer)是变量的地址(address),可以通过指针间接地读写变量的值,而无需知晓变量的名称。指针的零值是 nil,如果指针指向某个变量则其值必然不为 nil。两个指针相等仅当它们指向相同的变量或者都为 nil

1
2
3
4
5
x := 1
p := &x // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2 // equivalent to x = 2
fmt.Println(x) // "2"

函数返回局部变量的地址是安全的,即使函数调用返回该局部变量 v 仍会存在。由编译器逃逸分析确定,该变量会在堆上分配。根据静态分析知识,为保证安全性,分析肯定是偏向误报(Sound)而不是漏报(Complete)。每次调用函数 f 返回的值都不同。每次获取变量的地址或者复制指针时,都会为该变量创建新的别名(aliases),*pv 的别名。

1
2
3
4
5
var p = f()
func f() *int {
v := 1
return &v
}
1
2
3
4
5
6
7
8
9
10
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")

func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}

The new Function

另一种创建变量的方式是使用内置函数 new,表达式 new(T) 创建类型为 T 的未命名变量,将其初始化为类型 T 的零值,返回类型为 *T 的地址值。使用 new 创建的变量和普通局部变量没有区别,只是后者需要显式获取地址。

1
2
3
4
5
6
7
8
func newInt() *int {
return new(int)
}

func newInt() *int {
var dummy int
return &dummy
}

通常每次调用 new 都会返回具有唯一地址的不同变量,例外情况是,如果两个变量的类型不携带任何信息且大小为零(例如 struct{}[0]int),则根据实现的不同可能会具有相同的地址(实测得到的是不同地址)。由于 new 是内置函数而不是关键字,所以可以被重新定义为其他东西,不过此时不能在 delta 中使用内置的 new 函数。

1
func delta(old, new int) int { return new - old }

Lifetime of Variables

变量的声明周期是指其在程序执行过程中的存活时间。包级变量在整个程序执行过程中存活,局部变量在声明时创建,在不被引用时回收(GC 可达性分析)。因为变量的生命周期取决于可达性,所以局部变量在函数返回之后仍有可能存活。编译器会决定将变量分配到堆中还是栈中,这一决定并非取决于使用 var 还是 new 来声明变量(Pointers 小节中提到过的逃逸分析)。例如,下面示例中 x 必须在堆上分配,而 y 可以在栈上分配。

1
2
3
4
5
6
7
8
9
10
11
12
var global *int

func f() {
var x int
x = 1
global = &x
}

func g() {
y := new(int)
*y = 1
}

Assignments

Go 语言中仅有后置 x++x-- 而没有前置写法,而且该操作被视为语句而不是表达式,所以不能将其赋值给变量或者参与运算。

Tuple Assignment

元组赋值(tuple assignment)允许一次为多个变量赋值,在对任何变量更新之前,所有右侧表达式都会被计算出来。如果函数具有多个返回值,则赋值语句左侧必须包含相同数量的变量,可以使用 _ 忽略不需要的值。

1
2
x, y = y, x
_, err = io.Copy(dst, src) // discard byte count

Assignability

当值能够赋值给变量的类型时,该赋值操作才是合法的,不同类型有不同的赋值规则。只有当两个变量可以相互赋值时,它们才能够使用 ==!= 进行比较。(Java 中引用类型之间总是可以使用该运算符相互比较)

Type Declarations

类型声明形如 type name underlying-type,用于定义一个新的具有某个底层类型(underlying type)的命名类型(named type)。即使两个类型具有相同的底层类型,它们也是不同的类型,不能直接比较或组合,而需要使用 T(x) 进行显式类型转换。命名类型将底层类型的不同使用方式区分开来,避免不同使用方式之间混淆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Celsius float64
type Fahrenheit float64

const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

Packages and Files

在 Go 语言中,包的作用与其他语言中的库或模块相同,支持模块化、封装、独立编译和代码复用。一个包的源代码在一个或多个 .go 文件中,通常这些文件位于一个目录中,该目录的名称以导入路径结尾。例如 gopl.io/ch1/helloworld 包的文件存储在 $GOPATH/src/gopl.io/ch1/helloworld 目录中。

Imports

每个包都由其导入路径标唯一标识,包名要求和导入路径的最后一部分相同,例如 gopl.io/ch2/tempconv 导入路径的包名是 tempconv。不同导入路径的包名可能冲突,可以在导入时指定别名来避免冲突。

Package Initialization

包初始化首先会按照声明顺序初始化包级变量,如果包中有多个 .go 文件,go 工具会对按照文件名对文件进行排序,然后调用编译器,文件按照传入编译器的顺序进行初始化。如果变量之间存在依赖关系,则会优先按照依赖关系初始化。

1
2
3
var a = b + c // a initialized third, to 3
var b = f() // b initialized second, to 2, by calling f
var c = 1 // c initialized first, to 1

可以使用 init 函数来初始化包级变量,每个文件都可以包含任意数量的 init 函数,按照声明顺序执行。包按照导入顺序依次初始化,如果包之间存在依赖关系,则会优先按照依赖关系初始化。main 包最后被初始化,从而保证 main 函数开始之前所有包都完成初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// pc[i] is the population count of i.
var pc [256]byte

func init() {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
}

// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) int {
return int(pc[byte(x>>(0*8))] +
pc[byte(x>>(1*8))] +
pc[byte(x>>(2*8))] +
pc[byte(x>>(3*8))] +
pc[byte(x>>(4*8))] +
pc[byte(x>>(5*8))] +
pc[byte(x>>(6*8))] +
pc[byte(x>>(7*8))])
}

func BitCount(x uint64) int {
// Hacker's Delight, Figure 5-2.
x = x - ((x >> 1) & 0x5555555555555555)
x = (x & 0x3333333333333333) + ((x >> 2) & 0x3333333333333333)
x = (x + (x >> 4)) & 0x0f0f0f0f0f0f0f0f
x = x + (x >> 8)
x = x + (x >> 16)
x = x + (x >> 32)
return int(x & 0x7f)
}

func PopCountByClearing(x uint64) int {
n := 0
for x != 0 {
x = x & (x - 1) // clear rightmost non-zero bit
n++
}
return n
}

func PopCountByShifting(x uint64) int {
n := 0
for i := uint(0); i < 64; i++ {
if x&(1<<i) != 0 {
n++
}
}
return n
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Go 1.6, 2.67GHz Xeon
// $ go test -cpu=4 -bench=. gopl.io/ch2/popcount
// BenchmarkPopCount-4 200000000 6.30 ns/op
// BenchmarkBitCount-4 300000000 4.15 ns/op
// BenchmarkPopCountByClearing-4 30000000 45.2 ns/op
// BenchmarkPopCountByShifting-4 10000000 153 ns/op
//
// Go 1.6, 2.5GHz Intel Core i5
// $ go test -cpu=4 -bench=. gopl.io/ch2/popcount
// BenchmarkPopCount-4 200000000 7.52 ns/op
// BenchmarkBitCount-4 500000000 3.36 ns/op
// BenchmarkPopCountByClearing-4 50000000 34.3 ns/op
// BenchmarkPopCountByShifting-4 20000000 108 ns/op
//
// Go 1.7, 3.5GHz Xeon
// $ go test -cpu=4 -bench=. gopl.io/ch2/popcount
// BenchmarkPopCount-12 2000000000 0.28 ns/op
// BenchmarkBitCount-12 2000000000 0.27 ns/op
// BenchmarkPopCountByClearing-12 100000000 18.5 ns/op
// BenchmarkPopCountByShifting-12 20000000 70.1 ns/op

Scope

区分作用域(Scope)和生命周期(lifetime),声明的作用域是指程序文本中的一个区域,是编译时的属性。而变量的生命周期是指程序执行期间,其他部分可以引用该变量的时间范围,是运行时的属性。

语法块(syntactic block)是指由花括号包围的语句块,词法块(lexical block)是对语法块概念的泛化,以涵盖未明确使用花括号包围的其它声明组合。整个源代码也是词法块,被称为宇宙块(universe block)。程序可以包含多个同名的声明,只要声明在不同词法块中。当编译器遇到对某个名称的引用,它会从最内层的词法块开始向外找,直到宇宙块为止。

1
2
3
4
5
6
7
8
if x := f(); x == 0 {
fmt.Println(x)
} else if y := g(x); x == y {
fmt.Println(x, y)
} else {
fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here

Basic Data Types

Go 语言的类型分为四类:基本类型、聚合类型、引用类型和接口类型。基本类型包括数值类型、字符串和布尔类型,聚合类型包括数组和结构体,引用类型包括指针、切片、哈希表、函数和通道。

Integers

数值类型包括整数、浮点数和复数。Go 提供有符号和无符号整数运算,它们分别有四种不同大小的类型,int8, int16, int32, int64uint8, uint16, uint32, uint64。还有 intuintuintptr 类型,通常为 32 位或 64 位,具体位数由编译器决定。

runeint32 等价,通常用于表示 Unicode 码点。byteuint8 等价,通常用于表示字节数据。在 Go 语言中,% 运算符得到的余数符号总是和被除数相同。没有 ~x 按位取反,而是使用 ^x 执行按位取反。对于有符号数而言,>> 会使用符号位填充。

Floating-Point Numbers

浮点类型有 float32float64 两种,遵循 IEEE 754 标准。32 位浮点数的小数精度大约是 6 位,64 位浮点数的精度大约是 15 位。

1
2
var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"

Complex Numbers

有两种复数类型 complex64complex128,它们的元素分别是 float32float64 类型。可以使用内置函数 complexrealimag 处理复数,可以直接使用 i 来声明复数的虚部。

1
2
3
4
5
var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(-5+10i)"
fmt.Println(real(x*y)) // "-5"
fmt.Println(imag(x*y)) // "10"

Strings

字符串是不可变的字节序列,内置函数 len 返回字符串的字节数量,而不是字符数量,索引操作 s[i] 获取字符串 s 的第 i 个字节。字符串的第 i 个字节不一定就是第 i 个字符,因为非 ASCII 码点的 UTF-8 编码需要多个字节。使用 s[i:j] 可以获取子字符串,该子字符串是一个新的字符串,不过和原串共享底层字节数组。由于可以共享底层内存,所以字符串的复制和子串操作的开销很低。

1
2
3
s := "hello, world"
fmt.Println(len(s)) // "12"
fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')

可以在字符串中插入转义序列,特别地,有十六进制转义 \xhh 和八进制转义 \ooo,其中 hhooo 表示十六进制和八进制数字,八进制数不超过 \377 对应十进制 255。原始字符串字面量(raw string literal)使用的是反引号而不是单引号,输出时不会处理原始字符串字面量中的转义序列。 例外情况是,当原始字符串字面量跨多行编写时,会删除回车符 \r,从而使字符串在所有平台上保持一致。

1
2
3
4
const GoUsage = `Go is a tool for managing Go source code.
Usage:
go command [arguments]
...`

Unicode

Unicode 是字符编码标准,每个字符对应一个 Unicode 码点,在 Go 中使用 rune 表示(int32 的同义词),即使用 UTF-32 编码方式定长存储 Unicode 码点,但是这样空间开销较大。

UTF-8

UTF-8 变长编码(由 Go 语言开发者发明)使用 1 到 4 字节表示 rune,第一个字节的高位指示当前 rune 使用多少字节表示。变长编码无法直接通过索引访问字符串中的第 n 个字符,但这种编码方式空间有诸多优点。空间占用小,和 ASCII 兼容,自同步(可以通过后退不超过 3 个字节找到字符的开头),从左向右解码不会有歧义。任何字符的编码都不是其他一个或多个字符编码的子串,因此可以通过查找字节来查找字符,而无需担心前面的上下文。字典序和 Unicode 码点顺序相同,不存在嵌入的 NUL 字节(零字节),对于使用 NUL 终止字符串的语言来说非常方便。

Go 的源文件始终使用 UTF-8 编码方式,UTF-8 是 Go 程序处理文本字符串的首选编码方式。可以在字符串中使用 \uhhhh 表示 16 位码点,使用 \Uhhhhhhhh 表示 32 位码点,其中 h 是十六进制数字,每种形式都表示码点的 UTF-8 编码。例如,以下字符串字面量都表示相同的 6 字节字符串。

1
2
3
4
"世界"
"\xe4\xb8\x96\xe7\x95\x8c"
"\u4e16\u754c"
"\U00004e16\U0000754c"

rune 字面量中也可以使用 Unicode 转义字符,下面三种字面量是等价的。对于值小于 256 的 rune 可以使用单个十六进制转义字符表示,例如 '\x41' 表示 'A'。但是更大的值必须使用 \u\U 转义,'\xe4\xb8\x96' 不是合法的 rune 字面量,即使这三个字节是单个码点的 UTF-8 编码。

1
'世' '\u4e16' '\U00004e16'

由于 UTF-8 的特性,许多字符串操作都无需解码。可以使用以下方法检验一个字符串是否是另一个字符串的前缀。

1
2
3
func HasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}

字符串 s 占用 13 个字节,包含 9 个码点/字符/rune,要处理字符可以使用 UTF-8 解码器 DecodeRuneInString。不过 Go 的 range 循环在应用于字符串时会自动进行 UTF-8 解码。

1
2
3
4
5
6
7
8
9
10
11
12
13
s := "Hello, 世界"
fmt.Println(len(s)) // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"

for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%d\t%c\n", i, r)
i += size
}

for i, r := range "Hello, 世界" {
fmt.Printf("%d\t%q\t%d\n", i, r, r)
}

如果 UTF-8 解码遇到非法字节,则会生成特殊的 Unicode 替换字符 \uFFFD,显示为白色问号周围环绕黑色六边形或菱形图案。将 UTF-8 编码的字符串转换为 []rune 之后,会得到 Unicode 码点序列,反之亦然。将整数转换为字符串会将其解释为单个 rune 值,然后将其转换为 UTF-8 表示形式,如果对应的 rune 是无效的,则会使用替换字符。

1
2
3
4
5
// "program" in Japanese katakana
s := "プログラム"
fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
r := []rune(s)
fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]"

Strings and Byte Slices

字符串 s 可以使用 []byte(s) 转换为字节切片,然后使用 string(b) 转换回来。两个操作通常都会进行复制操作,以确保 b 的可变性和 s2 的不可变性。bytes 包提供 Buffer 类型,类似 Java 中的 StringBuilder,该类型无需初始化,其零值可以直接使用,因为之后调用的方法中会判断底层切片 buf 是否为 nil,然后为其分配内存。

1
2
3
s := "abc"
b := []byte(s)
s2 := string(b)

Conversions between Strings and Numbers

1
2
3
4
5
6
x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"

x, err := strconv.Atoi("123") // x is an int
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits

Constants

const 常量的底层类型必须是基本类型(和 Java 中的 final 很不一样)。当常量作为一个组声明时,除该组的第一个元素外,剩余元素的右侧表达式可以省略,此时默认会使用之前元素的表达式。

1
2
3
4
5
6
7
const (
a = 1
b
c = 2
d
)
fmt.Println(a, b, c, d) // "1 1 2 2"

The Constant Generator iota

可以使用常量生成器 iota 创建枚举常量组,iota 的值从 0 开始,每次递增 1。

1
2
3
4
5
6
7
8
type Flags uint
const (
FlagUp Flags = 1 << iota // is up
FlagBroadcast // supports broadcast access capability
FlagLoopback // is a loopback interface
FlagPointToPoint // belongs to a point-to-point link
FlagMulticast // supports multicast access capability
)

Untyped Constants

没有指定类型的常量是无类型(untyped)常量,编译器会以比基本类型更高的数值精度来表示无类型常量,并且在其上执行高精度运算而不是机器运算(受限于 CPU 的位数),至少可以假设其具有 256 位精度。例如 ZiBYiB 无法存储在任何整型变量中,但是可以在下面的表达式中使用。有六种无类型常量:untyped boolean、untyped integer、untyped rune、untyped floating-point、untyped complex 和 untyped string。

1
2
3
4
5
6
7
8
9
10
11
12
const (
_ = 1 << (10 * iota)
KiB // 1024
MiB // 1048576
GiB // 1073741824
TiB // 1099511627776 (exceeds 1 << 32)
PiB // 1125899906842624
EiB // 1152921504606846976
ZiB // 1180591620717411303424 (exceeds 1 << 64)
YiB // 1208925819614629174706176
)
fmt.Println(YiB/ZiB) // "1024"

浮点型常量 math.Pi 可以在需要浮点或复数的情况下使用,但是如果为其指定类型 float64,则精度会降低,而且在需要使用 float32complex128 类型的值时需要显式类型转换。

1
2
3
4
5
6
7
8
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

const Pi64 float64 = math.Pi
var x float32 = float32(Pi64)
var y float64 = Pi64
var z complex128 = complex128(Pi64)

只有常量可以不指定类型,当将无类型常量赋值给变量时,该常量会隐式转换为变量的类型。

1
2
3
4
var f float64 = 3 + 0i // untyped complex -> float64
f = 2 // untyped integer -> float64
f = 1e123 // untyped floating-point -> float64
f = 'a' // untyped rune -> float64

无论隐式还是显式转换,在转换时目标类型必须能够表示原始值,对于实数和复数的浮点数,允许四舍五入。

1
2
3
4
5
6
7
8
9
const (
deadbeef = 0xdeadbeef // untyped int with value 3735928559
a = uint32(deadbeef) // uint32 with value 3735928559
b = float32(deadbeef) // float32 with value 3735928576 (rounded up)
c = float64(deadbeef) // float64 with value 3735928559 (exact)
d = int32(deadbeef) // compile error: constant overflows int32
e = float64(1e309) // compile error: constant overflows float64
f = uint(-1) // compile error: constant underflows uint
)

在未显式指定类型的变量声明中,无类型常量的特性会决定变量的默认类型。无类型整型默认转换为 int 类型,无类型浮点数和复数默认转换为 float64complex128。如果要使用其他类型,需要显示类型转换,或者在变量声明中指定类型。在将无类型常量转换为接口值时,默认值非常重要,因为它们会决定接口的动态类型。

1
2
3
4
i := 0      // untyped integer; implicit int(0)
r := '\000' // untyped rune; implicit rune('\000')
f := 0.0 // untyped floating-point; implicit float64(0.0)
c := 0i // untyped complex; implicit complex128(0i)
1
2
3
4
fmt.Printf("%T\n", 0)      // "int"
fmt.Printf("%T\n", 0.0) // "float64"
fmt.Printf("%T\n", 0i) // "complex128"
fmt.Printf("%T\n", '\000') // "int32" (rune)

Composite Types

Arrays

数组元素默认初始化为元素类型的零值,可以使用数组字面量(array literal)来初始化数组。在数组字面量中,如果使用省略号 ... 代替数组长度,则数组长度就是列表中元素的数量。数组大小属于类型的一部分,[3]int[4]int 是不同的类型。大小必须是一个常量表达式,在编译时能够确定值。

1
2
3
var a [3]int // array of 3 integers
fmt.Println(a[0]) // print the first element
fmt.Println(a[len(a)-1]) // print the last element, a[2]
1
2
3
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"
1
2
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"

除指定值列表外,还可以指定索引和值构成的列表,索引可以按任意顺序排列,不需要列出所有索引,未指定值的索引默认取零值。下面的数组 r 包含 100 个元素,其中最后一个元素的值为 -1。如果数组元素是可比较的,那么相同数组类型之间也可以相互比较,只有当所有对应元素都相等时,两个数组才相等。

1
r := [...]int{99: -1}

将数组作为参数传递时,传递的是数组的副本,如果函数想要修改原数组,需要传递指向数组的指针。

1
2
3
func zero(ptr *[32]byte) {
*ptr = [32]byte{}
}

Slices

切片是可变长的序列,由指针、长度和容量组成。指针指向该切片能够访问的底层数组的第一个元素,该元素不一定是底层数组的第一个元素。长度是切片包含的元素数量,容量是切片起始位置到底层数组末尾之间的元素数量,长度不会超过容量。多个切片可以共享相同的底层数组。

1
2
3
4
5
months := [...]string{1: "January", /* ... */, 12: "December"}
Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2) // ["April" "May" "June"]
fmt.Println(summer) // ["June" "July" "August"]

超出切片容量 cap(s) 会导致 panic,超出长度 len(s) 会扩展切片长度。由于切片包含指向底层数组的指针,所以将其作为参数传递不会复制数组元素,参数和切片共享底层数组。

1
2
3
fmt.Println(summer[:20]) // panic: out of range
endlessSummer := summer[:5] // extend a slice (within capacity)
fmt.Println(endlessSummer) // "[June July August September October]"

切片字面量和数组字面量类似,只是未给出大小,它会先创建具有正确大小的数组,然后将该数组作为切片的底层数组。

1
s := []int{0, 1, 2, 3, 4, 5}

和数组不同,切片无法进行比较,不能使用 == 判断两个切片是否相等。标准库提供对 []byte 切片的比较函数 bytes.Equal,对于其他类型的切片,需要自己实现比较逻辑。例外情况是,切片可以和 nil 进行比较。

1
if summer == nil { /* ... */ }

切片的零值是 nilnil 切片的长度和容量都是零。不过长度和容量为零的切片不一定是 nil 切片,例如 []int{}make([]int, 3)[3:]。除和 nil 进行相等性比较之外,nil 切片和任何其他长度为零的切片具有相同的行为。

1
2
3
4
var s []int    // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil

内置函数 make 用于创建切片,如果省略容量参数,则容量默认和长度相等。内部实现上,make 会创建未命名的数组变量,然后返回该数组的一个切片,该数组只能通过切片访问。

1
2
make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

The append Function

内置函数 append 将元素添加到切片。如果容量足够则扩展切片长度,这会返回一个更大的新切片,和原切片共享底层数组。如果容量不足,则需要创建新数组,将旧数组的元素复制到新数组,此时和原切片的底层数组不同。下面是一个简易实现示例,内置函数 append 可能更加复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func appendInt(x []int, y int) []int {
var z []int
zlen := len(x) + 1
if zlen <= cap(x) {
// There is room to grow. Extend the slice.
z = x[:zlen]
} else {
// There is insufficient space. Allocate a new array.
// Grow by doubling, for amortized linear complexity.
zcap := zlen
if zcap < 2*len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x) // a built-in function; see text
}
z[len(x)] = y
return z
}

内置的 append 函数允许添加多个元素或切片,省略号 ...int 表示函数具有可变参数,在切片之后添加省略号表示将其展开为参数列表。

1
2
3
4
5
6
var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) // append the slice x
fmt.Println(x) // "[1 2 3 4 5 6 1 2 3 4 5 6]"

Maps

map 是对哈希表的引用,表示为 map[K]V,其中 KV 分别是键值的类型。键类型必须能够使用 == 比较,不建议使用浮点数作为键,因为浮点数可以是 NaN。可以使用内置函数 make 或哈希表字面量(map literal)创建 map,使用内置函数 delete 删除键。如果键不在哈希表中,则会返回值类型对应的零值。

1
2
3
4
5
6
7
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
ages["alice"] = 32
fmt.Println(ages["alice"]) // "32"
delete(ages, "alice") // remove element ages["alice"]

map 元素不是变量,无法获取其地址,原因之一是扩容会导致地址失效。

1
_ = &ages["bob"] // compile error: cannot take address of map element

可以使用基于范围的 for 循环遍历哈希表。map 的迭代顺序不是确定性的,实际实现为随机顺序,如果要按照顺序遍历,需要对键进行排序。由于已知 names 的最终大小,所以预分配指定容量的数组会更高效。

1
2
3
4
5
6
7
8
names := make([]string, 0, len(ages))
for name := range ages {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}

map 类型的零值是 nil,表示没有引用哈希表。对 nil 映射执行 []deletelenrange 都是安全的,其行为和空映射 map[string]int{} 类似。但是不允许向其中存储数据,会导致程序崩溃。

1
2
3
4
var ages map[string]int
fmt.Println(ages == nil) // "true"
fmt.Println(len(ages) == 0) // "true"
ages["carol"] = 21 // panic: assignment to entry in nil map

可以使用以下形式判断 map 中是否存在指定的键,以和默认零值区分开来。和切片类似,map 之间不能相互比较,只允许其和 nil 进行比较。如果想要将切片作为键,由于切片是不可比较的,所以可以将切片转换为字符串。

1
if age, ok := ages["bob"]; !ok { /* ... */ }

Structs

使用 type xxx struct 形式声明结构体,指向结构体的指针可以直接通过 . 访问其字段(不像 C++ 需要使用 ->),该写法等价于显式解引用之后再使用 .。字段通常一行写一个,不过相同类型的连续字段可以合并书写。字段顺序对于类型标识至关重要,不同字段顺序定义得到的是不同的类型。如果字段名称以大写开头,则该字段会被导出。

1
2
3
4
5
6
7
8
type Employee struct {
ID int
Name, Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
1
2
3
var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"
(*employeeOfTheMonth).Position += " (proactive team player)"

具有名称 S 的结构体不能声明类型为 S 的字段,即不能包含自身类型的字段,但是可以包含指向自身类型的指针。结构体的零值由其每个字段的零值组合而成。没有字段的结构体 struct{} 被称为空结构体,其大小为零,可以用作 map[string]struct{}{} 表示集合类型。

Struct Literals

可以使用结构体字面量(struct literal)初始化结构体,如果不指定类型,需要按照字段声明顺序,给所有字段指定初始值。如果指定类型,则可以随意指定某些字段的初始值,剩余字段默认初始化为零值。结构体作为参数传递使用的是值传递,可以仅传递指针来提高效率。两种形式不能在相同字面量中混合使用。

Comparing Structs

如果结构体的所有字段都是可比较的,那么该结构体就是可比较的,因此可以使用 ==!= 进行比较。和其他可比较类型类似,可比较的结构体也可以作为 map 的键。

Struct Embedding and Anonymous Fields

可以使用结构体嵌入(struct embedding)将一个结构体作为另一个结构体的匿名字段,从而允许将形如 x.d.e.f 的字段链简化为 x.f。Go 中使用组合而不是继承复用代码,结构体嵌入可以简化组合产生的过长字段链。由于匿名字段实际上有隐含的名称,所有不能有两个相同类型的匿名字段,因为它们会相互冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}

var w Wheel
w.X = 8 // equivalent to w.Circle.Point.X = 8
w.Y = 8 // equivalent to w.Circle.Point.Y = 8
w.Radius = 5 // equivalent to w.Circle.Radius = 5
w.Spokes = 20

JSON

将 Go 数据结构序列化为 JSON 字符串,可以使用 json.Marshal 函数。序列化会将 Go 结构体字段名称用作 JSON 对象的字段名称,只有导出的字段才会被序列化。通过定义适当的数据结构,在反序列化时可以获取指定的数据,其余数据将被忽略。可以使用字段标签(field tags)替换 JSON 中默认的字段名称,使用 key:"value" 形式声明字段标签。字段标签通常用于将 Go 字段的驼峰命名转换为 JSON 字段的下划线命名,可以使用 omitempty 表示字段为零值或者为空时不生成对应的 JSON 输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
type Movie struct {
Title string
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Actors []string
}

var movies = []Movie{
{Title: "Casablanca", Year: 1942, Color: false,
Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
{Title: "Cool Hand Luke", Year: 1967, Color: true,
Actors: []string{"Paul Newman"}},
{Title: "Bullitt", Year: 1968, Color: true,
Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
// ...
}

func main() {
{
//!+Marshal
data, err := json.Marshal(movies)
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)
//!-Marshal
}

{
//!+MarshalIndent
data, err := json.MarshalIndent(movies, "", " ")
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)
//!-MarshalIndent

//!+Unmarshal
var titles []struct{ Title string }
if err := json.Unmarshal(data, &titles); err != nil {
log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
//!-Unmarshal
}
}

Functions

Function Declarations

函数声明包含名称、参数列表、返回值列表以及函数体。如果函数值返回单个未命名返回值或者没有返回值,则可以省略返回值列表的括号。可以给返回值命名,此时默认会为每个名称声明对应的局部变量,初始化为其类型的零值。有返回值列表的函数必须以 return 语句结束,除非程序不会到达函数末尾。

1
2
3
func name(parameter-list) (result-list) {
body
}

相同类型的参数可以分为一组,仅编写一次类型。空标识 _ 表示参数未被使用。函数的类型有时被称为其签名(signature),如果两个函数具有相同的参数类型和返回值类型,那么它们就具有相同的签名。参数和返回值的名称不会影响签名,签名也不受分组形式声明的影响。

1
2
3
4
5
6
7
8
9
func add(x int, y int) int { return x + y }
func sub(x, y int) (z int) { z = x - y; return }
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }

fmt.Printf("%T\n", add) // "func(int, int) int"
fmt.Printf("%T\n", sub) // "func(int, int) int"
fmt.Printf("%T\n", first) // "func(int, int) int"
fmt.Printf("%T\n", zero) // "func(int, int) int"

参数总是值传递的,函数会接受参数的副本,对副本进行修改不会影响调用者。当参数是引用类型时,函数可以修改该参数间接引用的变量。如果发现某个函数没有函数体,则说明该函数是使用其他语言编写的。

1
func Sin(x float64) float64 // implemented in assembly language

Recursion

许多编程语言使用固定大小的函数栈,通常为 64KB 到 2MB之间。固定大小的函数栈会限制递归调用的深度,需要小心避免栈溢出。Go 使用可变大小的栈,初始较小然后根据需要增大,直至达到大约 1GB 的上限。

Multiple Return Values

Go 语言的垃圾回收机制会回收未使用的内存,但它不会自动释放未使用的操作系统资源,比如打开的文件和网络连接,这些资源应当显式的关闭。具有命名返回值的函数可以省略 return 语句的操作数,这被称为裸返回(bare return)。

1
2
3
func Size(rect image.Rectangle) (width, height int)
func Split(path string) (dir, file string)
func HourMinSec(t time.Time) (hour, minute, second int)

Errors

对于失败(failure)是预期行为的函数,通常会有额外的返回值。如果失败的原因只有一个,那么额外返回布尔类型,否则应该返回 error 类型。内置类型 error 是接口类型,为 nil 时表示成功,反之则表示失败。和其他使用异常机制的语言不同,Go 语言使用 panic 表示非预期的错误,预期错误则使用 ifreturn 控制流机制实现错误处理。

Error-Handling Strategies

当函数调用出现错误时,调用方有责任检查并采取相应措施。最常见的处理方式是传播错误,也可以为错误添加额外信息。因为错误通常是链式的,所以消息字符串不应该大小,也不应该使用换行符。通常函数 f(x) 负责报告所执行的操作 f 以及与错误相关的参数 x,而调用者负责补充 f(x) 没有的其它信息。

1
2
3
4
5
6
7
8
9
10
resp, err := http.Get(url)
if err != nil {
return nil, err
}

doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
}

另一种错误处理方式是重试,需要设置重试次数和重试前的等待时间。如果无法取得进展,调用者可以打印错误信息并终止程序,但这种处理方式仅用于程序的主要模块中。库函数通常应该将错误信息传递给调用者,除非该错误是 BUG。在某些情况下,可以仅记录错误信息并继续运行。极少数情况下,可以完全忽略某个错误。

End of File (EOF)

文件结束条件会产生 io.EOF 错误,需要特殊处理该错误。

1
2
3
4
5
6
7
8
9
10
11
in := bufio.NewReader(os.Stdin)
for {
r, _, err := in.ReadRune()
if err == io.EOF {
break // finished reading
}
if err != nil {
return fmt.Errorf("read failed: %v", err)
}
// ...use r...
}

Function Values

在 Go 语言中,函数是一等公民(first-class values),像其他值一样,函数值也有类型,也可以作为参数传递给函数或从函数返回。函数类型的零值是 nil,调用 nil 函数会导致 panic,函数值之间是不可比较的,只能和 nil 比较。

1
2
3
4
5
6
7
8
9
10
11
12
func square(n int) int     { return n * n }
func negative(n int) int { return -n }
func product(m, n int) int { return m * n }

f := square
fmt.Println(f(3)) // "9"

f = negative
fmt.Println(f(3)) // "-3"
fmt.Printf("%T\n", f) // "func(int) int"

f = product // compile error: can't assign f(int, int) int to f(int) int

Anonymous Functions

命名函数只能在包级进行声明,而匿名函数可以在任意表达式中使用。每次调用 squares 都会创建一个局部变量 x,并返回一个类型为 func() int 的匿名函数。此时,匿名函数可以访问和修改外层函数的局部变量 x。这些隐含的变量就是将函数视为引用类型,以及函数值之间无法比较的原因。这类函数值使用闭包(closures)技术实现。就像之前提到的返回局部变量的地址的例子,闭包示例也说明变量的生命周期不是由作用域决定,而是由可达性决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// squares returns a function that returns
// the next square number each time it is called.
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}

func main() {
f := squares()
fmt.Println(f()) // "1"
fmt.Println(f()) // "4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
}

当匿名函数需要递归时,必须先声明一个变量,然后将匿名函数赋值给该变量。

1
2
3
4
5
visitAll := func(items []string) {
// ...
visitAll(m[item]) // compile error: undefined: visitAll
// ...
}

Caveat: Capturing Iteration Variables

在迭代中使用匿名函数时,需要避免捕获迭代变量的陷阱,正确的做法是声明一个同名的局部变量覆盖迭代变量。

1
2
3
4
5
6
7
8
var rmdirs []func()
dirs := tempDirs()
for i := 0; i < len(dirs); i++ {
os.MkdirAll(dirs[i], 0755) // OK
rmdirs = append(rmdirs, func() {
os.RemoveAll(dirs[i]) // NOTE: incorrect!
})
}

Variadic Functions

可变参函数的最后一个参数类型前面使用省略号 ...,表示可以接收任意数量的该类型参数。实际上,调用者会创建一个数组,将参数赋值到其中,然后将数组的切片传递给函数。

1
2
3
4
5
6
7
func sum(vals ...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}

尽管 ...int 参数的行为类似切片,但是可变参函数的类型和带有切片参数函数的类型并不相同。

1
2
3
4
5
func f(...int) {}
func g([]int) {}

fmt.Printf("%T\n", f) // "func(...int)"
fmt.Printf("%T\n", g) // "func([]int)"

Deferred Function Calls

defer 关键字之后跟函数调用,表示延迟执行该调用。函数和参数表达式在执行该语句时就会被计算,而实际调用则会推迟到包含该 defer 语句的函数执行完毕时,无论函数是正常/异常结束。可以有任意数量的调用被延迟执行,它们会以执行 defer 语句相反的顺序依次执行(类似栈 LIFO)。

1
2
3
4
5
6
7
8
var mu sync.Mutex
var m = make(map[string]int)

func lookup(key string) int {
mu.Lock()
defer mu.Unlock()
return m[key]
}

可以在单个 defer 语句中,对函数的入口和出口进行检测。下面的 trace 函数会立即被调用,而其返回的函数在 bigSlowOperation 函数结束时被调用。

1
2
3
4
5
6
7
8
9
10
11
func bigSlowOperation() {
defer trace("bigSlowOperation")() // don't forget the extra parentheses
// ...lots of work...
time.Sleep(10 * time.Second) // simulate slow operation by sleeping
}

func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() { log.Printf("exit %s (%s)", msg, time.Since(start)) }
}

延迟执行的函数在 return 语句更新完返回值变量之后执行,可以在其中修改返回值。

1
2
3
4
5
6
func triple(x int) (result int) {
defer func() { result += x }()
return double(x)
}

fmt.Println(triple(4)) // "12"

Panic

当 Go 运行时检测到严重的错误时,会产生 panic,正常执行会停止,从函数栈顶到 main 函数的所有延迟函数调用会依次执行,然后程序会崩溃并记录日志。可以显示调用内置的 painc 函数。对于函数的前置条件进行确认是良好的做法,但是除非能够提供更详细的错误信息或更早地检测到错误,否则没有必要去确认在运行时会自动检查的条件。

1
2
3
4
5
6
func Reset(x *Buffer) {
if x == nil {
panic("x is nil") // unnecessary!
}
x.elements = nil
}

Recover

如果在延迟函数中调用内置的 recover 函数,且包含该 defer 语句的函数发生 panic,那么 recover 会终止当前的 panic 状态并返回 panic 的值。发生 panic 的函数不会继续执行,而是直接返回。在没有发生 panic 时调用 recover 函数不会有任何效果,仅仅返回 nil

1
2
3
4
5
6
7
8
func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...parser...
}

Methods

Method Declarations

方法声明就是在函数名之前添加额外的参数,该参数将函数和该参数的类型绑定。额外的参数 p 被称作该方法的接收者(receiver),在 Go 语言中,没有使用 thisself 表示接收者,而是像对待普通参数一样选择接收者的名称,通常使用类型的首字母作为其名称。以下两个函数声明不会相互冲突,一个是包级函数,另一个是 Point 类型的方法。

在 Go 语言中,字段和方法不能同名(和 Java 不同)。和其他面向对象语言不同,Go 中可以为大多数类型定义方法,而不仅仅是结构体类型。例外情况是:① 不能直接为基本类型定义方法,而必须使用 type 创建命名类型;② 不能为指针和接口类型定义方法,但是接收者可以是指向非指针类型的指针。另外,类型和方法必须定义在相同的包中。

1
2
3
4
5
6
7
8
9
10
11
type Point struct{ X, Y float64 }

// traditional function
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}

Methods with a Pointer Receiver

如果方法使用 (p Point) 声明,则和普通参数一样会复制该变量,如果想要在方法中修改相关变量,则应该使用 (p *Point) 指针类型接收变量的地址。在调用时也需要通过指针调用该方法,不过语法上允许直接通过 Point 变量而不是 *Point 调用 ScaleBy 方法,编译器会对其隐式地执行 & 取址操作。对于临时值由于还没有为其分配地址,所以不能使用简写形式。同样可以通过 *Point 变量调用 Distance 方法,编译器会隐式地执行 * 解引用操作。

1
2
3
4
5
6
7
8
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}

r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // "{2, 4}"
1
Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal

Nil Is a Valid Receiver Value

正如函数允许将 nil 指针作为参数,接收者也可以为 nil,当 nil 是该类型有意义的零值时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// An IntList is a linked list of integers.
// A nil *IntList represents the empty list.
type IntList struct {
Value int
Tail *IntList
}

// Sum returns the sum of the list elements.
func (list *IntList) Sum() int {
if list == nil {
return 0
}
return list.Value + list.Tail.Sum()
}

Composing Types by Struct Embedding

结构体嵌入会将内部组合结构体的方法提升到外部结构体中。这种组合方式和继承不同,两种类型并不是父子关系,不具有多态性。实际上,编译器会生成包装方法,其内部会调用字段的对应方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
type ColoredPoint struct {
Point
Color color.RGBA
}

red := color.RGBA{255, 0, 0, 255}
blue := color.RGBA{0, 0, 255, 255}
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}
fmt.Println(p.Distance(q.Point)) // "5"
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"
1
2
3
4
5
6
func (p ColoredPoint) Distance(q Point) float64 {
return p.Point.Distance(q)
}
func (p *ColoredPoint) ScaleBy(factor float64) {
p.Point.ScaleBy(factor)
}

Method Values and Expressions

1
2
3
4
5
6
7
8
9
10
11
12
p := Point{1, 2}
q := Point{4, 6}

distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // "5"
var origin Point // {0, 0}
fmt.Println(distanceFromP(origin)) // "2.23606797749979", ;5

scaleP := p.ScaleBy // method value
scaleP(2) // p becomes (2, 4)
scaleP(3) // then (6, 12)
scaleP(10) // then (60, 120)
1
2
3
4
5
6
7
8
9
10
11
p := Point{1, 2}
q := Point{4, 6}

distance := Point.Distance // method expression
fmt.Println(distance(p, q)) // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"

scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p) // "{2 4}"
fmt.Printf("%T\n", scale) // "func(*Point, float64)"

Interfaces

Interface Satisfaction

接口类型是抽象类型,其定义一组方法,只要具体类型有这些方法,就可以被视为该接口的实例(隐式实现接口)。接口类型之间也可以嵌入,使得当前接口具有另一个接口的方法,而不需要显示的全部定义出来。只要具体类型或接口类型有某个接口类型的所有方法,就可以将其赋值给该接口类型。可以将任何值赋值给空接口类型 interface{}。可以使用以下方式,在编译时断言 bytes.Buffer 类型满足 io.Writer 接口。

1
2
3
4
5
6
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)
1
2
3
// *bytes.Buffer must satisfy io.Writer
var w io.Writer = new(bytes.Buffer)
var _ io.Writer = (*bytes.Buffer)(nil)

Interface Values

接口类型的值(interface value)包含两个组成部分,即具体类型以及该类型的值,它们被称为动态类型(dynamic type)和动态值(dynamic value)。动态类型由类型描述符(type descriptor)表示,其提供类型的字段和方法信息。接口的零值,其类型和值的组成部分都被设置为 nil。如果接口的类型部分是 nil,则该接口值就是 nil

1
2
3
4
5
6
7
8
var w io.Writer
fmt.Printf("%T\n", w) // "<nil>"

w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"

w = new(bytes.Buffer)
fmt.Printf("%T\n", w) // "*bytes.Buffer"

可以使用 ==!= 比较接口值,如果两个接口值均为 nil,或者它们的动态类型和动态值都相同,则这两个接口值相同。如果两个接口值的动态类型相同,但是它们的动态类型不支持比较操作,则会引发 panic

1
2
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int

Caveat: An Interface Containing a Nil Pointer Is Non-Nil

debug = false 时,下面的代码会引发 panic,因为接口值的动态类型不是 nil,而动态值是 nil,在调用 (*bytes.Buffer).Write 方法时会出问题。解决方案是,将 buf 声明为 var buf io.Writer,避免将无效值(bytes.Buffer 类型的 nil 指针)赋值给接口类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const debug = true

func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // NOTE: subtly incorrect!
if debug {
// ...use buf...
}
}

// If out is non-nil, output will be written to it.
func f(out io.Writer) {
// ...do something...
if out != nil {
out.Write([]byte("done!\n")) // panic: nil pointer dereference
}
}

Type Assertions

类型断言(type assertion)是对接口值执行的操作,形如 x.(T),其中 x 是接口类型的表达式,T 是断言类型。如果 T 是具体类型,那么类型断言会检查 x 的动态类型是否和 T 相同,如果相同则返回 x 的动态值,否则引发 panic

1
2
3
4
var w io.Writer
w = os.Stdout
f := w.(*os.File) // success: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer

如果 T 是接口类型,那么类型断言会检查 x 的动态类型是否满足 T,如果满足则返回类型为 T 的接口值,该接口值和 x 具有相同的动态类型和动态值。不论 T 是什么类型,如果 xnil,则断言会失败。

1
2
3
4
5
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // success: *os.File has both Read and Write
w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method

如果使用以下类型断言方式,则断言失败不会引发 panic,而是额外返回 false

1
2
3
var w io.Writer = os.Stdout
f, ok := w.(*os.File) // success: ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil

Type Switches

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func sqlQuote(x interface{}) string {
switch x := x.(type) {
case nil:
return "NULL"
case int, uint:
return fmt.Sprintf("%d", x) // x has type interface{} here.
case bool:
if x {
return "TRUE"
}
return "FALSE"
case string:
return sqlQuoteString(x) // (not shown)
default:
panic(fmt.Sprintf("unexpected type %T: %v", x, x))
}
}

Rust 基础

参考 Rust DocumentationTour of Rustcrates.io

Getting Started

Hello, World!

1
2
3
fn main() {
println!("Hello, world!");
}
1
2
3
$ rustc main.rs
$ ./main
Hello, world!

Common Programming Concepts

Variables and Mutability

使用 let 声明变量,变量默认是不可变的,使用 mut 关键字使其可变。使用 const 声明常量,不允许对常量使用 mut,并且必须显示指定其类型。

1
2
let mut x = 5;
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

和其他语言不同,Rust 允许在相同作用域中遮蔽(shadowing)变量,而且允许使用不同的类型。

1
2
let spaces = "   ";
let spaces = spaces.len();

Data Types

Rust 数据类型分为两种:标量类型(scalar)和复合类型(compound)。有四种基本的标量类型:整型、浮点类型、布尔类型和字符类型。编译器通常可以推断出类型,如果不能则需要显式写出类型。

1
let guess: u32 = "42".parse().expect("Not a number!");

Scalar Types

有符号整型有 i8i16i32i64i128isize,其中 isize 类型和机器相关。无符号整型类似,只是将 i 开头替换为 u 开头。整型字面量:十进制 98_222、十六进制 0xff、八进制 0o77、二进制 0b1111_0000 和单字节字符 b'A'(仅限 u8 类型)。可以使用标准库函数显示处理整型溢出:wrapping_*checked_*overflowing_*saturating_*

浮点类型有 f32f64,默认使用 f64。布尔类型 bool 的值有 truefalse 两种。字符类型 char 大小为 4 字节,使用 UTF-32 编码方式。

Compound Types

Rust 有两个原生的复合类型,元组(tuple)和数组(array)。元组可以将一个或多个不同类型的值组合起来,声明之后长度固定不变。可以使用 .index、模式匹配(pattern matching)和解构(destructure)获取元组元素,其中 index 是元素的索引。不包含任何值的元组 () 被称为单元(unit),表示空值或空返回类型,如果表达式不返回其他值,则会隐式地返回单元值。

1
2
3
4
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
1
2
3
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");

数组中每个元素的类型都必须相同,数组长度也是固定的,在栈上分配空间。在声明类型时,需要在方括号中包含元素类型和数量。在初始化时,可以使用方括号包含初始值和元素数量,创建每个元素都相同的数组。

1
2
let a: [i32; 5] = [1, 2, 3, 4, 5];
let a = [3; 5];

Functions & Control Flow

Rust 代码中的变量和函数名使用 snake case 风格,使用下划线分隔单词。Rust 是基于表达式的语言(expression-based),函数体由一系列语句(statement)和可选地结尾表达式(expression)组成,语句不会返回值而表达式会。表达式结尾没有分号,如果加上分号它就变为语句。如果函数没有返回值,则可以省略返回类型,会隐式地返回空元组,否则需要使用 -> 显式声明。代码块 {}if 都是表达式,可以在 let 语句右侧使用。

1
2
3
fn five() -> i32 {
5
}
1
2
3
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");

Rust 有三种循环 loopwhileforloop 表示无限循环,可以使用 break 终止并指定返回值。

1
2
3
4
5
6
7
8
9
10
let mut counter = 0;

let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};

println!("The result is {result}");
1
2
3
4
5
for i in 0..n { ... }
for i in (0..n).rev() { ... }
for item in &arr { ... }
for item in &mut arr { ... }
for (i, item) in arr.iter().enumerate() { ... }

Understanding Ownership

What Is Ownership?

所有权(ownership)是 Rust 用于管理内存的一组规则。栈中的所有数据都必须占用已知且固定的大小,在编译时大小未知或大小可能变化的数据,需要存储在堆上,所有权主要目的就是管理堆数据。所有权规则:① Rust 中的每个值都有所有者(owner);② 每个值在任意时刻有且仅有一个所有者;③ 当所有者离开作用域之后,该值将被丢弃。

1
2
3
4
{
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid

所有权(Ownership)机制让 Rust 无需 GC 就能保证内存安全。GC 是运行时根据可达性分析回收内存,而所有权是在编译时确定变量的作用域,当内存的所有者变量离开作用域之后,相应内存就可以被释放。当变量离开作用域时,Rust 会自动调用 drop 函数来释放内存,类似 C++ 的 RAII 机制。

字符串 String 的数据存储在堆上,字符串变量包含指向堆中数据的指针、数据的长度和容量,其存储在栈上。使用 let s2 = s1; 只会复制引用(浅拷贝),为避免 s1s2 离开作用域之后,释放相同内存两次(调用 drop 函数),该语句执行之后 Rust 会使 s1 无效,不需要在其离开作用域之后回收内存,该操作被称为移动(move)。

1
2
3
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!"); // error[E0382]: borrow of moved value: `s1`

当给已有值的变量赋新值时,Rust 会立即调用 drop 释放原始值的内存。如果想要执行深拷贝,可以使用 clone 函数。如果类型实现 Copy 特征,则旧变量在被赋值给其他变量之后仍然有效。不能为实现 Drop 特征的类型实现 Copy 特征。

1
2
3
let mut s = String::from("hello");
s = String::from("ahoy");
println!("{s}, world!");

References and Borrowing

引用(reference)类似指针,它是一个地址,但是和指针不同,Rust 保证引用指向某个特定类型的有效值。传递引用值不会转移所有权,所以可以在 main 中继续使用 s1。函数 calculate_length 中的局部变量 s 是引用类型,其不拥有值的所有权,所以离开作用域之后,其指向的值也不会被回收,所以创建引用的行为被称为借用(borrowing)。

1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
s.len()
}

默认不能通过引用 & 修改指向的值,除非声明的是可变引用 &mut,只能对可变变量(使用 let mut 声明)创建可变引用。如果变量有不可变引用,则原变量可读但不可写。如果变量有可变引用,则原变量不可读写。如果变量有一个可变引用,则该变量不能有其他引用,不论是否可变。以上机制可以保证在编译时就避免数据竞争(data race),因为最多有一个线程可以修改变量,而且此时其他线程都无法读取该变量。

1
2
3
4
5
6
7
8
fn main() {
let mut s = String::from("hello");
change(&mut s);
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}

引用的作用域从声明的地方开始直到最后一次使用为止,下面的不可变引用 r1r2 的最后一次使用,发生在 r3 的声明之前,所以不会发生编译错误。

1
2
3
4
5
6
7
8
9
let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// Variables r1 and r2 will not be used after this point.

let r3 = &mut s; // no problem
println!("{r3}");

Rust 编译器保证数据不会在其引用之前离开作用域,即不允许悬垂引用,否则会发生编译错误。在其他语言中,C++ 存在悬垂引用,Java 中没有原始指针类型不存在这个问题,Go 中的变量生命周期由可达性决定而不是作用域,通过逃逸分析局部变量会在堆上分配,所以也可以避免悬垂引用。

1
2
3
4
5
6
7
8
fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String { // error[E0106]: missing lifetime specifier
let s = String::from("hello");
&s // error[E0515]: cannot return reference to local variable `s`
}

The Slice Type

切片(slice)是引用类型,不拥有值的所有权。切片仅由指向数据的指针和长度组成,不像 Go 中还包含容量字段。字符串切片的索引必须位于有效的 UTF-8 字符边界上,如果从多字节字符的中间位置创建切片,则会产生运行时错误。

1
2
3
4
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];

字符串切片的类型有 &str&mut str,区别在于指向的数据是否可变。使用 let mut 声明变量,只是表示可以修改变量绑定的切片,和切片数据是否可变无关。字符串字面量的类型是 &str,所以是不可变的,如果想要获取可变字符串切片,需要使用 String 结合 mut

1
let mut s = "Hello, world!";
1
2
let mut s = String::from("hello");
let slice = &mut s[..];

Defining and Instantiating Structs

在初始化结构体时,如果字段名称和初始化参数相同,则可以省略。如果要使用旧实例的大部分值创建新实例,可以使用结构体更新语法(struct update syntax),.. 表示剩余未显式设置的字段和给定旧实例具有相同的值。该语法类似使用 = 进行赋值,所以未实现 Copy 特征的类型会被转移所有权,旧实例的相应字段会失效。

1
2
3
4
5
6
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
1
2
3
4
5
6
7
8
fn main() {
// --snip--

let user2 = User {
email: String::from("another@example.com"),
..user1
};
}

可以定义元组结构体(tuple struct),元组结构体的字段没有名称只有类型。即使两个元组结构体有相同类型的字段,它们也是不同的类型。可以解构元组结构体,但是需要指定结构体的类型。

1
2
3
4
5
6
7
8
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
let Point(x, y, z) = origin;
}

没有任何字段的结构体被称为类单元结构体(unit-like struct)。如果在结构体中存储引用类型 &str,而不是自身拥有所有权的 String 类型,就需要使用生命周期,否则会发生编译错误。

1
2
3
4
5
struct AlwaysEqual;

fn main() {
let subject = AlwaysEqual;
}
1
2
3
4
5
6
struct User {
active: bool,
username: &str, // error[E0106]: missing lifetime specifier
email: &str, // error[E0106]: missing lifetime specifier
sign_in_count: u64,
}

Method Syntax

方法的第一个参数必须是 self,表示调用该方法的结构体实例,&self 实际上是 self: &Self 的缩写。在 impl 块中,Self 类型是 impl 块类型的别名,在下面就是 Rectangle。方法可以选择不可变借用 &self、可变借用 &mut self 或获取所有权 self。方法的名称可以和结构体的字段相同(Java 中也可以,Go 中不行)。当调用方法时,Rust 会自动根据方法签名添加 &&mut*,所以不需要显式转换。

1
2
3
4
5
6
7
8
9
10
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}

impl 中定义的函数被称为关联函数(associated function),其第一个参数不是 self(类似 Java 中的静态方法)。调用关联函数需要使用 Rectangle::square(x) 形式,表示函数 square 位于结构体 Rectangle 的命名空间中。每个结构体可以有多个 impl 块,不过必须和结构体在相同的 crate 中。

1
2
3
4
5
6
7
8
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}

Enums and Pattern Matching

Defining an Enum

枚举类型的字段被称为变体(variant),可以将数据附加到变体上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum IpAddrKind {
V4,
V6,
}

struct IpAddr {
kind: IpAddrKind,
address: String,
}

let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
1
2
3
4
5
6
7
8
enum IpAddr {
V4(String),
V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

每个变体附加数据的类型和数量可以不同,可以使用 impl 为枚举类型定义方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

impl Message {
fn call(&self) {
// method body would be defined here
}
}

let m = Message::Write(String::from("hello"));
m.call();

The match Control Flow Construct

可以使用变量名称之后跟 => 表示通配模式,放在最后以匹配其他情况,如果不使用变量则可以使用 _ => 形式。

1
2
3
4
enum Option<T> {
None,
Some(T),
}
1
2
3
4
5
6
7
8
9
10
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

Concise Control Flow with if let and let else

1
2
3
4
5
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
1
2
3
4
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}
1
2
3
4
5
6
7
8
9
10
11
fn describe_state_quarter(coin: Coin) -> Option<String> {
let Coin::Quarter(state) = coin else {
return None;
};

if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}

Common Collections

Storing Lists of Values with Vectors

1
2
3
4
5
6
7
8
9
10
let v = vec![1, 2, 3, 4, 5];

let third: &i32 = &v[2];
println!("The third element is {third}");

let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
1
2
3
4
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}

Storing UTF-8 Encoded Text with Strings

Rust 中有两种字符串,字符串切片 str 和字符串 String,它们使用的都是 UTF-8 编码,String 是对 Vec<u8> 的封装。使用 + 运算符拼接 String 字符串,会获取第一个参数的所有权,以及第二个参数的引用,类似下面的 add 函数。

实际上,&s2 会被转换为 &s2[..],从而满足 add 函数的 &str 参数类型,该技术被称为 dref coercion。s3 会获取 s1 的所有权,然后将 s2 复制到其末尾。也可以使用 format! 宏执行字符串拼接,该宏不会获取任何参数的所有权。

1
2
3
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
1
fn add(self, s: &str) -> String {
1
2
3
4
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");

由于字符串使用 UTF-8 编码,所以不支持使用索引访问,字符串的长度是其包含的字节数而不是字符数。在 Go 语言中,可以直接使用索引,访问的就是对应的字节。在 Rust 中,虽然不能使用索引访问字符串,但是可以使用范围获取字符串切片,前提是范围位于有效的 UTF-8 字符边界上。

1
2
let s1 = String::from("hi");
let h = s1[0]; // error[E0277]: the type `str` cannot be indexed by `{integer}`
1
2
let hello = "Здравствуйте";
let s = &hello[0..4];

使用 chars() 方法遍历字符,bytes 方法遍历字节。

1
2
3
4
5
6
for c in "Зд".chars() {
println!("{c}");
}
for b in "Зд".bytes() {
println!("{b}");
}

Storing Keys with Associated Values in Hash Maps

在使用 insert 时,对于实现 Copy 特征的类型,其值会复制到哈希表中,而拥有所有权的类型,其值会被移动到哈希表中。如果将引用插入哈希表,则引用指向的值,必须至少和哈希表的生命周期一样长(避免悬垂引用)。哈希表使用 SipHash 哈希函数,可以抵御涉及哈希表的拒绝服务攻击,允许用户指定其他哈希函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);

let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);

for (key, value) in &scores {
println!("{key}: {value}");
}

Error Handling

Unrecoverable Errors with panic!

panic 表示不可恢复的错误,在遇到 BUG 或显示调用 panic! 宏时产生。当出现 panic 时,程序默认会执行展开(unwinding),这意味着 Rust 会回溯函数栈并清理函数的数据。另一种选择是直接终止(abort),不清理数据就退出程序,此时程序所使用的内存由操作系统清理。

Recoverable Errors with Result

可以使用闭包和 unwrap_or_else 方法替代 match 来简化代码。使用 unwrap 方法,如果 Result 值是 Ok,则返回其中的值,否则会调用 panic!expect 方法类似,只是可以自定义 panic! 的错误信息。

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}
1
2
3
4
5
6
7
8
9
10
use std::fs::File;

fn main() {
let greeting_file_result = File::open("hello.txt");

let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}
1
2
3
4
5
6
7
8
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}

Generic Types, Traits, and Lifetimes

Rust 在编译时对泛型代码单态化(monomorphization)来保证效率,即为使用的具体类型生成对应类型的代码。在其他语言中,C++ 的做法类似,而 Java 执行类型擦除。

1
fn largest<T>(list: &[T]) -> &T {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Point<T> {
x: T,
y: T,
}

impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}

impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}

fn main() {
let p = Point { x: 5, y: 10 };

println!("p.x = {}", p.x());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Point<X1, Y1> {
x: X1,
y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}

fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };

let p3 = p1.mixup(p2);

println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

Traits: Defining Shared Behavior

孤儿规则(orphan rule):只有在特征或类型至少有一个属于当前 crate 时,才能对类型实现特征。也就是说,可以为内部类型实现外部特征、为外部类型实现内部特征,但是不能为外部类型实现外部特征。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pub trait Summary {
fn summarize_author(&self) -> String;

fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}

pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}

impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
1
2
3
pub fn notify(item: &(impl Summary + Display)) {

pub fn notify<T: Summary + Display>(item: &T) {
1
2
3
4
5
6
7
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{

Validating References with Lifetimes

Rust 中每个引用都有生命周期(lifetime),通常生命周期可以被隐式地推断出来,否则需要显式写出。生命周期的主要目标是避免悬垂引用,Rust 编译器使用借用检查器(borrow checker)比较作用域,确保所有的借用都是有效的。rx 的生命周期分别被标记为 'a'b,编译器会发现 r 引用的变量 x 的生命周期比自身更小,这会导致悬垂引用,所以编译器会报错。

1
2
3
4
5
6
7
8
9
10
fn main() { // error[E0597]: `x` does not live long enough
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
1
2
3
&i32        // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

生命周期注解语法用于将函数参数和返回值的生命周期相关联,生命周期也是泛型。以下函数签名表示,泛型生命周期参数 'a 的具体生命周期是 xy 中生命周期的较小者,返回值不能在生命周期 'a 结束之后使用。由于下面的 resulty 生命周期结束之后访问,所以会引发编译错误。

1
2
3
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
1
2
3
4
5
6
7
8
9
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str()); // error[E0597]: `string2` does not live long enough
}
println!("The longest string is {result}");
}

结构体可以存储引用类型,不过此时需要使用生命周期注解。下面注解意味着,ImportantExcerpt 实例不能在其字段 part 的生命周期结束之后使用。

1
2
3
struct ImportantExcerpt<'a> {
part: &'a str,
}

生命周期省略规则(lifetime elision rules):① 为每个引用参数分配不同的生命周期;② 如果只有一个输入生命周期参数,则将其生命周期赋给所有输出生命周期参数;③ 如果输入生命周期参数有 &self&mut self,则将 self 的生命周期赋给所有输出生命周期参数。如果应用规则之后,仍没有计算出引用的生命周期,则会引发编译错误。

1
2
3
4
5
6
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}

静态生命周期 'static 表示能够在程序运行期间存活,所有字符串字面量都具有静态生命周期。

1
let s: &'static str = "I have a static lifetime.";
1
2
3
4
5
6
7
8
9
10
11
12
13
use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {ann}");
if x.len() > y.len() { x } else { y }
}

Functional Language Features: Iterators and Closures

Closures: Anonymous Functions That Capture Their Environment

闭包(closures)是可以保存在变量中或作为参数传递给其他函数的匿名函数,和函数不同,它允许捕获其被定义时所在作用域中的值。通常不需要为闭包的参数和返回值声明类型注解,编译器可以推断出类型。必须调用 add_one_v3add_one_v4 闭包才能通过编译,编译器会根据调用上下文推断类型,对相同闭包使用不同的类型会引发编译错误。

1
2
3
4
fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;

闭包有三种捕获变量的方式:不可变借用、可变借用和获取所有权。前两种方式是隐式推断的,获取所有权使用 move

1
2
3
4
5
6
7
8
9
10
use std::thread;

fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");

thread::spawn(move || println!("From thread: {list:?}"))
.join()
.unwrap();
}

闭包可以执行:将捕获的值移出闭包、既不移动也不修改值、

闭包捕获和处理值的方式会影响闭包实现哪些特征:FnOnceFnMutFn 特征分别在获取所有权、可变借用和不可变借用时实现。它们之间是父子关系(和继承无关,只是指定依赖关系),FnOnce <- FnMut <- Fn。如果闭包不需要从环境中捕获值,可以直接使用函数而不是闭包,例如在 Option<Vec<T>> 值上调用 unwrap_or_else(Vec::new)

1
2
3
4
5
6
7
8
9
10
11
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
1
2
3
4
5
6
7
8
9
impl<T> [T] {
pub fn sort_by_key<K, F>(&mut self, mut f: F)
where
F: FnMut(&T) -> K,
K: Ord,
{
stable_sort(self, |a, b| f(a).lt(&f(b)));
}
}

Processing a Series of Items with Iterators

iter 方法生成不可变引用的迭代器,iter_mut 生成可变引用的迭代器,into_iter 生成获取所有权的迭代器。

1
2
3
4
5
6
7
pub trait Iterator {
type Item;

fn next(&mut self) -> Option<Self::Item>;

// methods with default implementations elided
}
1
2
3
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);