Go语言基础之Switch控制结构

Switch语句简介

Switch语句的优点在于:在执行一些多分支代码的场景下,switch分支控制结构能够使得代码更简洁,可读性更好,相应的维护性也会得到提升。

下面先用if分支控制结构举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
func personJob(p string) {
if p == "student" {
println("student's task is study")
} else if p == "worker" || p == "civil servant" || p == "server" {
println("Serving for people are their's task")
} else if p == "Boss" || p == "Leader" {
println("Serving for stuff are their's task")
} else if p == "teacher" {
println("Teaching children are their's task")
} else {
println("uhh...")
}
}

用switch改写上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func personJob(p string) {
switch p {
case "student":
println("student's task is study")
case "worker", "civil servant", "server":
println("Serving for people are their's task")
case "Boss", "Leader":
println("Serving for stuff are their's task")
case "teacher":
println("Teaching children are their's task")
default:
println("uhh...")
}
}

personJob("civil servant")

从代码呈现角度上看,switch多分支控制结构确实要比if分支控制结构要更见简洁美观。

接下来,分析下switch的一般形式:

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

switch initStmt; expr {
case expr1:
// 执行分支1
case expr2:
// 执行分支2
case expr3_1, expr3_2, expr3_3:
// 执行分支3
case expr4:
// 执行分支4
... ...
case exprN:
// 执行分支N
default:
// 执行默认分支
}

1.switch控制结构第一行由switch关键字开始,后面接着可选组成部分initStmt(用于短变量申明switch语句块的临时变量)。expr是一个表达式,通常是在switch后接上expr,除了短变量申明的场景;

2.switch后面的大括号内部是一个个的代码执行分支,每个分支从case关键字开始,case后接的是表达式或逗号分隔的表达式列表;

3.此外,switch还有一个特殊分支:default,与位置无关,当前面的分支都不匹配的时候才会执行这个默认分支

4.Switch分支控制结构是按照从上到下,从左到右的逻辑顺序执行的。

Switch语句的灵活性

相比于早期的C语言,Go语言的Switch语句具有更强的灵活性。

表达式支持类型广泛

1.switch语句表达式的求职结果支持各种类型(只要类型支持比较操作即可);

这里以自定义结构体为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type person struct {
name string
age int
job string
}

func test() {
p := person{"Jabari", 20, "student"}
switch p {
case person{"Hakim", 20, "student"}:
println("match Hakim")
case person{"Jeff", 20, "Boss"}:
println("match Jeff")
case person{"HJ", 21, "civil servant"}:
println("match HJ")
default:
println("nobody match")
}
}

test()

下面说一个特殊情况,当switch表达式类型为布尔类型时,如果求值结果始终为true,我们就可以省略switch 后面的表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

// 带有initStmt语句的switch语句
switch initStmt; {
case bool_expr1:
case bool_expr2:
... ...
}

// 没有initStmt语句的switch语句
switch {
case bool_expr1:
case bool_expr2:
... ...
}

不过,这里要注意,在带有 initStmt 的情况下,如果我们省略 switch 表达式,那么 initStmt 后面的分号不能省略,因为 initStmt 是一个语句。

支持临时变量声明

switch 语句的 initStmt 可用来声明只在这个 switch 隐式代码块中使用的变量,这种就近声明的变量最大程度地缩小了变量的作用域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func personJob(p string) {
switch a := p; a {
case "student":
println("student's task is study")
case "worker", "civil servant", "server":
println("Serving for people are their's task")
case "Boss", "Leader":
println("Serving for stuff are their's task")
case "teacher":
println("Teaching children are their's task")
default:
println("uhh...")
}
}

personJob("civil servant")

支持列表表达式

在C语言中,如果要使得多个case分支执行相同逻辑,那么我们只能通过下面方式实现:

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

void job_similarity(string job) {
switch(job) {
case "servant":
case "civil servant":
case "policeman":
case "leader":
printf("");
break;
case "student":
case "teacher":
printf("");
break;
default:
printf("");
}
}

在上面这段 C 代码中,前四个case 匹配成功后,执行的都是 case 4中的代码逻辑,case 5~case 6匹配成功后,执行的都是 case6 中的代码逻辑。

下面看看Go的实现方式:

1
2
3
4
5
6
7
8
9
10
11

func job_similarity(job int) {
switch job {
case "servant", "civil servant", "policeman", "leader":
println("")
case "student", "teacher":
println("")
default:
println("")
}
}

可以看到Go的switch语句处理的更加简洁、清晰。

仅执行匹配的 case分支

Go的switch语句取消了默认执行下一个 case 代码逻辑的语义。

Go 语言中的 Swith 语句修复了 C 语言的这个缺陷,取消了默认执行下一个 case 代码逻辑的“非常规”语义,每个 case 对应的分支代码执行完后就结束 switch 语句。

在少数场景下,如果需要执行下一个case的分支,可以显式使用 Go 提供的关键字 fallthrough 来实现,下面举个例子:

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

func case1() int {
println("eval case1 expr")
return 1
}

func case2() int {
println("eval case2 expr")
return 2
}

func switchexpr() int {
println("eval switch expr")
return 1
}

func main() {
switch switchexpr() {
case case1():
println("exec case1")
fallthrough
case case2():
println("exec case2")
fallthrough
default:
println("exec default")
}
}
1
2
3
4
5
eval switch expr
eval case1 expr
exec case1
exec case2
exec default

我们看到,switch expr 的求值结果与 case1 匹配成功,Go 执行了 case1 对应的代码分支。而且,由于 case1 代码分支中显式使用了 fallthrough,执行完 case1 后,代码执行流并没有离开 switch 语句,而是继续执行下一个 case,也就是 case2 的代码分支。

这里有一个注意点,由于 fallthrough 的存在,Go 不会对 case2 的表达式做求值操作,而会直接执行 case2 对应的代码分支。而且,在这里 case2 中的代码分支也显式使用了 fallthrough,于是最后一个代码分支,也就是 default 分支对应的代码也被执行了。

另外,还有一点要注意的是,如果某个 case 语句已经是 switch 语句中的最后一个 case 了,并且它的后面也没有 default 分支了,那么这个 case 中就不能再使用 fallthrough,否则编译器就会报错。

type switch

Go 语言的 switch 语句还支持求值结果为类型信息的表达式,也就是 type switch 语句。我们来看个例子:

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

func main() {
var x interface{} = 13
switch x.(type) {
case nil:
println("x is nil")
case int:
println("the type of x is int")
case string:
println("the type of x is string")
case bool:
println("the type of x is string")
default:
println("don't support the type")
}
}

>> the type of x is int

switch 关键字后面跟着的表达式为x.(type),这种表达式形式是 switch 语句专有的,而且也只能在 switch 语句中使用。这个表达式中的 x 必须是一个接口类型变量,表达式的求值结果是这个接口类型变量对应的动态类型。

以上面的代码var x interface{} = 13为例,x 是一个接口类型变量,它的静态类型为interface{},如果我们将整型值 13 赋值给 x,x 这个接口变量的动态类型就为 int。

着,case 关键字后面接的就不是普通意义上的表达式了,而是一个个具体的类型。这样,Go 就能使用变量 x 的动态类型与各个 case 中的类型进行匹配,之后的逻辑就都是一样的了。

通过x.(type),我们除了可以获得变量 x 的动态类型信息之外,也能获得其动态类型对应的值信息,现在我们把上面的例子改造一下:

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

func main() {
var x interface{} = 13
switch v := x.(type) {
case nil:
println("v is nil")
case int:
println("the type of v is int, v =", v)
case string:
println("the type of v is string, v =", v)
case bool:
println("the type of v is bool, v =", v)
default:
println("don't support the type")
}
}

>> the type of v is int, v = 13

这里我们将 switch 后面的表达式由x.(type)换成了v := x.(type)。对于后者,你千万不要认为变量 v 存储的是类型信息,其实 v 存储的是变量 x 的动态类型对应的值信息,这样我们在接下来的 case 执行路径中就可以使用变量 v 中的值信息了。

另外,在前面的 type switch 演示示例中,我们一直使用 interface{}这种接口类型的变量,Go 中所有类型都实现了 interface{}类型,所以 case 后面可以是任意类型信息。

但如果在 switch 后面使用了某个特定的接口类型 I,那么 case 后面就只能使用实现了接口类型 I 的类型了,否则 Go 编译器会报错。可以看看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

type I interface {
M()
}

type T struct {
}

func (T) M() {
}

func main() {
var t T
var i I = t
switch i.(type) {
case T:
println("it is type T")
case int:
println("it is type int")
case string:
println("it is type string")
}
}

在这个例子中,我们在 type switch 中使用了自定义的接口类型 I。那么,理论上所有 case 后面的类型都只能是实现了接口 I 的类型。但在这段代码中,只有类型 T 实现了接口类型 I,Go 原生类型 int 与 string 都没有实现接口 I,于是在编译上述代码时,编译器会报出如下错误信息:

1
2
3

19:2: impossible type switch case: i (type I) cannot have dynamic type int (missing M method)
21:2: impossible type switch case: i (type I) cannot have dynamic type string (missing M method)

switch在循环中使用

我们来实现一个代码,找出整型切片中的第一个偶数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
var sl = []int{5, 19, 6, 3, 8, 12}
var firstEven int = -1

for i := 0; i < len(sl); i++ {
switch sl[i] % 2 {
case 0:
firstEven = sl[i]
break
case 1:
// do nothing
}
}
println(firstEven)
}

>> 12

可以看到,输出的结果和我们想象的不一样。这是因为,在Go语言中,不带label的break语句中断执行并跳出的是同一函数内brak语句所在最内层的for、switch或select语句块。所以,上面的代码虽然在第一次找到偶数时跳出了switch,但是一直处于for loop中,所以会得出12。

下面修改一下即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
var sl = []int{5, 19, 6, 3, 8, 12}
var firstEven int = -1
loop:
for i := 0; i < len(sl); i++ {
switch sl[i] % 2 {
case 0:
firstEven = sl[i]
break loop
case 1:
// do nothing
}
}
println(firstEven)
}

main()

>> 6