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]);

Python 基础

参考官方文档The Python Tutorial

An Informal Introduction to Python

除法运算 / 总是返回浮点数,使用 // 向下取整,Java 中的整数除法是向零取整。使用 ** 计算幂,** 的优先级比 - 更高。交互模式下,上次输出的表达式值被赋给变量 _,最好将其视为只读类型,显式为其赋值会创建同名的局部变量,屏蔽内置变量的行为。

1
2
>>> 15 / 3, 11 / 4, 11 // 4, -(3**2), (-3) ** 2
(5.0, 2.75, 2, -9, 9)

字符串使用单引号 '...' 或双引号 "..." 表示,两者没有区别。可以使用三重引号跨行编写字符串,换行符会被包含在字符串中,可以在行尾添加 \ 避免该行为。字符串可以使用 + 合并,也可以使用 * 重复,相邻字符串字面量会自动合并。字符串可以使用索引访问,索引越界会报错,但是切片会自动处理索引越界。字符串是不可变的,要生成不同的字符串应该创建字符串。使用内置函数 len() 获取字符串的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> # 3 times 'un', followed by 'ium'
>>> 3 * 'un' + 'ium'
'unununium'
>>> 'Py' 'thon'
'Python'
>>> text = ('Put several strings within parentheses '
... 'to have them joined together.')
>>> text
'Put several strings within parentheses to have them joined together.'
>>> word = 'Python'
>>> word[42]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: string index out of range
>>> word[4:42]
'on'
>>> word[0] = 'J'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
>>> 'J' + word[1:]
'Jython'
>>> len(word)
6

列表可以使用 + 合并,列表是可变的可以使用 list.append() 方法添加元素。使用 = 将列表赋值给变量是浅拷贝,该变量引用该列表。列表允许包含不同类型的元素,且可以相互嵌套。

1
2
3
4
5
6
7
8
9
10
11
12
>>> squares = [1, 4, 9, 16, 25]
>>> squares + [36, 49, 64, 81, 100]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> rgb = ["Red", "Green", "Blue"]
>>> rgba = rgb
>>> id(rgb) == id(rgba)
True
>>> a = ['a', 'b', 'c']
>>> n = [1, 2, 3]
>>> x = [a, n]
>>> x
[['a', 'b', 'c'], [1, 2, 3]]

Python 使用缩进组织语句,同一块语句要使用相同大小的缩进,可以使用空格或制表符。

1
2
3
4
5
6
>>> a, b = 0, 1
>>> while a < 1000:
... print(a, end=',')
... a, b = b, a+b
...
0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,

More Control Flow Tools

if 语句由 ifelifelse 组成。for 语句由 forin 组成,使用 range() 函数生成等差数列。组合使用 range()len() 实现按索引迭代序列,或者使用 enumerate() 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
# Create a sample collection
users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}

# Strategy: Iterate over a copy
for user, status in users.copy().items():
if status == 'inactive':
del users[user]

# Strategy: Create a new collection
active_users = {}
for user, status in users.items():
if status == 'active':
active_users[user] = status
1
2
3
4
5
6
>>> list(range(5, 10))
[5, 6, 7, 8, 9]
>>> list(range(0, 10, 3))
[0, 3, 6, 9]
>>> list(range(-10, -100, -30))
[-10, -40, -70]
1
2
3
4
5
6
7
8
9
>>> a = ['Mary', 'had', 'a', 'little', 'lamb']
>>> for i in range(len(a)):
... print(i, a[i])
...
0 Mary
1 had
2 a
3 little
4 lamb

forwhile 循环可以有对应的 else 子句,如果循环在未执行 break 的情况下结束,则会执行该 else 子句。pass 语句不执行任何操作,当语法上需要一个语句,而程序不需要执行任何操作时,可以使用该语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> for n in range(2, 10):
... for x in range(2, n):
... if n % x == 0:
... print(n, 'equals', x, '*', n//x)
... break
... else:
... # loop fell through without finding a factor
... print(n, 'is a prime number')
...
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3
1
2
3
>>> while True:
... pass # Busy-wait for keyboard interrupt (Ctrl+C)
...

match 语句用于模式匹配,使用 _ 通配符匹配剩余情况,使用 | 将多个字面值组合到一个模式中。模式之后可以使用 if 子句,如果判断结果为假,则 match 会继续尝试匹配下一个 case 块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Point:
__match_args__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y

match points:
case []:
print("No points")
case [Point(0, 0)]:
print("The origin")
case [Point(x, y)]:
print(f"Single point {x}, {y}")
case [Point(0, y1), Point(0, y2)]:
print(f"Two on the Y axis at {y1}, {y2}")
case _:
print("Something else")
1
2
3
4
5
match point:
case Point(x, y) if x == y:
print(f"Y=X at {x}")
case Point(x, y):
print(f"Not on the diagonal")

使用 def 定义函数,如果函数内第一个语句是字符串,则该字符串是文档字符串。函数中的所有变量赋值,都会将值存储在局部符号表中。函数中查找变量引用的顺序依次是,当前函数的局部符号表,外层函数的局部符号表,全局符号表,内置名称表。所以尽管可以引用全局变量和外层函数变量,但是赋值会创建本地变量,除非使用 global/nonlocal 语句定义该变量。函数参数是值传递的,值是对象的引用(地址)。

1
2
3
4
5
6
7
8
9
10
11
12
>>> def fib(n):    # write Fibonacci series less than n
... """Print a Fibonacci series less than n."""
... a, b = 0, 1
... while a < n:
... print(a, end=' ')
... a, b = b, a+b
... print()
...
... # Now call the function we just defined:
... fib(2000)
...
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

函数定义将函数名和函数对象关联,存储在当前符号表中。可以使用其他名称指向相同函数对象。没有 return 语句的函数返回 None,通常解释器会隐藏单个 None 值,可以使用 print() 函数打印。

1
2
3
4
5
>>> fib
<function fib at 0x000001B7F0AA1120>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89
1
2
3
>>> fib(0)
>>> print(fib(0))
None

可以为函数参数指定默认值,默认值在函数定义时求值。默认值只计算一次,当默认值是对象时,多次调用之间会共享该对象。如果是可变对象,则之后的调用会被之前的影响,如果不想共享默认值则需要显式处理。

1
2
3
4
5
6
7
8
9
>>> i = 5
...
... def f(arg=i):
... print(arg)
...
... i = 6
... f()
...
5
1
2
3
4
5
6
7
8
9
10
11
>>> def f(a, L=[]):
... L.append(a)
... return L
...
... print(f(1))
... print(f(2))
... print(f(3))
...
[1]
[1, 2]
[1, 2, 3]
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def f(a, L=None):
... if L is None:
... L = []
... L.append(a)
... return L
...
... print(f(1))
... print(f(2))
... print(f(3))
...
[1]
[2]
[3]

可以使用 *args 表示可变参数,该参数是元组类型。可以使用 *** 执行解包。

1
2
3
4
5
>>> list(range(3, 6))            # normal call with separate arguments
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args)) # call with arguments unpacked from a list
[3, 4, 5]
1
2
3
4
5
6
7
8
9
>>> def parrot(voltage, state='a stiff', action='voom'):
... print("-- This parrot wouldn't", action, end=' ')
... print("if you put", voltage, "volts through it.", end=' ')
... print("E's", state, "!")
...
... d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
... parrot(**d)
...
-- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !

使用 lambda 表达式创建匿名函数。可以为函数添加注解,注解被存储在函数的 __annotations__ 属性中。

1
2
3
4
5
6
7
8
>>> def make_incrementor(n):
... return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43
1
2
3
4
5
>>> f = make_incrementor(42)
>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]
1
2
3
4
5
6
7
8
9
10
>>> def f(ham: str, eggs: str = 'eggs') -> str:
... print("Annotations:", f.__annotations__)
... print("Arguments:", ham, eggs)
... return ham + ' and ' + eggs
...
... f('spam')
...
Annotations: {'ham': <class 'str'>, 'eggs': <class 'str'>, 'return': <class 'str'>}
Arguments: spam eggs
'spam and eggs'

Data Structures

列表推导式的方括号 [] 中包含以下内容:一个表达式,一个 for 子句,之后是零个或多个 for/if 子句。使用 zip() 函数在多个迭代器上并行迭代,每个迭代器返回一个元素组成元组。使用 del 语句删除列表元素。

1
2
3
4
5
6
7
8
9
>>> combs = []
... for x in [1,2,3]:
... for y in [3,1,4]:
... if x != y:
... combs.append((x, y))
...
... combs
...
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
1
2
>>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
1
2
3
4
5
6
7
>>> matrix = [
... [1, 2, 3, 4],
... [5, 6, 7, 8],
... [9, 10, 11, 12],
... ]
>>> list(zip(*matrix))
[(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]

元组由逗号分隔的值组成,元组是不可变的,但可以包含可变对象,此时可以修改该可变对象。空元组使用 () 创建,单元素元组使用该元素之后跟 , 逗号创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> t = 12345, 54321, 'hello!'
>>> t[0]
12345
>>> t
(12345, 54321, 'hello!')
>>> # Tuples may be nested:
>>> u = t, (1, 2, 3, 4, 5)
>>> u
((12345, 54321, 'hello!'), (1, 2, 3, 4, 5))
>>> # Tuples are immutable:
>>> t[0] = 88888
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> # but they can contain mutable objects:
>>> v = ([1, 2, 3], [3, 2, 1])
>>> v
([1, 2, 3], [3, 2, 1])
1
2
3
4
5
6
7
8
>>> empty = ()
>>> singleton = 'hello', # <-- note trailing comma
>>> len(empty)
0
>>> len(singleton)
1
>>> singleton
('hello',)

可以使用花括号或 set() 函数创建集合,创建空集合只能使用 set() 而不能使用 {}{} 创建的是空字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}
>>> print(basket) # show that duplicates have been removed
{'orange', 'banana', 'pear', 'apple'}
>>> 'orange' in basket # fast membership testing
True
>>> 'crabgrass' in basket
False

>>> # Demonstrate set operations on unique letters from two words
>>>
>>> a = set('abracadabra')
>>> b = set('alacazam')
>>> a # unique letters in a
{'a', 'r', 'b', 'c', 'd'}
>>> a - b # letters in a but not in b
{'r', 'd', 'b'}
>>> a | b # letters in a or b or both
{'a', 'c', 'r', 'd', 'b', 'm', 'z', 'l'}
>>> a & b # letters in both a and b
{'a', 'c'}
>>> a ^ b # letters in a or b but not both
{'r', 'd', 'b', 'm', 'z', 'l'}

字典的键必须是不可变类型,使用 {} 创建空字典,获取不存在键的值会报错。使用 list(d) 获取键列表,使用 sorted(d) 获取排序之后的键列表,使用 innot in 判断键是否存在,使用 dict() 函数创建字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> tel = {'jack': 4098, 'sape': 4139}
>>> tel['guido'] = 4127
>>> tel
{'jack': 4098, 'sape': 4139, 'guido': 4127}
>>> tel['jack']
4098
>>> del tel['sape']
>>> tel['irv'] = 4127
>>> tel
{'jack': 4098, 'guido': 4127, 'irv': 4127}
>>> list(tel)
['jack', 'guido', 'irv']
>>> sorted(tel)
['guido', 'irv', 'jack']
>>> 'guido' in tel
True
>>> 'jack' not in tel
False

遍历字典的键值对使用 items() 方法,遍历序列的索引和元素使用 enumerate() 函数,并行遍历使用 zip() 函数,反向遍历使用 reversed() 函数,排序遍历使用 sorted() 函数。

1
2
3
4
5
6
>>> knights = {'gallahad': 'the pure', 'robin': 'the brave'}
... for k, v in knights.items():
... print(k, v)
...
gallahad the pure
robin the brave
1
2
3
4
5
6
>>> for i, v in enumerate(['tic', 'tac', 'toe']):
... print(i, v)
...
0 tic
1 tac
2 toe
1
2
3
4
5
6
7
8
>>> questions = ['name', 'quest', 'favorite color']
... answers = ['lancelot', 'the holy grail', 'blue']
... for q, a in zip(questions, answers):
... print('What is your {0}? It is {1}.'.format(q, a))
...
What is your name? It is lancelot.
What is your quest? It is the holy grail.
What is your favorite color? It is blue.

使用 isis not 比较两个对象是否是同一个对象。比较运算支持链式操作,a < b == ca < b and b == c 等价。andornot 表示与或非,相当于 Java 中的 &&||!。布尔值有 TrueFalse,使用大写字母开头。在表达式内部赋值需要使用 :=,避免误用 =。布尔运算中使用普通值而不是布尔值时,短路运算的返回值是最后一个已求值的参数。

1
2
3
4
>>> string1, string2, string3 = '', 'Trondheim', 'Hammer Dance'
>>> non_null = string1 or string2 or string3
>>> non_null
'Trondheim'

Classes

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
>>> def scope_test():
... def do_local():
... spam = "local spam"
...
... def do_nonlocal():
... nonlocal spam
... spam = "nonlocal spam"
...
... def do_global():
... global spam
... spam = "global spam"
...
... spam = "test spam"
... do_local()
... print("After local assignment:", spam)
... do_nonlocal()
... print("After nonlocal assignment:", spam)
... do_global()
... print("After global assignment:", spam)
...
... scope_test()
... print("In global scope:", spam)
...
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

可以为类定义 __init__() 方法,在创建对象之后自动执行。实例对象作为第一个参数隐式传递给方法,obj_name.foo() 相当于 ClassName.foo(obj_name),该参数通常被命名为 self,也可以被命名为其他名称,self 没有特殊含义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Dog:

kind = 'canine' # class variable shared by all instances

def __init__(self, name):
self.name = name # instance variable unique to each instance

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # shared by all dogs
'canine'
>>> e.kind # shared by all dogs
'canine'
>>> d.name # unique to d
'Fido'
>>> e.name # unique to e
'Buddy'

可以动态给对象添加属性或方法,可以在类之外定义函数,然后将其赋值给类属性,毕竟 self 没有特殊含义。在 Python 中,所有值都是对象,都有相应的类型,类型信息存储在 object.__class__ 中。

1
2
3
4
5
6
7
8
9
10
11
>>> class Warehouse:
... purpose = 'storage'
... region = 'west'
...
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east
1
2
3
4
5
6
7
8
9
10
11
# Function defined outside the class
def f1(self, x, y):
return min(x, x+y)

class C:
f = f1

def g(self):
return 'hello world'

h = g

Python 中的方法都是 virtual 方法(动态绑定),属性默认是公有的。不存在私有变量,不过约定以单下划线 _ 开头的变量应该被视为私有的,不论是函数、方法或属性。在类定义中,名称改写(name mangling)会将至少带有两个前缀下划线的标识符 __update 替换为 _Mapping_update,避免子类重写方法影响父类中相应方法的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)

def update(self, iterable):
for item in iterable:
self.items_list.append(item)

__update = update # private copy of original update() method

class MappingSubclass(Mapping):

def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)