深入Go语言 - 7

语句statement

目录 [−]

  1. 空语句
  2. 标签语句
  3. 表达式语句
  4. send语句
  5. 自增和自减语句
  6. 赋值语句
  7. if 语句
  8. switch 语句
  9. for 语句
  10. select 语句
  11. return 语句
  12. break 语句
  13. continue 语句
  14. goto语句
  15. defer 语句
  16. go 语句

本章介绍Go语言的各种语句。

Go语言的语法定义罗列了所有的语句类型:

Statement =
    Declaration | LabeledStmt | SimpleStmt |
    GoStmt | ReturnStmt | BreakStmt | ContinueStmt | GotoStmt |
    FallthroughStmt | Block | IfStmt | SwitchStmt | SelectStmt | ForStmt |
    DeferStmt .

SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | ShortVarDecl .

语句控制程序逻辑的运行。终止语句(terminating statement)代表结束当前程序逻辑单元的运行。
1、 A "return" or "goto" statement.
2、A call to the built-in function panic.
3、A block in which the statement list ends in a terminating statement.
4、 An "if" statement in which:

  • the "else" branch is present, and
  • both branches are terminating statements.

5、A "for" statement in which:

  • there are no "break" statements referring to the "for" statement, and
  • the loop condition is absent.

6、A "switch" statement in which:

  • there are no "break" statements referring to the "switch" statement,
  • there is a default case, and
  • the statement lists in each case, including the default, end in a terminating statement, or a possibly labeled "fallthrough" statement.

7、A "select" statement in which:

  • there are no "break" statements referring to the "select" statement, and
  • the statement lists in each case, including the default if present, end in a terminating statement.

8、A labeled statement labeling a terminating statement.

其它的语句都不会终止程序单元的执行。

参考

空语句

空语句不错任何事

标签语句

增加一个标签, 为 goto、break、continue 使用

1
Error: log.Panic("error encountered")

表达式语句

除了特别的内置函数, 一般函数和方法调用、receive 操作都可以出现在语句的上下文中,
这样的语句可以用括号括起来。

下面的内置的函数不能在语句中:

append cap complex imag len make new real
unsafe.Alignof unsafe.Offsetof unsafe.Sizeof

下面的操作都可以作为语句:

1
2
3
4
h(x+y)
f.Close()
<-ch
(<-ch)

但是下面的内置函数调用就不可以:

1
len("foo") //错误

send语句

可以通过send的语句往channel发送一个值。

自增和自减语句

注意,Go语言中"++"和"--"是语句(statement),而不是表达式(expression),并且只能放在操作数的后面(postfix)。操作数必须是可寻址的或者是map索引表达式。

x++ 等价于 x += 1x-- 等价于 x -= 1

因为它们不是表达式,所以不能以表达式的方式给其它变量赋值,下面的代码是错误的:

1
2
3
4
5
6
7
var m = map[string]int{"1": 1, "2": 2}
m["1"]++
i := 0
i++
var j = i++ //错误

赋值语句

赋值语句左边的操作数必须是可寻址的、或者是map索引表达式、抑或是空标识符_。

操作数可以用括号括起来,不管是左边的操作数还是右边的操作数。

1
2
3
4
5
6
var i, j int
(i), (j) = (3), 4
var k int
(k) = i
_, _, _ = i, j, k

复合的赋值操作 x op= y等价于x = x op (y),但是x只会被计算一次。运算符的左右两边只能是单值的表达式,而且左边的操作符不能是空标识符。

前面也有代码举例,Go支持多值赋值(tuple),左边的操作数的数量必须和右边的值的数量相同。

赋值过程分为两阶段:

  1. 计算左边的索引表达式的操作数和指针, 右边的表达式
  2. 从左到右赋值

下面是赋值语句的一些例子,你可以琢磨琢磨。

a, b = b, a  // exchange a and b

x := []int{1, 2, 3}
i := 0
i, x[i] = 1, 2  // set i = 1, x[0] = 2

i = 0
x[i], i = 2, 1  // set x[0] = 2, i = 1

x[0], x[0] = 1, 2  // set x[0] = 1, then x[0] = 2 (so x[0] == 2 at end)

x[1], x[3] = 4, 5  // set x[1] = 4, then panic setting x[3] = 5.

type Point struct { x, y int }
var p *Point
x[2], p.x = 6, 7  // set x[2] = 6, then panic setting p.x = 7

i = 2
x = []int{3, 5, 7}
for i, x[i] = range x {  // set i, x[2] = 0, x[0]
    break
}
// after this loop, i == 0 and x == []int{3, 5, 3}

if 语句

if语句类似其它语言中比如Java、C的if语句,但是也有不同:
1、if语句中的条件表达式可以有简单的语句:

1
2
3
4
5
6
7
if x := f(); x < y {
return x
} else if x > z {
return z
} else {
return y
}

2、代码块必须以大括号括起来,即使是单行代码块不能省略大括号

1
2
if x > max
x = max //错误

3、if 中的条件表达式可以省略括号,而且一般都省略括号

switch 语句

switch提供了多路处理的机制。switch有两种:表达式switch和type switch。 类型switch在前一章中已经介绍了,本节只介绍表达式switch。

表达式switch(以下直接叫switch)中的表达式会被计算,然后和case clause进行比较。case clause中的表达式不一定是常数。表达式的计算都是从左到右,从上而下的计算的。

如果遇到第一个满足的case,其它的case就不会执行,如果没有满足条件的case,并且有一个default的case的话,会执行default代码块,相当于default用来兜底。最多有一个default,而且一般都将它放在底部,尽管它的位置和其它case cluase没有顺序限制。

在case表达式中,对于未声明类型的bool常量,它首先会被转换成bool类型,对于其它常量,会被转换成缺省类型进行比较。

编译器可能不允许有重复的 case 常量 clause,但是非常量的表达式可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "fmt"
func main() {
i := 4
switch i {
case 1:
fmt.Println(1)
case 2:
fmt.Println(2)
case f():
fmt.Println("f")
case 4:
fmt.Println(4)
default:
fmt.Println("unknown")
}
}
func f() int {
return 4
}

fallthrough语句可以是case语句或者default的代码块的最后一个语句,只要当前的case语句或者default语句不是最后一个case语句。 但是fallthrough不能出现在代码块的中间(非最后一个语句)。

fallthrough将直接跳到下一个case clause的代码块的第一个语句中,不会和那个case 表达式再进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
i := 1
switch i {
case 1:
fmt.Println(1)
fallthrough
//fmt.Println("after fallthrough") //错误
case 2:
fmt.Println(2)
case 3:
fmt.Println(3)
default:
fmt.Println("unknown")
}

上面的代码输出 1 和 2。

switch 表达式的前面可以有简单的表达式,它在switch表达式计算之前执行。

1
2
3
switch x := f(); x>0 {
case true:
}

switch表达式也可以省略,省略的时候意味着表达式为 true,所以会进入case 表达式为true的代码块:

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

switch表达式也一般省略括号。

for 语句

for 语句也不用括号,而且在三段式的for语句中,加上括号反而报错。

Go扩展了其它语言中的for语句饿功能,它有以下的形式:

1、普通的三段式的for语句 for init; condition; post {……}

1
2
3
for i := 0; i < 10; i++ {
fmt.Println(i)
}

其中的三段中的任意部分都可以省略,但是分号不能省略。

2、只包含条件表达式的for语句 for condition {}
Go语言中没有while语句,所以for的这种形式类似while语句。

1
2
3
4
i := 0
for i < 10 {
i++
}

3、for ... range语句
对于数组、slice、字符串、map对象以及 从channel中接收的值,可以使用这种语句进行遍历。

1
2
3
4
5
var s = []int{1, 2, 3, 4, 5}
for i, v := range s {
fmt.Printf("index: %d, value: %d\n", i, v)
}

对于不同的类型, range迭代的结果可能是一个值,也可能是两个值。具体如下:

Range 表达式 第一个值 第二个值
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  

4、 无任何条件和语句的 for

1
2
3
4
5
6
7
i := 10
for {
i++
if i > 20 {
break
}
}
  • 对于数组,数组指针、slice,索引从0开始,止于 len(a) -1
  • 对于字符串, range将迭代unicode code point,而不是字节byte, 因此索引值为当前unicode字符的起始位置,值为rune。如果不是unicode字符,第二个值为0xFFFD
  • 对于map的迭代每次迭代可能不同。入股迭代的过程中还没有被访问的对象被移除了,后续的迭代中不会出现这些删除的值。如果迭代的过程中往map中增加值,则增加的值可能出现后续的迭代中,也可能不出现。
  • 对于channel,迭代一直会执行,直到channel被关闭。如果channel为nil,则range表达式永远被阻塞,一定要注意。

select 语句

select语句从一组send操作和receive操作中选择一个执行。

它类似switch但是只用来对channel进行操作。

  • channel操作数如果是一个表达式,那么表达式只会被计算一次
  • 如果有多个case可以被执行,只有一个case会被选择执行。选择算法是伪随机算法。如果没有case可以执行,并且有一个default case,则这个default会被选择执行, 如果没有default, select会被阻塞直到有一个case可以被执行。

select一次只有一个case会执行,所以很多情况下我们把它放入到一个 for循环中。

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
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

select 语句也经常加入超时的case:

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
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
//执行超时case
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
//执行C2
c2 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c2 <- "result 2"
}()
select {
case res := <-c2:
fmt.Println(res)
case <-time.After(time.Second * 3):
fmt.Println("timeout 2")
}

return 语句

return语句从函数中返回。

如果函数没有返回类型, return语句不能返回任何值。

如果函数有返回类型,那么有几种情况
1、返回值可以显示地写在return的后面,每个表达式必须是单值的

1
2
3
4
5
6
7
func simpleF() int {
return 2
}
func complexF1() (re float64, im float64) {
return -7.0, -4.0
}

2、return语句中的表达式列表可以是对返回多只函数的调用

1
2
3
func complexF2() (re float64, im float64) {
return complexF1()
}

3、如果函数的返回类型参数指定了名字,则return 可以返回空。这些返回类型参数就像本地变量一样。

1
2
3
4
5
6
7
8
9
10
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
}

所有的结果值在函数的开始时都被初始化为它们的零值。

编译器可能不允许下面的scope中的return返回空:

1
2
3
4
5
6
func f(n int) (res int, err error) {
if _, err := f(n-1); err != nil {
return // invalid return statement: err is shadowed
}
return
}

break 语句

break用来终止执行最内层的 for、 switch 或者 select语句。

如果break后面跟着一个标签,则标签应该紧贴着要终止的for、switch或者selector语句,下面的代码中 break label2就是错的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
label2:
i := 10
label1:
for {
i++
if i > 20 {
break label1
}
fmt.Println(i)
if i < 10 {
break label2
}
}

为什么要用标签呢?因为不带标签的break只能终止最内层(innermost)的循环,如果像终止外部的循环,就得用标签了:

1
2
3
4
5
6
7
8
label1:
for {
fmt.Println("hello")
for {
fmt.Println("world")
break label1
}
}

continue 语句

continue语句开始最内层的 for循环的下一次迭代,终止本次迭代。

同样,如果后面跟着标签,将开始外层的标签指定的for循环的下一次迭代。

goto语句

goto跳转太强大,在很多语言中虽然都提供但是不推荐使用,Go也一样。

下面的goto是不允许的,因为标签L跳过了变量v等声明和赋值,如果后面的代码访问v会有问题。

1
2
3
goto L // BAD
v := 3
L:

代码块外部的goto不能跳到一个代码块内部的标签上:

1
2
3
4
5
6
7
8
9
10
if n%2 == 1 {
goto L1
}
for n > 0 {
f()
n--
L1:
f()
n--
}

defer 语句

defer语句调用一个函数,这个函数将会在当前函数返回的时候才被调用,货站当前函数执行了一个return语句,或者发生panic。

无论是否有panic, defer函数都会调用。

函数可以是普通函数或者方法。调用内置函数有限制,限制条件和上面的表达式语句一样。

当defer语句执行时,函数的参数就会被计算一次,但是函数还没有被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
i := 0
defer func(i int) {
fmt.Printf("defer: %d\n", i)
}(i)
i = 50
defer func(i int) {
fmt.Printf("defer: %d\n", i) //50
}(i)
i = 100

当一个函数中有多个defer语句时,最后的defer语句中函数最先被执行,也就是defer函数的执行是和它的定义成反序。比如上面的代码先输出50,再输出0.

defer语句中函数也可以有返回值,但是返回值会被忽略。

defer语句中如果定义了一个函数字面量(匿名函数),而且函数中对外部的函数返回参数有所修改的话,修改结果会生效,比如下面的结果,虽然函数最后一个语句返回0,但是defer函数又修改了result的结果,所以f的最终结果为1。

1
2
3
4
5
6
7
// f 返回 1
func f() (result int) {
defer func() {
result++
}()
return 0
}

defer语句虽然挺有用处,但是也需要注意它的函数会被推迟到函数返回的时候才会执行。如果defer函数中有对共享资源的占用的释放,比如锁、文件、连接等,等到函数返回才释放会影响程序的性能,所以对资源能今早释放则今早释放,未必非得放在defer函数中释放。

go 语句

go语句将一个函数调用在一个新的goroutine中独立执行。

goroutine是值得仔细介绍的内容,而且goroutine 调度也非常的有趣,这会在下一章中独立介绍。