# 顺序编程
# 流程控制
Go 语言提供的流程控制语句包括 if
、switch
、for
、goto
、select
,其中 select
用于监听 channel
(通道)在讲解通道的时候再详细介绍。
# if 语句
语法:
if optionalStatement1; booleanExpression1 {
block1
} else if optionalStatement2; booleanExpression2 {
block2
} else {
block3
}
2
3
4
5
6
7
其中 optionalStatement
是可选的表达式,真正决定分支走向的是 booleanExpression1
的值。
// `if` 和 `else` 分支结构在 Go 中当然是直接了当的了。
package main
import "fmt"
func main() {
// 这里是一个基本的例子。
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
// 你可以不要 `else` 只用 `if` 语句。
if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}
// 在条件语句之前可以有一个语句;任何在这里声明的变量
// 都可以在所有的条件分支中使用。
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
}
// 注意,在 Go 中,你可以不使用圆括号,但是花括号是需
// 要的。
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
# for 语句
Go 语言的 for
语句可以遍历数组,切片,映射等类型,也可以用于无限循环。以下是其语法:
for { // 无限循环
block
}
for booleanExpression { // while循环,在Go语言中没有while关键字
}
for index, char := range aString { // 迭代字符串
}
for item := range aChannel { // 迭代通道
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// `for` 是 Go 中唯一的循环结构。这里有 `for` 循环
// 的三个基本使用方式。
package main
import "fmt"
func main() {
// 最常用的方式,带单个循环条件。
i := 1
for i <= 3 {
fmt.Println(i)
i = i + 1
}
// 经典的初始化/条件/后续形式 `for` 循环。
for j := 7; j <= 9; j++ {
fmt.Println(j)
}
// 不带条件的 `for` 循环将一直执行,直到在循环体内使用
// 了 `break` 或者 `return` 来跳出循环。
for {
fmt.Println("loop")
break
}
}
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
# 跳转语句
Go 语言中使用 goto
关键字实现跳转。goto
语句的语义非常简单,就是跳转到本函数内的某个标签,例如:
func myfunc(){
i := 0
THIS: //定义一个THIS标签
fmt.Println(i)
i++
if i < 1 {
goto THIS //跳转到THIS标签
}
}
2
3
4
5
6
7
8
9
# switch 语句
Go 语言中 switch
分支既可用于常用的分支就像 C 语言中的 switch
一样,也可以用于类型开关,所谓类型开关就是用于判断变量属于什么类型。但是需要注意的是 Go 语言的 switch
语句不会自动贯穿,相反,如果想要贯穿需要添加 fallthrough
语句。表达式开关 switch
的语法如下:
switch optionalStatement; optionalExpression {
case expression1: block1
...
case expressionN: blockN
default: blockD
}
2
3
4
5
6
下面是个例子:
switch { // 没有表达式,默认为True值,匹配分支中值为True的分支
case value < minimum:
return minimum
case value > maximum:
return maximum
default:
return value
}
2
3
4
5
6
7
8
在上面的例子中,switch 后面没有默认的表达式,这个时候 Go 语言默认其值为 True
。
在前面我们提到过类型断言,如果我们知道变量的类型就可以使用类型断言,但是当我们知道类型可能是许多类型中的一种时候,我们就可以使用类型开关。其语法如下:
switch optionalStatement; typeSwitchGuard {
case type1: block1
...
case typeN: blockN
default: blockD
}
2
3
4
5
6
说了这么多,让我们进行下练习,创建源文件 switch_t.go
,输入以下代码:
package main
import (
"fmt"
)
func classchecker(items ...interface{}) { // 创建一个函数,该函数可以接受任意多的任意类型的参数
for i, x := range items {
switch x := x.(type) { // 创建了影子变量
case bool:
fmt.Printf("param #%d is a bool, value: %t\n", i, x)
case float64:
fmt.Printf("param #%d is a float64, value: %f\n", i, x)
case int, int8, int16, int32, int64:
fmt.Printf("param #%d is a int, value: %d\n", i, x)
case uint, uint8, uint16, uint32, uint64:
fmt.Printf("param #%d is a uint, value: %d\n", i, x)
case nil:
fmt.Printf("param #%d is a nil\n", i)
case string:
fmt.Printf("param #%d is a string, value: %s\n", i, x)
default:
fmt.Printf("param #%d's type is unknow\n", i)
}
}
}
func main() {
classchecker(5, -17.98, "AIDEN", nil, true, complex(1, 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
以上代码中我们首先创建了一个接收任意数量任意类型参数的函数,然后使用 for ... range aSlice
的语法迭代了每一个在切片 items
中的元素,接着使用了 switch
类型开关判断了每一个参数的类型,并打印了其值和类型。程序运行输出如下:
$ go run switch_t.go
param #0 is a int, value: 5
param #1 is a float64, value: -17.980000
param #2 is a string, value: AIDEN
param #3 is a nil
param #4 is a bool, value: true
param #5's type is unknow
2
3
4
5
6
7
// _switch_ ,方便的条件分支语句。
package main
import "fmt"
import "time"
func main() {
// 一个基本的 `switch`。
i := 2
fmt.Print("write ", i, " as ")
switch i {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
}
// 在一个 `case` 语句中,你可以使用逗号来分隔多个表
// 达式。在这个例子中,我们很好的使用了可选的
// `default` 分支。
switch time.Now().Weekday() {
case time.Saturday, time.Sunday:
fmt.Println("it's the weekend")
default:
fmt.Println("it's a weekday")
}
// 不带表达式的 `switch` 是实现 if/else 逻辑的另一种
// 方式。这里展示了 `case` 表达式是如何使用非常量的。
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("it's before noon")
default:
fmt.Println("it's after noon")
}
}
// todo: type switches
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
# 函数
Go 语言可以很方便的自定义函数,其中有特殊的函数 main
函数。main
函数必须出现在 main
包里,且只能出现一次。当 Go 程序运行时候会自动调用 main
函数开始整个程序的执行。main
函数不可接收任何参数,也不返回任何结果。
# 函数的定义
在 Go 语言中,函数的基本组成包括:关键字 func
、函数名、参数列表、返回值、函数体和返回语句,这里我们用一个简单的加法函数来对函数的定义进行说明。
package add
func Add(a int, b int) (num int){
return a + b
}
2
3
4
5
# 函数的调用
函数调用非常简单,先将被调用函数所在的包导入,就可以直接使用该函数了。注意需要把包文件夹放到 $GOPATH
目录中,实例如下:
package main
import (
"add" //导入 add 包
"fmt"
)
func main(){
c := add.Add(1, 2) //调用 add 包中的 add 函数
fmt.Println(c)
}
2
3
4
5
6
7
8
9
10
11
// _函数_ 是 Go 的中心。我们将通过一些不同的例子来
// 进行学习。
package main
import "fmt"
// 这里是一个函数,接受两个 `int` 并且以 `int` 返回它
// 们的和
func plus(a int, b int) int {
// Go 需要明确的返回值,例如,它不会自动返回最
// 后一个表达式的值
return a + b
}
func main() {
// 正如你期望的那样,通过 `name(args)` 来调用一
// 个函数,
res := plus(1, 2)
fmt.Println("1+2 =", res)
}
// todo: coalesced parameter types
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 函数的多返回值
与 C/C++ 和 JAVA 不同,Go 语言的函数和方法可以有多个返回值,这是 Go 提供的一个优美的特性,示例如下:
package Divide
import "errors"
func divide (a int, b int) (num int, err error){ //定义两个返回值
if b == 0 {
err = errors.New("被除数不能为零!")
return
}
return a / b, nil //支持多个返回值
}
2
3
4
5
6
7
8
9
10
// Go 内建_多返回值_ 支持。这个特性在 Go 语言中经常被用到,
// 例如用来同时返回一个函数的结果和错误信息。
package main
import "fmt"
// `(int, int)` 在这个函数中标志着这个函数返回 2 个 `int`。
func vals() (int, int) {
return 3, 7
}
func main() {
// 这里我们通过_多赋值_ 操作来使用这两个不同的返回值。
a, b := vals()
fmt.Println(a)
fmt.Println(b)
// 如果你仅仅想返回值的一部分的话,你可以使用空白定
// 义符 `_`。
_, c := vals()
fmt.Println(c)
}
// todo: named return parameters
// todo: naked returns
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
# 变参函数
// [_可变参数函数_](http://zh.wikipedia.org/wiki/可變參數函數)。可以用任意
// 数量的参数调用。例如,`fmt.Println` 是一个常见的变参函数。
package main
import "fmt"
// 这个函数使用任意数目的 `int` 作为参数。
func sum(nums ...int) {
fmt.Print(nums, " ")
total := 0
for _, num := range nums {
total += num
}
fmt.Println(total)
}
func main() {
// 变参函数使用常规的调用方式,除了参数比较特殊。
sum(1, 2)
sum(1, 2, 3)
// 如果你的 slice 已经有了多个值,想把它们作为变参
// 使用,你要这样调用 `func(slice...)`。
nums := []int{1, 2, 3, 4}
sum(nums...)
}
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
# 匿名函数
在 Go 语言中,你可以在代码里随时定义匿名函数,匿名函数由一个不带函数名的函数声明和函数体组成,示例如下:
func (a, b, c int) bool {
return a * b < c
}
2
3
你可以将匿名函数直接赋值给一个变量,也可以直接调用运行,示例如下:
x := func (a, b, c int) bool {
return a * b < c
}
func (a, b, c int) bool {
return a * b < c
} (1, 2, 3) //小括号内直接给参数列表表示函数调用
2
3
4
5
6
7
Go 支持通过闭包
来使用匿名函数
。匿名函数在你想定义一个不需要命名的内联函数时是很实用的。
package main
import "fmt"
// 这个 `intSeq` 函数返回另一个在 `intSeq` 函数体内定义的
// 匿名函数。这个返回的函数使用闭包的方式 _隐藏_ 变量 `i`。
func intSeq() func() int {
i := 0
return func() int {
i += 1
return i
}
}
func main() {
// 我们调用 `intSeq` 函数,将返回值(也是一个函数)赋给
// `nextInt`。这个函数的值包含了自己的值 `i`,这样在每
// 次调用 `nextInt` 是都会更新 `i` 的值。
nextInt := intSeq()
// 通过多次调用 `nextInt` 来看看闭包的效果。
fmt.Println(nextInt())
fmt.Println(nextInt())
fmt.Println(nextInt())
// 为了确认这个状态对于这个特定的函数是唯一的,我们
// 重新创建并测试一下。
newInts := intSeq()
fmt.Println(newInts())
}
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
# 类型处理
# 类型转换
Go 语言提供了一种在不同但相互兼容的类型之间相互转换的方式,这种转换非常有用并且是安全的。但是需要注意的是在数值之间进行转换可能造成其他问题,如精度丢失或者错误的结果。以下是类型转换的语法:
resultOfType := Type(expression)
几个例子:
x := int16(2345) // 声明一个类型为int16的整数,其值为2345
y := int32(x) // 将int16类型的整数转换为int32类型
a := uint16(65000) // 声明一个类型为uint16类型的整数
b := int16(a) // 转换为int16类型,虽然能转换成功,但是由于65000超过in16类型的范围,会导致结果错误,b的值为 -536
2
3
4
另外在 Go 语言中可以通过 type
关键字声明类型,如 type StringsSlice []string
将 []string
(string
类型的切片)声明为 StringSlice
类型。
# 类型断言
说到类型断言就需要先了解下 Go 语言中的接口。在 Go 语言中接口是一个自定义类型。它声明了一个或者多个方法。任何实现了这些方法的对象(类型)都满足这个接口。
接口是完全抽象的,不能实例化。interface{}
类型表示一个空接口,任何类型都满足空接口。也就是说 interface{}
类型的值可以用于表示任意 Go 语言类型的值。
这里的空接口有点类似于 Python 语言中的 object 实例。既然 interface{}
可以用于表示任意类型,那有的时候我们需要将 interface{}
类型转换为我们需要的类型,这个操作称为类型断言。
一般情况下只有我们希望表达式是某种特定类型的值时才使用类型断言。Go 语言中可以使用以下语法:
resultOfType, boolean := expression.(Type)
:安全的类型断言。resultOfType := expression.(Type)
:非安全的类型断言,失败时程序会产生异常。
创建源文件 type_t.go
,输入以下源文件:
package main
import (
"fmt"
)
func main() {
x := uint16(65000)
y := int16(x) // 将 x转换为int16类型
fmt.Printf("type and value of x is: %T and %d\n", x, x) // %T 格式化指令的作用是输出变量的类型
fmt.Printf("type and value of y is: %T and %d\n", y, y)
var i interface{} = 99 // 创建一个interface{}类型,其值为99
var s interface{} = []string{"left", "right"}
j := i.(int) // 我们假设i是兼容int类型,并使用类型断言将其转换为int类型
fmt.Printf("type and value of j is: %T and %d\n", j, j)
if s, ok := s.([]string); ok { // 创建了影子变量,if的作用域中覆盖了外部的变量s
fmt.Printf("%T -> %q\n", s, s)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
运行程序:
$ go run type_t.go
type and value of x is: uint16 and 65000
type and value of y is: int16 and -536
type and value of j is: int and 99
[]string -> ["left" "right"]
2
3
4
5
# 错误处理
错误处理是任何语言都需要考虑到的问题,而 Go 语言在错误处理上解决得更为完善,优雅的错误处理机制是 Go 语言的一大特点。
# error
Go 语言引入了一个错误处理的标准模式,即 error
接口,该接口定义如下:
type error interface {
Error() string
}
2
3
对于大多数函数,如果要返回错误,可以将 error
作为多返回值的最后一个:
func foo(param int)(ret int, err error)
{
...
}
2
3
4
调用时的代码:
n, err := foo(0)
if err != nil {
// 错误处理
} else {
// 使用返回值n
}
2
3
4
5
6
我们还可以自定义错误类型,创建源文件 error.go
,输入以下代码:
package main
import "fmt"
import "errors"
//自定义的出错结构
type myError struct {
arg int
errMsg string
}
//实现Error接口
func (e *myError) Error() string {
return fmt.Sprintf("%d - %s", e.arg, e.errMsg)
}
//两种出错
func error_test(arg int) (int, error) {
if arg < 0 {
return -1, errors.New("Bad Arguments - negtive!")
}else if arg >256 {
return -1, &myError{arg, "Bad Arguments - too large!"}
}
return arg*arg, nil
}
//相关的测试
func main() {
for _, i := range []int{-1, 4, 1000} {
if r, e := error_test(i); e != nil {
fmt.Println("failed:", e)
} else {
fmt.Println("success:", r)
}
}
}
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
// Go 语言使用一个独立的·明确的返回值来传递错误信息的。这与
// 使用异常的 Java 和 Ruby 以及在 C 语言中经常见到的超重的
// 单返回值/错误值相比,Go 语言的处理方式能清楚的知道哪个函数
// 返回了错误,并能像调用那些没有出错的函数一样调用。
package main
import "errors"
import "fmt"
// 按照惯例,错误通常是最后一个返回值并且是 `error` 类
// 型,一个内建的接口。
func f1(arg int) (int, error) {
if arg == 42 {
// `errors.New` 构造一个使用给定的错误信息的基本
// `error` 值。
return -1, errors.New("can't work with 42")
}
// 返回错误值为 nil 代表没有错误。
return arg + 3, nil
}
// 通过实现 `Error` 方法来自定义 `error` 类型是可以的
。
// 这里使用自定义错误类型来表示上面的参数错误。
type argError struct {
arg int
prob string
}
func (e *argError) Error() string {
return fmt.Sprintf("%d - %s", e.arg, e.prob)
}
func f2(arg int) (int, error) {
if arg == 42 {
// 在这个例子中,我们使用 `&argError` 语法来建立一个
// 新的结构体,并提供了 `arg` 和 `prob` 这个两个字段
// 的值。
return -1, &argError{arg, "can't work with it"}
}
return arg + 3, nil
}
func main() {
// 下面的两个循环测试了各个返回错误的函数。注意在 `if`
// 行内的错误检查代码,在 Go 中是一个普遍的用法。
for _, i := range []int{7, 42} {
if r, e := f1(i); e != nil {
fmt.Println("f1 failed:", e)
} else {
fmt.Println("f1 worked:", r)
}
}
for _, i := range []int{7, 42} {
if r, e := f2(i); e != nil {
fmt.Println("f2 failed:", e)
} else {
fmt.Println("f2 worked:", r)
}
}
// 你如果想在程序中使用一个自定义错误类型中的数据,你
// 需要通过类型断言来得到这个错误类型的实例。
_, e := f2(42)
if ae, ok := e.(*argError); ok {
fmt.Println(ae.arg)
fmt.Println(ae.prob)
}
}
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# defer
你可以在 Go 函数中添加多个 defer
语句,当函数执行到最后时,这些 defer 语句会按照逆序执行(即最后一个 defer
语句将最先执行),最后该函数返回。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,我们一般写打开一个资源是这样操作的:
func CopyFile(dst, src string) (w int64, err error) {
srcFile, err := os.Open(src)
if err != nil {
return
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return
}
defer dstFile.Close()
return io.Copy(dstFile, srcFile)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
如果 defer
后面一条语句干不完清理工作,也可以使用一个匿名函数:
defer func(){
...
}()
2
3
注意,defer
语句是在 return
之后执行的,新建源文件 defer.go
输入以下代码:
func test() (result int) {
defer func() {
result = 12
}()
return 10
}
func main() {
fmt.Println(test()) // 12
}
2
3
4
5
6
7
8
9
10
# panic 和 recover
panic()
函数用于抛出异常,recover()
函数用于捕获异常,这两个函数的原型如下:
func panic(interface{})
func recover() interface{}
2
当在一个函数中调用 panic()
时,正常的函数执行流程将立即终止,但函数中之前使用 defer
关键字延迟执行的语句将正常展开执行,之后该函数将返回到调用函数,并导致逐层向上执行 panic()
流程,直至所属的 goroutine
中所有正在执行的函数被终止。错误信息将被报告,包括在调用 panic()
函数时传入的参数,这个过程称为错误流程处理。
panic()
接受一个 interface{}
参数,可支持任意类型,例如:
panic(404)
panic("network broken")
panic(Error("file not exists"))
2
3
在 defer
语句中,可以使用 recover()
终止错误处理流程,这样可以避免异常向上传递,但要注意 recover()
之后,程序不会再回到 panic()
那里,函数仍在 defer
之后返回。新建一个源文件 error1.go
,输入以下代码:
func foo() {
panic(errors.New("i'm a bug"))
return
}
func test() (result int) {
defer func() {
if r := recover(); r != nil {
err := r.(error)
fmt.Println("Cache Exception:", err)
}
}()
foo()
return 10
}
func main() {
fmt.Println(test()) // 0
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
panic
意味着有些出乎意料的错误发生。通常我们用它来表示程序正常运行中不应该出现的,或者我们没有处理好的错误。
package main
import "os"
func main() {
// 我们将在真个网站中使用 panic 来检查预期外的错误。这个
// 是唯一一个为 panic 准备的例子。
panic("a problem")
// panic 的一个基本用法就是在一个函数返回了错误值但是我们并不知道(或
// 者不想)处理时终止运行。这里是一个在创建一个新文件时返回异常错误时的
// `panic` 用法。
_, err := os.Create("/tmp/file")
if err != nil {
panic(err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
注意,不像有些语言中处理多个错误那样,在 Go 中习惯使用错误码返回任意可能的值。
Defer 被用来确保一个函数调用在程序执行结束前执行。同样用来执行一些清理工作。 defer
用在像其他语言中的ensure
和 finally
用到的地方。
package main
import "fmt"
import "os"
// 假设我们想要创建一个文件,向它进行写操作,然后在结束
// 时关闭它。这里展示了如何通过 `defer` 来做到这一切。
func main() {
// 在 `closeFile` 后得到一个文件对象,我们使用 defer
// 通过 `closeFile` 来关闭这个文件。这会在封闭函数
// (`main`)结束时执行,就是 `writeFile` 结束后。
f := createFile("/tmp/defer.txt")
defer closeFile(f)
writeFile(f)
}
func createFile(p string) *os.File {
fmt.Println("creating")
f, err := os.Create(p)
if err != nil {
panic(err)
}
return f
}
func writeFile(f *os.File) {
fmt.Println("writing")
fmt.Fprintln(f, "data")
}
func closeFile(f *os.File) {
fmt.Println("closing")
f.Close()
}
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