这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

控制流程

Golang 语法中的控制流程

流程控制语句属于特殊的语句。

1 - if 语句

Golang 中的 if 语句

If语句

类似for循环,go中的if语句也是同样,if后面不能有括号,而if里面必须要有花括号:

if x < 0 {
    return sqrt(-x) + "i"
}

for 一样,if 语句可以在条件之前执行一个简单的语句:

if v := math.Pow(x, n); v < lim {
    // v在这里可以访问
    return v
}
// v在这里不可以访问

注意:这个语句定义的变量的作用域仅在 if 范围之内,包括else:

if v := math.Pow(x, n); v < lim {
    return v
} else {
    // else这里可以访问
    fmt.Printf("%g >= %g\n", v, lim)
}

If statements

https://golang.org/ref/spec#If_statements

“if “语句根据布尔表达式的值指定两个分支的条件执行。如果表达式的值为真,则执行 “if “分支,否则,如果存在,则执行 “else “分支。

IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
if x > max {
	x = max
}

表达式前面可以有一个简单的语句,在表达式被评估之前执行。

if x := f(); x < y {
	return x
} else if x > z {
	return z
} else {
	return y
}

if

https://golang.org/doc/effective_go.html#if

在 go 中,一个简单的if是这样的:

if x > 0 {
    return y
}

强制括号鼓励在多行上写简单的if语句。无论如何这样做都是好的风格,特别是当正文中包含一个控制语句,如返回或break时。

由于if和switch接受一个初始化语句,所以通常会看到一个用来设置局部变量的语句。

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

在 Go 库中,你会发现,当一个 if 语句没有流入下一个语句时–也就是说,正文以 break、continue、goto 或 return 结尾时,不必要的 else 会被省略。

f, err := os.Open(name)
if err != nil {
    return err
}
codeUsing(f)

这是一个常见情况的例子,代码必须防范一连串的错误情况。如果成功的控制流向下运行,在出现错误情况时消除错误情况,那么代码的阅读效果就会很好。由于错误情况往往以return语句结束,因此产生的代码不需要 else语句。

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)

2 - switch 语句

Golang 中的 switch 语句

go语言实战

switch的语法和for、if类似,同样的括号和花括号使用规则,同样的容许在switch前执行一个简单的语句,同样的变量访问范围限制:

switch os := runtime.GOOS; os {
    case "darwin":
    	fmt.Println("OS X.")
    case "linux":
    	fmt.Println("Linux.")
    default:
    	fmt.Printf("%s.", os)
}

特别需要支出的是,和c、java中的switch语句不同,golang中的switch在命中某个case子语句并执行完成之后,会自动终结分支并结束switch语句。这是默认行为,和c,java中会自动继续下一个分支匹配,需要明确break才能退出不同。

如果想继续执行后面的case子语句,需要在case子语句最后使用 fallthrough 语句。

执行顺序

switch 的条件从上到下顺序执行,当匹配成功的时候终止。

switch i {
    case 0:
    case f():
}

i==0 时不会调用 f

if变体

没有条件的 switch 等同于 switch true ,这个变体可以用更清晰的形式来编写多个判断条件的 if 语句:

t := time.Now()
// 等同于 switch true {
switch {
    case t.Hour() < 12:
    	fmt.Println("Good morning!")
    case t.Hour() < 17:
    	fmt.Println("Good afternoon.")
    default:
    	fmt.Println("Good evening.")
}

golang语言规范

https://golang.org/ref/spec#Switch_statements

“switch “语句提供多向执行。表达式或类型指定符与 “switch “内的 “case “进行比较,以确定执行哪个分支。

SwitchStmt = ExprSwitchStmt | TypeSwitchStmt .

有两种形式:表达式switch和类型switch。在表达式switch中,case包含表达式,与switch表达式的值进行比较。在类型switch中,case中包含的类型是与特别注释的switch表达式的类型进行比较的。switch表达式在一个switch语句中只被评估一次。

表达式switch

在表达式switch中,对switch表达式进行评估,并从左到右、从上到下评估case表达式,case表达式不一定是常量,第一个等于switch表达式的会触发执行相关case的语句,其他case则跳过。如果没有匹配的情况,并且有一个 “default"情况,则执行它的语句。最多只能有一个 default case,它可能出现在 “switch “语句的任何地方。缺少的switch表达式相当于布尔值true。

ExprSwitchStmt = "switch" [ SimpleStmt ";" ] [ Expression ] "{" { ExprCaseClause } "}" .
ExprCaseClause = ExprSwitchCase ":" StatementList .
ExprSwitchCase = "case" ExpressionList | "default" .

如果switch表达式的值是一个无类型常量,则首先隐式转换为其默认类型;如果是一个无类型布尔值,则首先隐式转换为布尔类型。预先声明的无类型值nil不能作为switch表达式使用。

如果一个case表达式是无类型的,它首先被隐式转换为switch表达式的类型。对于每个(可能转换的)case表达式x和switch表达式的值t,x == t必须是一个有效的比较。

换句话说,switch表达式被当作是用来声明和初始化一个没有显式类型的临时变量t;正是t的那个值与每个case表达式x进行了相等性测试。

在一个case或default子句中,最后一个非空语句可以是一个(可能被标记为)“fallthrough “语句,以表明控制权应该从这个子句的末尾流向下一个子句的第一个语句。否则控制流向 “switch “语句的末尾。“fallthrough “语句可以作为一个表达式switch的所有分句的最后一条语句出现,但最后一个分句除外。

switch表达式之前可以有一个简单的语句,该语句在表达式被评估之前执行。

switch tag {
default: s3()
case 0, 1, 2, 3: s1()
case 4, 5, 6, 7: s2()
}

switch x := f(); {  // missing switch expression means "true"
case x < 0: return -x
default: return x
}

switch {
case x < y: f1()
case x < z: f2()
case x == 4: f3()
}

实现限制。编译器可能不允许多个case表达式求值于同一个常量。例如,目前的编译器不允许在case表达式中使用重复的整数、浮点或字符串常量。

类型switch

类型switch比较的是类型而不是值。在其他方面,它类似于表达式switch。它由一个特殊的switch表达式标记,它具有使用保留字类型而不是实际类型的类型断言形式。

switch x.(type) {
// cases
}

然后,case将实际类型T与表达式x的动态类型进行匹配,与类型断言一样,x必须是接口类型,案例中列出的每个非接口类型T必须实现x的类型,类型switch的case中列出的类型必须全部不同。

TypeSwitchStmt  = "switch" [ SimpleStmt ";" ] TypeSwitchGuard "{" { TypeCaseClause } "}" .
TypeSwitchGuard = [ identifier ":=" ] PrimaryExpr "." "(" "type" ")" .
TypeCaseClause  = TypeSwitchCase ":" StatementList .
TypeSwitchCase  = "case" TypeList | "default" .
TypeList        = Type { "," Type } .

TypeSwitchGuard可以包括一个简短的变量声明。当使用这种形式时,变量被声明在每个子句隐含块的TypeSwitchCase的末尾。在有一个case的子句中,正好列出一个类型,变量具有该类型;否则,变量具有TypeSwitchGuard中表达式的类型。

case可以使用预先声明的标识符nil代替类型;当TypeSwitchGuard中的表达式是一个nil接口值时,就会选择该case。最多可以有一个nil case。

给定一个类型为interface{}的表达式x,下面的类型switch。

switch i := x.(type) {
case nil:
	printString("x is nil")                // type of i is type of x (interface{})
case int:
	printInt(i)                            // type of i is int
case float64:
	printFloat64(i)                        // type of i is float64
case func(int) float64:
	printFunction(i)                       // type of i is func(int) float64
case bool, string:
	printString("type is bool or string")  // type of i is type of x (interface{})
default:
	printString("don't know the type")     // type of i is type of x (interface{})
}

可以重写:

v := x  // x is evaluated exactly once
if v == nil {
	i := v                                 // type of i is type of x (interface{})
	printString("x is nil")
} else if i, isInt := v.(int); isInt {
	printInt(i)                            // type of i is int
} else if i, isFloat64 := v.(float64); isFloat64 {
	printFloat64(i)                        // type of i is float64
} else if i, isFunc := v.(func(int) float64); isFunc {
	printFunction(i)                       // type of i is func(int) float64
} else {
	_, isBool := v.(bool)
	_, isString := v.(string)
	if isBool || isString {
		i := v                         // type of i is type of x (interface{})
		printString("type is bool or string")
	} else {
		i := v                         // type of i is type of x (interface{})
		printString("don't know the type")
	}
}

在类型switch保护之前可以有一个简单的语句,在保护被评估之前执行。

在类型转换中不允许使用 “fallthrough “语句。

Effective Go

https://golang.org/doc/effective_go.html#switch

go的switch比C语言的switch更通用。表达式不需要是常数,甚至不需要是整数,case从上到下进行评估,直到找到匹配的情况,如果switch没有表达式,它就会切换到true。因此,把一个if-else-if-else链写成switch是可能的,也是很习惯的。

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}

没有自动跌破,但 case 可以用逗号分隔的列表呈现。

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

虽然它们在Go中并不像其他一些类似C语言那样常见,但break语句可以用来提前终止一个开关。不过有时候,需要脱离周围的循环,而不是switch,在Go中,可以通过给循环加上一个标签,然后对这个标签进行 “break “来实现。这个例子展示了这两种用法。

Loop:
	for n := 0; n < len(src); n += size {
		switch {
		case src[n] < sizeOne:
			if validateOnly {
				break
			}
			size = 1
			update(src[n])

		case src[n] < sizeTwo:
			if n+1 >= len(src) {
				err = errShortInput
				break Loop
			}
			if validateOnly {
				break
			}
			size = 2
			update(src[n] + src[n+1]<<shift)
		}
	}

当然,continue语句也接受一个可选的标签,但它只适用于循环。

在本节的最后,这里有一个 byte slice 的比较例程,它使用了两个switch语句。

// Compare returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b
func Compare(a, b []byte) int {
    for i := 0; i < len(a) && i < len(b); i++ {
        switch {
        case a[i] > b[i]:
            return 1
        case a[i] < b[i]:
            return -1
        }
    }
    switch {
    case len(a) > len(b):
        return 1
    case len(a) < len(b):
        return -1
    }
    return 0
}

类型switch

也可以使用swtich来发现接口变量的动态类型。这样的类型切换使用了类型断言的语法,括号内有关键字type。如果switch在表达式中声明了一个变量,那么该变量在每个子句中都会有相应的类型。在这种情况下重用名称也是一种习惯,实际上是在每个情况下声明一个新的变量,名称相同,但类型不同。

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

3 - for 语句

Golang 中的 for 语句

go语言实战

for loop

Go 只有一种循环结构 for 循环。

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

和Java的语法相反:

  1. for 后面没有括号(),注意是强制一定不能有
  2. 循环体必须有{}

跟 C 或者 Java 中一样,可以让前置、后置语句为空:

sum := 1
for ; sum < 1000; {
    sum += sum
}

这就非常类似C、Java中的while循环了,因此干脆继续简写,省略掉分括号:

sum := 1
for sum < 1000 {
    sum += sum
}

更绝一点,死循环:

for {
}

golang语言规范

https://golang.org/ref/spec#For_statements

“for “语句指定重复执行块。有三种形式。迭代可以由一个条件(condition)、一个 “for “子句或一个 “range “子句控制。

ForStmt = "for" [ Condition | ForClause | RangeClause ] Block .
Condition = Expression .

单一条件的 for 语句

在最简单的形式中,“for “语句指定重复执行一个块,只要一个布尔条件评估为真。该条件在每次迭代前都会被评估。如果条件不存在,则相当于布尔值为true。

for a < b {
	a *= 2
}

带有for子句的for语句

带有 ForClause 的 “for “语句也是由它的条件(condition)控制的,但除此之外,它还可以指定一个 init 和一个post语句,如赋值、增量或减量语句。init 语句可以是一个简短的变量声明,但post语句不能。init语句声明的变量在每次迭代中都会被重复使用。

ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .
InitStmt = SimpleStmt .
PostStmt = SimpleStmt .
for i := 0; i < 10; i++ {
	f(i)
}

如果非空,则在评估第一次迭代的条件之前,init语句会被执行一次;post语句会在每次执行块之后执行(而且只有在块被执行的情况下)。ForClause 的任何元素都可以是空的,但分号是必须的,除非只有一个条件。如果条件不存在,则相当于布尔值为true。

for cond { S() }    is the same as    for ; cond ; { S() }
for      { S() }    is the same as    for true     { S() }

Effective Go

https://golang.org/doc/effective_go.html#for

Go for循环与C的循环类似-但不一样。它统一了for和while,而且没有do-while。有三种形式,其中只有一种有分号。

// Like a C for (除了不容许用括号)
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

短声明使其很容易在循环中直接声明索引变量。

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

最后,Go没有逗号运算符,++和–是语句而不是表达式。因此,如果你想在for中运行多个变量,你应该使用并行赋值(尽管这排除了++和–)。

// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

4 - for range 语句

Golang 中的 for range 语句

go语言规范

For statements with range clause

https://golang.org/ref/spec#For_statements

带有 “range"子句的 “for” 语句会遍历数组、切片、字符串或map的所有条目,或通道(channel)上接收的值。对于每个条目,如果存在,它将迭代值分配给相应的迭代变量,然后执行该块。

RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .

“range"子句中右边的表达式称为range表达式,它可以是数组、数组指针、分片、字符串、map或允许接收操作的通道。与赋值一样,如果存在,左边的操作数必须是可寻址或映射索引表达式;它们表示迭代变量。如果范围表达式是通道,最多允许一个迭代变量,否则最多可以有两个。如果最后一个迭代变量是空白标识符,那么范围子句就相当于没有该标识符的同一个子句。

范围表达式x在开始循环之前被评估一次,但有一个例外:如果最多存在一个迭代变量,并且len(x)是常数,则不评估范围表达式。

左边的函数调用在每次迭代时都会被评估一次。对于每一次迭代,如果存在各自的迭代变量,就会产生如下的迭代值。

Range expression                          1st value          2nd value

array or slice  a  [n]E, *[n]E, or []E    index    i  int    a[i]       E
string          s  string type            index    i  int    see below  rune
map             m  map[K]V                key      k  K      m[k]       V
channel         c  chan E, <-chan E       element  e  E
  1. 对于数组、数组指针或切片值a,从元素索引0开始,按递增顺序产生索引迭代值。如果最多存在一个迭代变量,则 range 循环产生从0到 len(a)-1 的迭代值,并且不对数组或分片本身进行索引。对于nil slice,迭代次数为0. 对于字符串值,“range循环"会产生从 0 到 len(a)-1 的迭代值,并且不对数组或分片本身进行索引。

  2. 对于字符串值,“range"子句从字节索引 0 开始对字符串中的Unicode code point 进行迭代。在连续迭代时,索引值将是字符串中连续的UTF-8编码 code point 的第一个字节的索引,第二个值,类型为 rune,将是对应code point的值。如果迭代遇到无效的UTF-8序列,第二个值将是0xFFFD,即Unicode replacement(替换)字符,下一次迭代将推进字符串中的一个字节。

  3. 对map的迭代顺序没有指定,也不能保证每次迭代的顺序相同。如果在迭代过程中删除了一个尚未到达的map条目,将不会产生相应的迭代值。如果在迭代过程中创建了一个map条目,该条目可能在迭代过程中产生,也可能被跳过。对于每个创建的条目,以及从一个迭代到下一个迭代,选择可能不同。如果map为nil,则迭代次数为0。

  4. 对于通道,产生的迭代值是通道上连续发送的值,直到通道关闭。如果通道为nil,则range表达式永远阻塞。

迭代值像赋值语句一样被赋值到相应的迭代变量中。

迭代变量可以由 “range “子句使用短变量声明的形式(:=)来声明。在这种情况下,它们的类型被设置为各自迭代值的类型,它们的作用域是 “for"语句的块;它们在每次迭代中被重复使用。如果迭代变量是在 “for"语句之外声明的,那么在执行后它们的值将是最后一次迭代的值。

var testdata *struct {
	a *[7]int
}
for i, _ := range testdata.a {
	// testdata.a is never evaluated; len(testdata.a) is constant
	// i ranges from 0 to 6
	f(i)
}

var a [10]string
for i, s := range a {
	// type of i is int
	// type of s is string
	// s == a[i]
	g(i, s)
}

var key string
var val interface{}  // element type of m is assignable to val
m := map[string]int{"mon":0, "tue":1, "wed":2, "thu":3, "fri":4, "sat":5, "sun":6}
for key, val = range m {
	h(key, val)
}
// key == last map key encountered in iteration
// val == map[key]

var ch chan Work = producer()
for w := range ch {
	doWork(w)
}

// empty a channel
for range ch {}

Effective Go

https://golang.org/doc/effective_go.html#for

如果你正在循环数组、切片、字符串或map,或者从一个通道读取,range子句可以管理循环。

for key, value := range oldMap {
    newMap[key] = value
}

如果你只需要range内的第一项(键或索引),就放弃第二项。

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

如果你只需要范围中的第二项(值),使用空白的标识符(下划线)来丢弃第一项。

sum := 0
for _, value := range array {
    sum += value
}

对于字符串,range 为你做了更多的工作,通过解析UTF-8分解出各个Unicode code point。错误的编码会消耗一个字节,并产生替换符U+FFFD。(名称(与相关的内置类型) rune是单个Unicode code point的go术语。详情请参见语言规范。) 。循环

for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
    fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}

打印:

character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7

参考资料

5 - range 语句

Golang 中的 range 语句

rang遍历

range 关键字用来遍历 listarray 或者 map。为了方便理解,可以认为 range 等效于 for earch index of

对于 arrays 或者 slices, 将会返回整型的下标;

// 对于数组,rang返回index
a := [...]string{"a", "b", "c", "d"}
for i := range a {
    fmt.Println("Array item", i, "is", a[i])
}

支持返回单值或者两个值, 如果返回一个值,那么为下标,否则为下标和下标所对应的值。

a := [...]string{"a", "b", "c", "d"}
for i, v := range a {
    fmt.Println("Array item", i, "is", v)
}

对于 map,将会返回下一个键值对的 key

// 对于map, range 返回 key 
capitals := map[string] string {"France":"Paris", "Italy":"Rome", "Japan":"Tokyo" }
for key := range capitals {
    fmt.Println("Map item: Capital of", key, "is", capitals[key])
}

同样支持返回两个值, 直接拿到key和对应的value:

capitals := map[string] string {"France":"Paris", "Italy":"Rome", "Japan":"Tokyo" }
for key, value := range capitals {
    fmt.Println("Map item: Capital of", key, "is", value)
}

6 - go 语句

Golang 中的 go 语句

golang语言规范

https://golang.org/ref/spec#Go_statements

“go"语句在同一地址空间内,以独立的并发控制线程或goroutine的形式开始执行一个函数调用。

GoStmt = "go" Expression .

表达式必须是函数或方法调用,不能用括号。内建函数的调用与表达式语句一样受到限制。

函数值和参数在调用goroutine中像往常一样被评估,但与常规调用不同的是,程序的执行不会等待被调用的函数完成,而是在新的goroutine中开始独立执行。相反,函数在新的goroutine中开始独立执行。当函数终止时,它的goroutine也会终止。如果函数有任何返回值,则在函数完成时将其丢弃。

go Server()
go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)

7 - select 语句

Golang 中的 select 语句

go语言实战

select是Golang中的控制语句,语法类似于switch语句。但是select只用于通信,要求每个case必须是IO操作。

不带default语句的select会阻塞直到某个case满足:

ch1 := make (chan int, 1)
ch2 := make (chan int, 1)

select {
case <-ch1:
    fmt.Println("ch1 pop one element")
case <-ch2:
    fmt.Println("ch2 pop one element")
}

如果两个case同时满足,则随机执行某个case语句,其他case语句不会执行。

如果不想阻塞,则可以带上default子语句:

select {
case <-ch1:
    fmt.Println("ch1 pop one element")
case <-ch2:
    fmt.Println("ch2 pop one element")
default:
    fmt.Println("not ready yet")
}

如果两个case条件都不满足,则直接跳到 default 流程而不阻塞。

golang语言规范

https://golang.org/ref/spec#Select_statements

“select"语句选择一组可能的发送或接收操作中的某一个进行。它看起来类似于 “switch"语句,但情况都是指通信操作(指IO操作)。

SelectStmt = "select" "{" { CommClause } "}" .
CommClause = CommCase ":" StatementList .
CommCase   = "case" ( SendStmt | RecvStmt ) | "default" .
RecvStmt   = [ ExpressionList "=" | IdentifierList ":=" ] RecvExpr .
RecvExpr   = Expression .

带有RecvStmt的情况下,可以将RecvExpr的结果分配给一个或两个变量,这些变量可以使用短变量声明。RecvExpr必须是一个(可能是括号)接收操作。最多只能有一个default缺省情况,它可以出现在case列表中的任何地方。

“select “语句的执行分几个步骤进行:

  1. 对于语句中的所有case,在进入 “select “语句后,接收操作的通道操作数和发送语句的通道和右侧表达式都会按照源代码顺序精确地评估一次。其结果是一组可以接收或发送的通道,以及相应的发送值。无论选择哪种(如果有的话)通信操作进行,该评估中的任何副作用都会发生。RecvStmt左侧带有短变量声明或赋值的表达式尚未被评估。

  2. 如果有一个或多个通信可以进行,则通过统一的伪随机选择选择一个可以进行的单一通信。否则,如果有默认case,则选择该case。如果没有default case,“select “语句就会阻塞,直到至少有一个通信可以继续进行。

  3. 除非选择的case是默认情况,否则会执行相应的通信操作。

  4. 如果选择的case是一个带有短变量声明或赋值的RecvStmt,则左手边的表达式被评估,接收到的值(或数值)被赋值。

  5. 所选case的语句列表被执行。

由于在nul通道上的通信永远无法进行,所以只有nil通道而没有 default case 的 select 永远阻塞。

var a []int
var c, c1, c2, c3, c4 chan int
var i1, i2 int
select {
case i1 = <-c1:
	print("received ", i1, " from c1\n")
case c2 <- i2:
	print("sent ", i2, " to c2\n")
case i3, ok := (<-c3):  // same as: i3, ok := <-c3
	if ok {
		print("received ", i3, " from c3\n")
	} else {
		print("c3 is closed\n")
	}
case a[f()] = <-c4:
	// same as:
	// case t := <-c4
	//	a[f()] = t
default:
	print("no communication\n")
}

for {  // send random sequence of bits to c
	select {
	case c <- 0:  // note: no statement, no fallthrough, no folding of cases
	case c <- 1:
	}
}

select {}  // block forever

8 - return 语句

Golang 中的 return 语句

golang语言规范

https://golang.org/ref/spec#Return_statements

函数F中的 “return"语句会终止F的执行,并可选地提供一个或多个结果值。在F返回给调用者之前,任何被F deferred 的函数都会被执行。

ReturnStmt = "return" [ ExpressionList ] .

在没有结果类型的函数中,“return"语句不能指定任何结果值。

func noResult() {
	return
}

有三种方法可以从一个具有结果类型的函数中返回值:

  1. 返回值可以在 “return"语句中明确列出。每个表达式必须是单值的,并且可以分配给函数结果类型的相应元素。

    func simpleF() int {
    	return 2
    }
    
    func complexF1() (re float64, im float64) {
    	return -7.0, -4.0
    }
    
  2. “return"语句中的表达式列表可以是对一个多值函数的单次调用。其效果就好比该函数返回的每个值都被分配到一个临时变量中,其类型为相应的值,然后用 “return"语句列出这些变量,这时就适用前一种情况的规则。

    func complexF2() (re float64, im float64) {
    	return complexF1()
    }
    
  3. 如果函数的结果类型为其结果参数指定了名称,那么表达式列表可以为空。结果参数作为普通的局部变量,函数可以根据需要为它们赋值。return 语句返回这些变量的值。

    func complexF3() (re float64, im float64) {
    	re = 7.0
    	im = 4.0
    	return
    }
    
    func (devnull) Write(p []byte) (n int, _ error) {
    	n = len(p)
    	return
    }
    

无论如何声明,所有的结果值在进入函数时都初始化为其类型的零值。指定结果的 “return” 语句会在执行任何deferred 函数之前设置结果参数。

实现限制:如果在返回的地方有不同的实体(常量、类型或变量)与结果参数同名,编译器可以不允许在 “return” 语句中使用空表达式列表。

9 - break 语句

Golang 中的 break 语句

golang语言规范

https://golang.org/ref/spec#Break_statements

“break “语句终止了同一函数中最内层的 “for”、“switch “或 “select “语句的执行。

BreakStmt = "break" [ Label ] .

如果有标签,则标签必须包围住 “for”、“switch “或 “select “语句,而且标签是执行终止。

10 - continue 语句

Golang 中的 continue 语句

golang语言规范

https://golang.org/ref/spec#Continue_statements

“continue"语句在其post语句处开始最里面的 “for"循环的下一次迭代。“for"循环必须在同一个函数内。

ContinueStmt = "continue" [ Label ] .

如果有标签,则标签必须包含 “for” 语句。

RowLoop:
	for y, row := range rows {
		for x, data := range row {
			if data == endOfRow {
				continue RowLoop
			}
			row[x] = data + bias(x, y)
		}
	}

11 - goto 语句

Golang 中的 goto 语句

golang语言规范

“goto” 语句将控制权转移到同一函数中带有相应标签的语句。

GotoStmt = "goto" Label .
goto Error

执行 “goto"语句不能导致在goto之后有任何变量进入它还没有进行的范围。例如,这个例子:

	goto L  // BAD
	v := 3
L:

是错误的,因为跳转到标签L时跳过了v的创建。

块外的 “goto"语句不能跳转到该块内的标签。例如,这个例子:

if n%2 == 1 {
	goto L1
}
for n > 0 {
	f()
	n--
L1:
	f()
	n--
}

是错误的,因为标签L1在 “for"语句的块内,而goto却不在。

12 - fallthrough 语句

Golang 中的 fallthrough 语句

golang语言规范

https://golang.org/ref/spec#Fallthrough_statements

“fallthrough"语句将控制权转移到 “switch” 语句中下一个case子句的第一条语句。它只能作为这种子句中的最后一个非空语句使用。

参考资料

  • go语言fallthrough的用法心得
    • Go里面 switch 默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码。
    • fallthrough不能用在switch的最后一个分支
    • fallthrough到下一个case块时,不执行case匹配检查!不执行case匹配检查!不执行case匹配检查!

特别注意最后一条,有点和常识不符合(我原本理解的fallthrough只是取消break,然后继续做下一个case的匹配,但实际fallthrough把case匹配检查也取消了):

switch {
case true:
   fmt.Println("The integer was <= 5")
   fallthrough
case false:
   fmt.Println("The integer was <= 6")
   fallthrough
default:
   fmt.Println("default case")
}

打印结果:

The integer was <= 5
The integer was <= 6
default case

13 - defer 语句

Golang 中的 defer 语句

go语言实战

defer 语句会延迟函数的执行直到外层函数返回,通常用于执行清理操作。

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

其他使用场景,如释放mutex:

mu.Lock()
defer mu.Unlock()

打印footer:

printHeader()
defer printFooter()
printContent1()
printContent2()

使用事项:

  1. 调用所需的参数会立刻评估

    func a() {
        i := 0
        defer fmt.Println(i)
        i++
        return
    }
    

    这里会打印0,因为 defer fmt.Println(i) 执行时i为0,参数在此时确定,后面的改动不会影响defer语句的参数。

  2. 多个defer调用会入栈,后进先出

    func b() {
        for i := 0; i < 4; i++ {
            defer fmt.Print(i)
        }
    }
    

    这里会打印3210,顺序和defer语句的顺序相反。

  3. defer语句有机会修改函数返回值

    func c() (i int) {
        defer func() { i++ }()
        return 1
    }
    

    这里的函数返回值会被defer修改,从而返回2。

golang语言规范

“defer"语句调用函数,该函数的执行被推迟到外围函数返回的那一刻,这可能是因为外围函数执行了一个返回语句,到达了其函数体的终点,或者是因为相应的goroutine发生panic/恐慌。

DeferStmt = "defer" Expression .

表达式必须是函数或方法调用,不能用括号。内置函数的调用与表达式语句一样受到限制。

每次执行 “defer"语句时,函数值和调用的参数都会像往常一样被评估并重新保存,但实际函数不会被调用。相反,deferred的函数在外围函数return之前立即被调用,其顺序与defer的顺序相反。也就是说,如果外围函数通过一个显式return语句返回,则在该return语句设置了任何结果参数之后,但在函数return给调用者之前,defer函数会被执行。如果一个defer函数的值评价为nil,则在函数被调用时,而不是在 “defer"语句被执行时,执行就会panic/慌乱。

例如,如果defer函数是一个函数字面量,而外围的函数有命名的结果参数,这些结果参数在字面量的范围内,那么defer函数可以在结果参数被返回之前访问和修改它们。如果defer函数有任何返回值,那么当函数完成时,它们将被丢弃。(也请参见关于处理恐慌的章节。)

lock(l)
defer unlock(l)  // unlocking happens before surrounding function returns

// prints 3 2 1 0 before surrounding function returns
for i := 0; i <= 3; i++ {
	defer fmt.Print(i)
}

// f returns 42
func f() (result int) {
	defer func() {
		// result is accessed after it was set to 6 by the return statement
		result *= 7
	}()
	return 6
}

Effective Go

https://golang.org/doc/effective_go.html#defer

Go 的 defer 语句安排在执行 defer 的函数返回之前立即运行函数调用(defer函数)。这是一种不寻常但有效的方法,用于处理一些情况,例如无论函数走哪条路径返回,都必须释放资源。规范的例子是解锁mutex或关闭文件。

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}

defer对Close这样的函数的调用有两个好处。首先,它保证你永远不会忘记关闭文件,如果你后来编辑函数添加了新的返回路径,就很容易犯这个错误。第二,它意味着close坐在open的附近,这比把它放在函数的最后要清楚得多。

defer函数的参数(如果函数是方法,则包括接收方)是在defer执行时评估的,而不是在调用执行时评估的。除了避免担心变量在函数执行时改变值之外,这意味着单个defer调用点可以defer多个函数的执行。下面是一个例子:

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

defer函数是按照 LIFO 顺序执行的,所以这段代码会在函数返回时导致 4 3 2 1 0 被打印出来。更有价值的例子是通过程序跟踪函数执行的简单方法。我们可以写几个简单的跟踪例程,比如这样:

func trace(s string)   { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something....
}

我们可以更好地利用 defer 函数的参数在 defer 执行时被评估这一事实。追踪例程可以设置未追踪例程的参数。这个例子:

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

打印:

entering: b
in b
entering: a
in a
leaving: a
leaving: b

对于习惯了其他语言的块级资源管理的程序员来说,defer可能看起来很奇特,但它最有趣和最强大的应用恰恰来自于它不是基于块而是基于函数。

参考资料