A Tour of Go
本教程覆盖了Go语言最重要的特性,包括:
- 基础知识:变量和函数、流控制语句、内置类型
- 方法和接口
- 泛型
- 并发
代码:https://github.com/ZZy979/go-tutorials/tree/main/tour
1.Hello World
下面是Go语言的Hello World程序:
1
2
3
4
5
6
7
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
可以使用Go Playground在线运行。也可以本地安装Go并在命令行中运行:
1
2
$ go run hello.go
Hello, 世界
2.基础知识
2.1 包、函数和声明
2.1.1 包
Go程序由包(package)组成,程序从main包开始运行。
下面的程序使用了导入路径为"fmt"和"math/rand"的包。
包名与导入路径的最后一个元素相同。例如,math/rand包中的文件以package rand开头。
2.1.2 导入
多个导入语句可以使用括号组织在一起,称为 “factored” (提取公因式的)。例如,
1
2
3
4
import (
"fmt"
"math"
)
等价于
1
2
import "fmt"
import "math"
使用factored导入语句是一种良好的编程风格。
2.1.3 导出名字
在Go中,以大写字母开头的名字称为导出的(exported)。例如Pi是导出的,而pi不是导出的。
当导入一个包时,只能访问其导出的名字,未导出的名字只能在包内访问。(注:二者的区别相当于Java的public和包可见)
例如,以下代码将报错 “name cos not exported by package math” 。应将math.cos改为math.Cos。
2.1.4 函数
函数(function)使用关键字func定义,可以接受零个或多个参数。注意Go的声明语法中类型在变量名之后。
例如,函数add()接受两个int类型的参数,返回类型为int。
1
2
3
func add(x int, y int) int {
return x + y
}
当两个或两个以上连续的参数类型相同时,可以只保留最后一个。例如,x int, y int可简写为x, y int。
函数可以返回任意个结果。例如,函数swap()返回两个字符串:
1
2
3
func swap(x, y string) (string, string) {
return y, x
}
注意:和Java一样,Go的函数是传值调用(包括数组和结构体类型),因此无法通过形参修改实参。但切片和映射等都是引用类型,形参和实参是指向同一对象的两个“指针”,因此可以通过形参修改实参指向的值。
2.1.5 命名的返回值
Go的函数返回值可以被命名,可以将其当作在函数顶部定义的变量。这些名字应当标明返回值的含义。
无参数的return语句返回这些命名返回值,称为“裸return”。裸return语句只应当用在短函数中,如下例所示。
1
2
3
4
5
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
2.1.6 变量
var语句用于声明变量(variable),类型放在最后。
var语句可以位于包级别或函数级别,如下例所示。
1
2
3
4
5
6
var c, python, java bool
func main() {
var i int
fmt.Println(i, c, python, java)
}
变量声明可以包含初始值。此时可以省略类型,变量类型由初始值决定。
1
2
3
4
5
6
var i, j int = 1, 2
func main() {
var c, python, java = true, false, "no!"
fmt.Println(i, j, c, python, java)
}
basics/variables-with-initializers.go
在函数内,可以用:=短赋值语句替代有初始值的var声明。在函数外不能使用:=。
例如:
1
2
k := 3
c, python, java := true, false, "no!"
basics/short-variable-declarations.go
2.1.7 基本类型
Go的基本类型有:
boolstringint,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64,uintptrbyte(uint8的别名)rune(int32的别名,表示Unicode码点)float32,float64complex64,complex128
下面的例子展示了几种类型的变量。与import语句一样,变量声明也可以写成 “factored” 形式。
1
2
3
4
5
6
7
8
9
10
11
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
func main() {
fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe)
fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt)
fmt.Printf("Type: %T Value: %v\n", z, z)
}
int、uint和uintptr通常在32位系统上是32位宽,在64位系统上是64位宽。当需要整数值时,应该使用int,除非有特殊原因使用固定宽度或无符号整数类型。
2.1.8 零值
没有显式初始化的变量将被赋予零值(zero value)。各种类型的零值如下表所示:
| 类型 | 零值 |
|---|---|
| 数值类型 | 0 |
| 布尔类型 | false |
| 字符串 | "" |
| 指针、切片、映射、函数、接口、channel | nil |
| 数组 | 所有元素都是零值 |
| 结构体 | 所有每个字段都是零值 |
2.1.9 类型转换
表达式T(v)将值v转换为类型T。例如:
1
2
3
i := 42
f := float64(i)
u := uint(f)
与C语言不同,在Go中不同类型间赋值必须进行显式类型转换。删除下面代码中的float64或uint转换将导致编译失败。
2.1.9 类型推断
当声明变量而没有显式指定类型时,变量的类型将通过右侧的值推断得到。
当右侧值是一个变量时,新变量的类型与其相同:
1
2
var i int
j := i // j is an int
当右侧值是数字常量时,整数、浮点数和复数的类型分别为int、float64和complex128:
1
2
3
i := 42 // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128
2.1.10 常量
常量使用关键字const声明,不能使用:=语法。例如:
1
const Pi = 3.14
数字常量是高精度的值。无类型的常量会根据其上下文需要采用相应的类型。
例如,在下面的程序中打印needInt(Big)会导致编译失败,因为将无类型常量Big赋给int值会溢出。
2.2 流控制语句:for、if、switch和defer
https://go.dev/tour/flowcontrol
2.2.1 for语句
Go只有一种循环语句,即for语句,语法如下:
1
2
3
for init; condition; post {
statements
}
例如:
1
2
3
4
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
注意:
- 与C和Java不同,Go的
for语句三部分周围没有小括号,而大括号是必需的。 init部分通常是短赋值语句(:=),声明的变量仅在for语句内部可见。- Go的
++运算符没有前缀形式。
for语句的init和post部分是可选的。例如:
1
2
3
4
sum := 1
for ; sum < 1000; {
sum += sum
}
如果只有condition部分,则可以将分号省略。此时for语句相当于C语言的while语句。
1
2
3
4
sum := 1
for sum < 1000 {
sum += sum
}
flowcontrol/for-is-gos-while.go
如果省略循环条件,就是无限循环:
1
2
for {
}
2.2.2 if语句
if语句的语法如下:
1
2
3
if condition {
statements
}
与for语句类似,条件周围没有小括号,但大括号是必需的。例如:
1
2
3
4
5
6
func sqrt(x float64) string {
if x < 0 {
return sqrt(-x) + "i"
}
return fmt.Sprint(math.Sqrt(x))
}
if语句也可以包含变量声明,作用域仅在if语句内部。
1
2
3
4
5
6
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}
flowcontrol/if-with-a-short-statement.go
if-else语句的语法如下:
1
2
3
4
5
if condition {
statements
} else {
statements
}
例如:
1
2
3
4
5
6
7
8
9
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
} else {
fmt.Printf("%g >= %g\n", v, lim)
}
// can't use v here, though
return lim
}
注意:
if语句声明的变量在else部分也可以访问。- 大括号的位置不能改变!
练习:牛顿法求平方根
使用牛顿法实现平方根函数Sqrt(x):初始猜测值z=1.0,使用迭代公式
1
z -= (z*z - x) / (2*z)
直到z2足够接近x。
flowcontrol/exercise-loops-and-functions.go
2.2.3 switch语句
switch语句是一种比if-else语句序列更简洁的方式,它会执行第一个值与条件表达式相等的case对应的语句。语法如下:
1
2
3
4
5
6
7
8
switch expr {
case val1:
statement1
case val2:
statement2
default:
statement
}
例如:
1
2
3
4
5
6
7
8
9
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("macOS.")
case "linux":
fmt.Println("Linux.")
default:
fmt.Printf("%s.\n", os)
}
与C和Java不同,Go的switch语句只执行匹配的一个分支(即没有fall through),相当于C语言的switch每个分支结尾自动添加了break。
另一个重要区别是,Go的switch中每个case的值不要求是常量,也不要求是整数。
switch的求值顺序为从上到下,直到一个case匹配成功。例如,
1
2
3
4
switch i {
case 0:
case f():
}
如果i==0则不会调用f()。
flowcontrol/switch-evaluation-order.go
没有条件的switch语句等价于switch true(即执行等于true的分支)。例如:
1
2
3
4
5
6
7
8
9
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
flowcontrol/switch-with-no-condition.go
2.2.4 defer语句
defer语句用于将函数的执行延迟到其所在函数返回时,但参数会立即求值。
例如,下面的程序会先打印 “hello” ,当main()函数返回时再打印 “world” 。
1
2
3
4
5
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
defer函数调用被压入一个栈中。当前函数返回时,按照后进先出的顺序执行这些调用。
关于defer语句的更多细节参见Defer, Panic, and Recover。
例如,下面的程序
1
2
3
4
5
6
7
8
9
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
输出结果为
1
2
3
4
5
6
7
8
9
10
11
12
counting
done
9
8
7
6
5
4
3
2
1
0
2.3 内置类型:结构体、切片和映射
Go的数据类型分为四类
- 基本类型:数字、字符串、布尔型
- 复合类型:数组、结构体
- 引用类型:指针、切片、映射、函数、channel
- 接口类型
2.3.1 指针
Go有指针(pointer)。指针用于保存值的内存地址。
类型*T是指向T类型值的指针。指针的零值是nil。
运算符&生成指向一个值的指针:
1
2
i := 42
p = &i // p points to i
运算符*表示指针指向的值,称为解引用(dereferencing):
1
2
fmt.Println(*p) // read i through the pointer p
*p = 21 // set i through the pointer p
与C语言不同,Go没有指针运算(如p+1或p-q)。
2.3.2 结构体
结构体(struct)是一些字段(field)的集合。例如:
1
2
3
4
type Vertex struct {
X int
Y int
}
像这样创建结构体类型的变量:
1
v := Vertex{1, 2}
使用.运算符访问结构体的字段:
1
2
v.X = 4
fmt.Println(v.X)
指向结构体的指针
结构体字段可以通过结构体指针访问。要访问指针p指向的结构体的字段X,可以使用(*p).X,也可简写为p.X(注:Go没有p->X这种写法)。
1
2
3
v := Vertex{1, 2}
p := &v
p.X = 1e9
结构体字面值
结构体字面值表示一个新创建的结构体值,列出所有字段的值:
1
v1 := Vertex{1, 2} // has type Vertex
也可以使用Name: Value语法(类似于Python的关键字参数)。此时可以仅列出一部分字段(省略的字段使用对应类型的零值),字段的顺序无关紧要。
1
2
v2 := Vertex{X: 1} // Y:0 is implicit
v3 := Vertex{} // X:0 and Y:0
注意:Go允许获取结构体字面值的指针,这在C++中是不合法的(右值不能取地址,除非是右值引用)。但Go不允许获取数字常量的指针。
1
2
p := &Vertex{1, 2} // OK
q := &42 // error
2.3.3 数组
类型[n]T是具有n个T类型值的数组。例如,
1
var a [10]int
将变量a声明为包含10个整数的数组。
使用[]运算符访问数组元素:
1
2
3
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
声明数组变量时可以使用{}提供元素的初始值,例如:
1
primes := [6]int{2, 3, 5, 7, 11, 13}
数组的长度是其类型的一部分,因此[3]int和[4]int是两种不同的数组类型,并且数组不能改变大小。
2.3.4 切片
数组的大小是固定的。切片(slice)是一种动态大小、灵活的视图,可以访问数组元素。实际上,切片比数组常用得多。
类型[]T是具有T类型元素的切片。
通过指定数组和两个索引(下界和上界)来构造切片:a[low:high]表示数组a的一个左闭右开区间(包括a[low],不包括a[high])。
例如:
1
var s []int = primes[1:4]
切片类似于数组的引用
- 切片不存储数据,仅描述底层数组的一部分。
- 多个切片可以共享底层数组,并且引用的数组区间可能重叠。
- 改变切片的元素会修改底层数组的对应元素,共享同一个底层数组的其他切片也能看到修改。
- 切片之间不能进行相等比较。
切片字面值
切片字面值类似于数组字面值,但没有长度。例如:
1
[]bool{true, true, false}
切片字面值构造了一个包含相同元素的数组,并创建了一个引用它的切片。
切片默认值
切片下界的默认值是0,上界的默认值是数组/切片的长度。
例如,对于长度为10的数组a,以下切片表达式是等价的:
1
2
3
4
a[0:10]
a[:10]
a[0:]
a[:]
切片长度和容量
切片具有长度(length)和容量(capacity)。
- 切片的长度是它包含元素的个数。
- 切片的容量是底层数组从下界开始的元素个数。
例如,对于长度为10的数组a,切片a[3:8]的长度为5、容量为7。
可以使用表达式len(s)和cap(s)获得切片s的长度和容量。
可以通过重新切片来扩展切片的长度,前提是长度不能超过容量。例如:
1
2
3
4
5
6
s := []int{2, 3, 5, 7, 11, 13} // len=6 cap=6
s = s[:0] // len=0 cap=6 [] 2 3 5 7 11 13
s = s[:4] // len=4 cap=6 [2 3 5 7] 11 13
s = s[2:] // len=2 cap=4 2 3 [5 7] 11 13
s = s[1:4] // len=3 cap=3 2 3 5 [7 11 13]
s = s[0:8] // runtime error: slice bounds out of range [:8] with capacity 3
Nil切片
切片的零值是nil。
nil切片的长度和容量都是0,没有底层数组。
用make函数创建切片
可以使用内置函数make()创建切片——这就是创建动态数组的方式。
make()函数创建一个给定长度的零值数组,并返回引用该数组的切片:
1
a := make([]int, 5) // len(a)=5
可以通过第三个参数指定容量:
1
2
3
4
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:] // len(b)=4, cap(b)=4
切片的切片
切片的元素可以是任意类型,包括切片。例如:
1
2
3
4
5
board := [][]string{
{"_", "_", "_"},
{"_", "_", "_"},
{"_", "_", "_"},
}
向切片添加元素
Go的内置函数append()用于向切片添加新元素,返回新的切片。详见文档func append。
1
func append(s []T, vs ...T) []T
第一个参数s是切片,其余参数是要添加到切片的值。返回值是原切片加上新元素后的切片。例如:
1
2
3
4
var s []int // len=0 cap=0 []
s = append(s, 0) // len=1 cap=1 [0]
s = append(s, 1) // len=2 cap=2 [0 1]
s = append(s, 2, 3, 4) // len=5 cap=6 [0 1 2 3 4]
注:向切片中添加新元素时
- 如果切片的底层数组足够大,则会覆盖数组中切片尾部之后的元素;否则会创建一个新的数组,返回的切片将会引用新的数组,此时不会修改原来的底层数组。
- 由于不能确认新的切片和原始切片是否引用相同的底层数组,应该将
append()的返回值直接赋值给输入的切片变量。例如:
1
2
3
4
a := []int{0, 1, 2}
s := a[0:2] // a=[0 1 2], s=[0 1]
s = append(s, 3) // a=[0 1 3], s=[0 1 3]
s = append(s, 4, 5) // a=[0 1 3], s=[0 1 3 4 5]
关于切片的更多细节参见Go Slices: usage and internals。
2.3.5 Range
range用于在for循环中遍历切片或映射。
使用range遍历切片时,每次返回两个值:索引和对应元素的拷贝(相当于Python中的enumerate()函数)。
1
2
3
4
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
如果不需要索引或值,可以将对应的变量名改为_:
1
2
for i, _ := range s
for _, v := range s
如果只需要索引,可以省略第二个变量(此时相当于Python中的for i in range(len(s)))(为什么这种形式不是遍历值?只遍历值不是更常用?):
1
for i := range s
练习:绘制函数图像
实现函数Pic(dx, dy),返回一个长度为dy的二维切片[][]uint8,其中每个元素是长度为dx的8位无符号整数切片。
运行该程序时,将展示一张图像,将Pic()返回的二维切片作为像素灰度值。每个元素由其x和y坐标计算得到,计算方式可以任意选择,例如(x+y)/2、x*y或x^y(异或)。
不同计算函数得到的图像如下。
(x+y)/2
x xor y
xy
x2 + y2
2.3.6 映射
映射(map)将键映射到值。
类型map[K]V表示键类型为K、值类型为V的映射。映射的零值是nil。
使用make()函数创建映射:make(map[K]V)。
使用[]运算符访问与键关联的值。例如:
1
2
m := make(map[string]Vertex)
m["Bell Labs"] = Vertex{40.68433, -74.39967}
映射字面值
映射字面值类似于结构体字面值,但键是必需的:
1
map[K]V{key1: value1, key2: value2, ...}
可以使用map[K]V{}创建空映射。
注意:如果写成多行的形式,最后一行的末尾也必须有逗号。例如:
1
2
3
4
var m = map[string]Vertex{
"Bell Labs": Vertex{40.68433, -74.39967},
"Google": Vertex{37.42202, -122.08408},
}
可以省略字面值中的结构体类型名。例如:
1
2
3
4
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
moretypes/map-literals-continued.go
操作map
- 插入或更新元素:
m[key] = value - 读取元素:
v := m[key],如果键不存在则返回值类型的零值。 - 查询键是否存在:
v, ok := m[key],如果键存在则ok为true,v为对应的值;否则ok为false,v为值类型的零值。 - 删除元素:
delete(m, key),如果键不存在则什么都不做。 - 映射之间不能进行相等比较。
可以使用range遍历映射的键值对(相当于Python的dict.items()),顺序是不确定的。例如:
1
2
3
4
m := map[string]int{"foo": 1, "bar": 2, "baz": 3}
for k, v := range m {
fmt.Println(k, v)
}
Go语言没有提供set类型,但可以通过只使用映射的键实现。
练习:单词计数
实现WordCount(s)函数,返回字符串s中每个“单次”的出现次数构成的映射。可以使用strings.Fields()将字符串按空白符分割成单词。
注:标准库strings和strconv包提供了字符串操作。
2.3.7 函数字面值
在Go中函数也是值,可以用作函数参数和返回值。
例如:
1
2
3
4
5
6
7
8
9
10
11
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot))
}
其中func(x, y float64) float64 {...}是函数字面值(function literal),定义了一个匿名函数(相当于C++和Java的Lambda表达式)。参见Go语言规范Function literals。
2.3.8 函数闭包
闭包(closure)是引用了函数体外部变量的函数。该函数可以访问和赋值所引用的变量,此时称函数“绑定”到该变量。
例如,每次调用函数adder()都会返回一个闭包,每个闭包绑定到自己的sum变量。
1
2
3
4
5
6
7
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
moretypes/function-closures.go
在上面的程序中,闭包pos和neg绑定的sum变量是独立的。
练习:斐波那契闭包
实现fibonacci()函数,返回一个函数闭包,该闭包返回连续的斐波那契数(1, 1, 2, 3, 5, …)。
moretypes/exercise-fibonacci-closure.go
注:这个例子类似于Python的生成器,每次调用f都会生成下一个值,但无法用for循环遍历。
3.方法和接口
3.1 方法
Go没有类,但是可以在类型上定义方法。
方法(method)是带有特殊的接收器(receiver)参数的函数。(注:接收器类似于Python方法的self参数)
接收器出现在单独的参数列表中,位于func关键字和方法名之间。例如,
1
2
3
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
在这个例子中,Abs()方法有一个Vertex类型的名为v的接收器。
使用.语法来调用方法,例如v.Abs()。
方法就是带有接收器参数的特殊函数。 Abs()方法等价的普通函数写法如下:
1
2
3
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
也可以为非结构体类型定义方法。例如:
1
2
3
4
5
6
7
8
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
注意:即使MyFloat是float64的别名,也必须使用类型转换。
方法接收器的类型必须与方法定义在同一个包中。不能为其他包中的类型定义方法(包括内置类型,例如int)。但可以通过定义类型别名实现,如上面的例子所示。
指针接收器
可以声明带有指针接收器的方法,即接收器类型为*T,其中T本身不能是一个指针类型。
例如,下面的Scale()方法是对*Vertex定义的。
1
2
3
4
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
带有指针接收器的方法可以修改接收器指针指向的值,而值接收器是原始值的副本。
例如,如果将Scale()方法改为值接收器,那么v.Scale(10)无法修改v的字段,下面的程序将输出5而不是50。
注:与访问字段一样,通过指针调用方法也是使用.语法。
类似地,如果将Scale()重写为接受非指针参数的函数,则函数内修改的只是原始值的副本,不会影响实参。
methods/methods-pointers-explained.go
调用带有指针参数的函数时,实参必须是指针:
1
2
3
var v Vertex
ScaleFunc(v, 5) // Compile error!
ScaleFunc(&v, 5) // OK
而调用带有指针接收器的方法时,接收器可以是指针也可以是值:
1
2
3
4
var v Vertex
v.Scale(5) // OK, same as (&v).Scale(5)
p := &v
p.Scale(10) // OK
这是因为Go会自动进行取地址,v.Scale(5)等价于(&v).Scale(5)。
反过来,调用带有值参数的函数时,实参必须是值:
1
2
3
var v Vertex
fmt.Println(AbsFunc(v)) // OK
fmt.Println(AbsFunc(&v)) // Compile error!
而调用带有值接收器的方法时,接收器可以是指针也可以是值:
1
2
3
4
var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK, same as (*p).Abs()
如果是指针则Go会自动解引用,即p.Abs()等价于(*p).Abs()。
选择指针接收器的原因有两个:
- 方法需要修改原始值。
- 避免值拷贝,对于大结构体将更加高效。
一般来说,一个类型的所有方法应该要么都是值接收器,要么都是指针接收器,不应该混合使用两者(在下一节中将解释原因)。
在下面的例子中,Scale()和Abs()方法的接收器类型都是*Vertex,尽管Abs()不需要修改原始值。
methods/methods-with-pointer-receivers.go
3.2 接口
接口(interface)类型定义为一组方法签名的集合。例如:
1
2
3
type Abser interface {
Abs() float64
}
接口Abser包含一个无参数、返回float64的方法Abs()。
只要类型T实现了接口定义的所有方法,就实现了该接口。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
类型MyFloat和*Vertex都定义了Abs()方法,因此实现了Abser接口。
注意:
- 在上面的例子中,
Vertex类型没有实现Abser接口,因为Abs()方法只对*Vertex类型定义了。 - 如果类型
T定义了一个接口的所有方法,但有些是值接收器、有些是指针接收器,则T和*T都没有实现该接口。这就是为什么推荐一种类型的所有方法的接收器保持一致的原因。
接口类型的变量可以存放任何实现了该接口的类型的值。例如,在下面的程序中,f和&v都可以赋给Abser类型的变量a,但v不可以。
1
2
3
4
5
6
7
8
9
10
11
func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
a = f // MyFloat implements Abser
a = &v // *Vertex implements Abser
a = v // error: Vertex does NOT implement Abser
fmt.Println(a.Abs())
}
Go的接口是隐式实现的,不需要像Java一样用implements关键字显式声明。这使得接口的定义和实现可以解耦,不需要出现在同一个包中。
methods/interfaces-are-satisfied-implicitly.go
接口值
接口值可以认为是值和实际类型的元组:(value, type)。
调用接口值的方法将会调用实际类型对应的同名方法(注:这类似于C++的虚函数调用)。
例如,假设类型*T和F都实现了接口I,则下面的两个i.M()分别调用T.M()和F.M()。
1
2
3
4
5
6
7
var i I
i = &T{"Hello"}
i.M() // calls T.M()
i = F(math.Pi)
i.M() // calls F.M()
如果接口值的底层值是nil,则调用方法时接收器将会是nil。例如:
1
2
3
4
var i I
var t *T
i = t // interface value with nil underlying value
i.M() // calls T.M() with nil receiver
在某些语言中这会引发空指针异常,但在Go中编写能够处理接收器为nil的方法是很常见的。例如下面例子中的方法M()。
1
2
3
4
5
6
7
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
methods/interface-values-with-nil.go
注意:
- 底层值是
nil的接口值本身不是nil。 - 在上面的例子中,如果接收器
t是nil,则访问t.S仍然会报错 “runtime error: invalid memory address or nil pointer dereference” 。
nil接口值没有底层值和实际类型。对nil接口值调用方法将产生运行时错误,因为无法确定要调用哪个具体方法。
1
2
var i I
i.M() // error: call method on nil interface value
methods/nil-interface-values.go
空接口
不包含任何方法的接口类型称为空接口:
1
interface{}
空接口可以表示任何类型的值,因为所有类型都实现了至少0个方法(类似于Java的Object类)。
空接口通常用于处理未知类型的值的代码。例如,fmt.Println()接受任意个数的interface{}类型的参数。
类型断言
类型断言(type assertion)用于访问接口值的底层值(即(value, type)中的value)。
1
t := i.(T)
该语句断言接口值i的实际类型为T,并将底层值赋给变量t。如果i的实际类型不是T则会报错(panic)。
要测试一个接口值是否是特定的类型,可以使用返回两个值的类型断言:
1
t, ok := i.(T)
如果i的实际类型是T,则ok是true,t是i的底层值;否则ok是false,t是T类型的零值。
类型switch
类型switch类似于普通switch语句,但其中的case指定类型(而不是值),用于与接口值的实际类型进行比较。
例如:
1
2
3
4
5
6
7
8
switch v := i.(type) {
case T:
// here v has type T
case S:
// here v has type S
default:
// no match; here v has the same type as i
}
其中声明部分的语法与类型断言i.(T)相同,只是具体类型T被替换为关键字type。
这个switch语句判断接口值i的实际类型是否是T或S。在这两个case中,变量v的类型分别是T和S,值是i的底层值。在default分支中,v的类型和值与i相同。
Stringer接口
最常用的接口之一是fmt包定义的Stringer:
1
2
3
type Stringer interface {
String() string
}
Stringer表示可以描述为字符串的类型 (其String()方法类似于Java的Object.toString()和Python的__str__()方法)。
例如,Person类型实现了Stringer接口,可以将自身转换为字符串。
1
2
3
4
5
6
7
8
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
如果一个类型实现了Stringer接口,那么fmt包中的打印函数会使用其String()方法返回的字符串,而不是默认表示。
练习:Stringer
让IPAddr类型实现fmt.Stringer接口,以点分十进制格式打印IP地址。例如,IPAddr{1, 2, 3, 4}打印成"1.2.3.4"。
1
type IPAddr [4]byte
3.3 错误
Go程序使用error值来表达错误状态。
error类型是一个内置接口:
1
2
3
type error interface {
Error() string
}
函数通常会返回一个error值,调用代码应当通过检查错误是否为nil来进行错误处理:错误为nil表示成功,否则表示失败。例如:
1
2
3
4
5
6
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
注:这个例子使用了自定义错误类型MyError。可以使用标准库函数errors.New()来创建error值。
练习:错误
修改之前的练习(2.2.2节)中牛顿法求平方根的Sqrt()函数,当参数为负数时返回一个error值。
创建一个新类型
1
type ErrNegativeSqrt float64
并使其实现error接口:
1
func (e ErrNegativeSqrt) Error() string
例如,ErrNegativeSqrt(-2).Error()返回"cannot Sqrt negative number: -2"。
注意:在Error()方法内部调用fmt.Sprint(e)会导致无限递归,因为该函数会调用e.Error()。为了避免这一问题,可以先将e转换为float64类型。
3.4 Reader接口
io包定义了Reader接口,用于读取流数据。Go标准库包含该接口的很多实现,包括文件、网络连接、压缩算法、加密算法等。
io.Reader接口有一个Read()方法:
1
2
3
type Reader interface {
Read(b []byte) (n int, err error)
}
Read()方法使用读取的数据填充给定的byte切片,返回填充的字节数和错误值。当流结束时返回io.EOF错误。
示例代码创建了一个strings.Reader,用于从字符串读取数据,每次读取8个字节。
练习:Reader
实现一个Reader类型,生成字符'A'的无限流。
练习:rot13Reader
一种常见的用法是用一个io.Reader包装另一个io.Reader,并以某种方式修改流。(注:这类似于Java中的组合输入流)
例如,gzip.NewReader()函数接受一个io.Reader参数(压缩的数据流),返回一个*gzip.Reader,该类型也实现了io.Reader(解压缩的数据流)。
编写一个类型rot13Reader,实现io.Reader,从另一个io.Reader读取数据,通过对所有字母字符应用rot13替换加密来修改流。
也就是说,对于输入流中的字母按以下规则替换:
- 输入:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz - 输出:
NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm - 其他字符不变
例如,输入 “Lbh penpxrq gur pbqr!” ,应该输出 “You cracked the code!” 。
methods/exercise-rot-reader.go
3.5 Image接口
image包定义了Image接口:
1
2
3
4
5
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}
该接口描述了一个矩形像素网格,Bounds()方法返回其矩形边界,At(x, y)返回坐标(x, y)处像素的颜色。详见文档image.Image。
例如:
1
2
3
m := image.NewRGBA(image.Rect(0, 0, 100, 100))
fmt.Println(m.Bounds())
fmt.Println(m.At(0, 0).RGBA())
练习:Image
实现一个类似于之前的图像生成器(2.3.5节),但这次返回一个image.Image接口值,而不是切片。
自定义一个Image类型,实现image.Image接口:
ColorModel()应该返回color.RGBAModel。Bounds()方法应该返回一个image.Rectangle,例如image.Rect(0, 0, w, h)。At()应该返回一个颜色值,上一个练习中的v值在这里对应color.RGBA{v, v, 255, 255}。
4.泛型
4.1 类型参数
Go的函数可以使用类型参数(type parameter)来处理多种类型,称为泛型函数(generic function)。函数的类型参数位于参数列表前的方括号内。例如:
1
func Index[T comparable](s []T, x T) int
这个声明表示s是满足内置约束comparable的任意类型T的切片,x也是相同类型的值。
约束comparable允许对特定类型的值使用==和!=运算符。在这个例子中,我们使用它来将一个值与切片的所有元素进行比较,直到找到匹配项。
1
2
3
4
5
6
7
8
9
10
11
// Index returns the index of x in s, or -1 if not found.
func Index[T comparable](s []T, x T) int {
for i, v := range s {
// v and x are type T, which has the comparable
// constraint, so we can use == here.
if v == x {
return i
}
}
return -1
}
这个Index()函数适用于任何支持比较的类型。
1
2
3
4
5
6
7
// Index works on a slice of ints
si := []int{10, 20, 15, -10}
fmt.Println(Index(si, 15))
// Index also works on a slice of strings
ss := []string{"foo", "bar", "baz"}
fmt.Println(Index(ss, "hello"))
4.2 泛型类型
除了泛型函数,Go还支持泛型类型(generic type)。类型也可以带有类型参数,这对于实现泛型数据结构非常有用。
下面的例子展示了一个可以存储任何类型值的单向链表的类型声明。
1
2
3
4
5
6
7
8
9
10
type Node[T any] struct {
next *Node[T]
val T
}
// List represents a singly-linked list that holds values of any type.
type List[T any] struct {
head *Node[T]
size int
}
包含方法的完整实现:
5.并发
https://go.dev/tour/concurrency
5.1 Goroutine
goroutine是由Go运行时管理的轻量级线程。
1
go f(x)
会启动一个新的goroutine来运行函数f(x),参数的求值发生在当前goroutine中,f的执行发生在新的goroutine中。
goroutine在相同的地址空间中运行,因此对共享内存的访问必须要同步。sync包提供了一些有用的原语。
示例程序启动了两个goroutine(其中一个是主goroutine),交替打印 “hello” 和 “world” 。
5.2 channel
channel是一种管道,可以使用运算符<-通过它发送和接收值。
1
2
ch <- v // Send v to channel ch.
v := <-ch // Receive from ch, and assign value to v.
(数据沿着箭头方向流动)
与映射和切片一样,channel也必须使用make()函数创建:
1
ch := make(chan int)
默认情况下,发送和接收操作会阻塞,直到另一端准备就绪。这使得goroutine之间无需显式的锁或条件变量即可同步。
示例代码使用两个goroutine分别对切片前半部分和后半部分的数字求和。一旦两个goroutine都完成计算,就会计算最终结果。
缓冲channel
channel可以带缓冲(buffered)。为了创建缓冲channel,需要将缓冲大小作为make()的第二个参数:
1
ch := make(chan int, 100)
cap(ch)返回channel的缓冲长度。
对于缓冲channel,发送操作只有当缓冲满时才会阻塞,接收操作只有当缓冲为空时才会阻塞。
concurrency/buffered-channels.go
在上面的例子中,如果将ch的容量改为1,或者在第一个打印语句之前再加一个ch <- 3,或者在最后再增加一个打印语句fmt.Println(<-ch),则程序会陷入死锁:
1
fatal error: all goroutines are asleep - deadlock!
range和close
发送者可以使用close()关闭一个channel,表示没有更多的值要发送。
接收者可以使用返回两个值的接收表达式来检查channel是否已经被关闭:
1
v, ok := <-ch
如果没有可接收的值并且channel已关闭,则ok为false。
循环for i := range c不断地从channel接收值,直到它被关闭。
注意:
- 只有发送者应该关闭channel,接收者绝不能关闭。向已关闭的channel发送值会导致panic。
- channel不像文件,通常不需要关闭。除非必须要告诉接收者没有更多的值,例如终止
range循环。
concurrency/range-and-close.go
5.3 select语句
select语句允许goroutine等待多个通信操作。
select语句会阻塞直到某个case可以执行,然后执行该分支。如果有多个case准备就绪,则随机选择一个。
例如,假设c和quit是两个channel,下面的语句当c可写入时执行第一个分支,当quit可读取时执行第二个分支。
1
2
3
4
5
6
7
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
注:
- 与
switch语句不同,select的各个case不是从上到下判断的。 - Go的
select语句类似于Python的select模块。
如果select语句中没有其他分支准备就绪,则执行default分支。
可以使用default分支尝试发送或接收操作,而不会阻塞:
1
2
3
4
5
6
select {
case i := <-c:
// use i
default:
// receiving from c would block
}
concurrency/default-selection.go
练习:等价二叉树
可能有许多不同的二叉树存储相同的值序列。例如,以下是两棵存储序列1, 1, 2, 3, 5, 8, 13的二叉树。
在这个练习中,将利用Go的并发和channel机制来编写一个检查两棵二叉树是否存储相同序列的函数。
二叉树类型定义如下:
1
2
3
4
5
type Tree struct {
Left *Tree
Value int
Right *Tree
}
1.实现Walk()函数,用于对二叉树进行中序遍历,并将结果发送到channel。
1
func Walk(t *tree.Tree, ch chan int)
2.测试Walk()函数。使用tree.New(k)创建一个随机结构但有序的二叉树,存储值k, 2k, …, 10k。创建一个channel ch并启动一个goroutine来执行Walk():
1
go Walk(tree.New(1), ch)
从ch读取并打印10个值,结果应该为1, 2, …, 10。
3.实现Same()函数,使用Walk()来确定t1和t2是否存储相同的值(即判断两棵二叉树的中序遍历结果是否相同)。
1
func Same(t1, t2 *tree.Tree) bool
4.测试Same()函数。Same(tree.New(1), tree.New(1))应该返回true,Same(tree.New(1), tree.New(2))应该返回false。
关于tree包的细节参见文档。
concurrency/exercise-equivalent-binary-trees.go
5.4 sync.Mutex
使用channel进行goroutine间通信非常方便。但如果我们不需要通信,只是想确保同一时刻只有一个goroutine可以访问某个变量,以避免冲突呢?
这种概念称为互斥(mutual exclusion),提供这种特性的数据结构的通常称为互斥锁(mutex)。
Go标准库提供了sync.Mutex类型,有Lock()和Unlock()两个方法(注:相当于Java中的锁)。
下面的例子实现了一个并发安全的计数器。map本身不是并发安全的,要在多个goroutine中访问同一个map必须进行同步。
可以通过使用Lock()和Unlock()包围一个代码块使其成为互斥执行的(即临界区),如Inc()方法所示。也可以使用defer来确保Mutex会被解锁,如Value()方法所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
c.v[key]++
c.mu.Unlock()
}
// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
defer c.mu.Unlock()
return c.v[key]
}
练习:网络爬虫
在这个练习中,将使用Go的并发特性实现并行网络爬虫。
实现Crawl()函数,并行爬取URL,但不要爬取重复的URL。
注:
fakeResult表示页面内容及其包含的URL。fakeFetcher是一个预定义的URL到fakeResult的映射。其Fetch()方法在映射中查找给定的URL,用于模拟页面爬取过程,如果URL不存在则返回一个错误。




