golang中一种不常见的switch语句写法
Posted apocelipes
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang中一种不常见的switch语句写法相关的知识,希望对你有一定的参考价值。
最近翻开源代码的时候看到了一种很有意思的switch用法,分享一下。
注意这里讨论的不是typed switch
,也就是case语句后面是类型的那种。
直接看代码:
func (s *systemd) Status() (Status, error)
exitCode, out, err := s.runWithOutput("systemctl", "is-active", s.unitName())
if exitCode == 0 && err != nil
return StatusUnknown, err
switch
case strings.HasPrefix(out, "active"):
return StatusRunning, nil
case strings.HasPrefix(out, "inactive"):
// inactive can also mean its not installed, check unit files
exitCode, out, err := s.runWithOutput("systemctl", "list-unit-files", "-t", "service", s.unitName())
if exitCode == 0 && err != nil
return StatusUnknown, err
if strings.Contains(out, s.Name)
// unit file exists, installed but not running
return StatusStopped, nil
// no unit file
return StatusUnknown, ErrNotInstalled
case strings.HasPrefix(out, "activating"):
return StatusRunning, nil
case strings.HasPrefix(out, "failed"):
return StatusUnknown, errors.New("service in failed state")
default:
return StatusUnknown, ErrNotInstalled
你也可以在这找到它:代码链接
简单解释下这段代码在做什么:调用systemctl命令检查指定的服务的运行状态,具体做法是过滤systemctl的输出然后根据得到的字符串的前缀判断当前的运行状态。
有意思的在于这个switch,首先它后面没有任何表达式;其次在每个case后面都是个函数调用表达式,返回值都是bool类型的。
虽然看起来很怪异,但这段代码肯定没有语法问题,可以编译通过;也没有语义或者逻辑问题,因为人家用的好好的,这个项目接近4000个星星不是大家乱点的。
这里就不卖关子了,直接公布答案:
- 如果
switch
后面没有任何表达式,那么它等价于这个:switch true
; - case表达式按从上到下从左到右的顺序求值;
- 如果case后面的表达式求出来的值和switch后面的表达式的值一样,那么就进入这个分支,其他case被忽略(除非用了fallthrough,但这会直接跳进下一个case的分支,不会执行下一个case上的表达式)。
那么上面那一串代码就好理解了:
- 首先是
switch true
,期待有个case能求出true这个值; - 从上到下执行
strings.HasPrefix
,如果是false就往下到下一个case,如果是true就进入这个case的分支。
它等价于下面这段:
func (s *systemd) Status() (Status, error)
exitCode, out, err := s.runWithOutput("systemctl", "is-active", s.unitName())
if exitCode == 0 && err != nil
return StatusUnknown, err
if strings.HasPrefix(out, "active")
return StatusRunning, nil
if strings.HasPrefix(out, "inactive")
// inactive can also mean its not installed, check unit files
exitCode, out, err := s.runWithOutput("systemctl", "list-unit-files", "-t", "service", s.unitName())
if exitCode == 0 && err != nil
return StatusUnknown, err
if strings.Contains(out, s.Name)
// unit file exists, installed but not running
return StatusStopped, nil
// no unit file
return StatusUnknown, ErrNotInstalled
if strings.HasPrefix(out, "activating")
return StatusRunning, nil
if strings.HasPrefix(out, "failed")
return StatusUnknown, errors.New("service in failed state")
return StatusUnknown, ErrNotInstalled
可以看到,光从可读性上来说的话两者很难说谁更优秀;两者同样需要注意把常见的情况放在最前面来减少不必要的匹配(这里的switch-case不能像给整数常量时那样直接进行跳转,实际执行和上面给出的if语句是差不多的)。
那么我们再来看看两者的生成代码,通常我不喜欢去研究编译器生成的代码,但这次是个小例外,对于执行流程上很接近的两段代码,编译器会怎么处理呢?
我们做个简化版的例子:
func status1(cmdOutput string, flag int) int
switch
case strings.HasPrefix(cmdOutput, "active"):
return 1
case strings.HasPrefix(cmdOutput, "inactive"):
if flag > 0
return 2
return -1
case strings.HasPrefix(cmdOutput, "activating"):
return 1
case strings.HasPrefix(cmdOutput, "failed"):
return -1
default:
return -2
func status2(cmdOutput string, flag int) int
if strings.HasPrefix(cmdOutput, "active")
return 1
if strings.HasPrefix(cmdOutput, "inactive")
if flag > 0
return 2
return -1
if strings.HasPrefix(cmdOutput, "activating")
return 1
if strings.HasPrefix(cmdOutput, "failed")
return -1
return -2
这是switch版本的汇编:
main_status1_pc0:
TEXT main.status1(SB), ABIInternal, $40-24
CMPQ SP, 16(R14)
PCDATA $0, $-2
JLS main_status1_pc273
PCDATA $0, $-1
SUBQ $40, SP
MOVQ BP, 32(SP)
LEAQ 32(SP), BP
FUNCDATA $0, gclocals·wgcWObbY2HYnK2SU/U22lA==(SB)
FUNCDATA $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB)
FUNCDATA $5, main.status1.arginfo1(SB)
FUNCDATA $6, main.status1.argliveinfo(SB)
PCDATA $3, $1
MOVQ CX, main.flag+64(SP)
MOVQ AX, main.cmdOutput+48(SP)
MOVQ BX, main.cmdOutput+56(SP)
PCDATA $3, $-1
MOVL $6, DI
LEAQ go:string."active"(SB), CX
PCDATA $1, $0
CALL strings.HasPrefix(SB)
NOP
TESTB AL, AL
JNE main_status1_pc258
MOVQ main.cmdOutput+48(SP), AX
MOVQ main.cmdOutput+56(SP), BX
LEAQ go:string."inactive"(SB), CX
MOVL $8, DI
NOP
CALL strings.HasPrefix(SB)
TESTB AL, AL
JEQ main_status1_pc147
MOVQ main.flag+64(SP), CX
TESTQ CX, CX
JLE main_status1_pc130
MOVL $2, AX
MOVQ 32(SP), BP
ADDQ $40, SP
RET
main_status1_pc130:
MOVQ $-1, AX
MOVQ 32(SP), BP
ADDQ $40, SP
RET
main_status1_pc147:
MOVQ main.cmdOutput+48(SP), AX
MOVQ main.cmdOutput+56(SP), BX
LEAQ go:string."activating"(SB), CX
MOVL $10, DI
CALL strings.HasPrefix(SB)
TESTB AL, AL
JNE main_status1_pc243
MOVQ main.cmdOutput+48(SP), AX
MOVQ main.cmdOutput+56(SP), BX
LEAQ go:string."failed"(SB), CX
MOVL $6, DI
PCDATA $1, $1
CALL strings.HasPrefix(SB)
TESTB AL, AL
JEQ main_status1_pc226
MOVQ $-1, AX
MOVQ 32(SP), BP
ADDQ $40, SP
RET
main_status1_pc226:
MOVQ $-2, AX
MOVQ 32(SP), BP
ADDQ $40, SP
RET
main_status1_pc243:
MOVL $1, AX
MOVQ 32(SP), BP
ADDQ $40, SP
RET
main_status1_pc258:
MOVL $1, AX
MOVQ 32(SP), BP
ADDQ $40, SP
RET
main_status1_pc273:
NOP
PCDATA $1, $-1
PCDATA $0, $-2
MOVQ AX, 8(SP)
MOVQ BX, 16(SP)
MOVQ CX, 24(SP)
CALL runtime.morestack_noctxt(SB)
MOVQ 8(SP), AX
MOVQ 16(SP), BX
MOVQ 24(SP), CX
PCDATA $0, $-1
JMP main_status1_pc0
我把inline给关了,不然hasprefix内联出来的东西会导致整个汇编代码难以阅读。
上面的代码还是很好理解的,“active”和“inactive”的case被放在一起,如果匹配到了就跳转进入对应的分支;“activing”和“failed”的case也放在了一起,匹配到之后的操作与前面两个case一样(实际上上面两个case的匹配执行完就会跳转到这两个,至于为啥要多一次跳转我没深究,可能是为了提高L1d
的命中率,一大块指令可能会导致缓存里放不下从而付出更新缓存的代价,而有流水线优化的情况下一个jmp带来的开销可能低于缓存未命中的惩罚,不过这在实践里很难测量,权当我在自言自语也行)。最后那一串带ret的语句块就是对应的case的分支。
再来看看if的代码:
main_status2_pc0:
TEXT main.status2(SB), ABIInternal, $40-24
CMPQ SP, 16(R14)
PCDATA $0, $-2
JLS main_status2_pc273
PCDATA $0, $-1
SUBQ $40, SP
MOVQ BP, 32(SP)
LEAQ 32(SP), BP
FUNCDATA $0, gclocals·wgcWObbY2HYnK2SU/U22lA==(SB)
FUNCDATA $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB)
FUNCDATA $5, main.status2.arginfo1(SB)
FUNCDATA $6, main.status2.argliveinfo(SB)
PCDATA $3, $1
MOVQ CX, main.flag+64(SP)
MOVQ AX, main.cmdOutput+48(SP)
MOVQ BX, main.cmdOutput+56(SP)
PCDATA $3, $-1
MOVL $6, DI
LEAQ go:string."active"(SB), CX
PCDATA $1, $0
CALL strings.HasPrefix(SB)
NOP
TESTB AL, AL
JNE main_status2_pc258
MOVQ main.cmdOutput+48(SP), AX
MOVQ main.cmdOutput+56(SP), BX
LEAQ go:string."inactive"(SB), CX
MOVL $8, DI
NOP
CALL strings.HasPrefix(SB)
TESTB AL, AL
JEQ main_status2_pc147
MOVQ main.flag+64(SP), CX
TESTQ CX, CX
JLE main_status2_pc130
MOVL $2, AX
MOVQ 32(SP), BP
ADDQ $40, SP
RET
main_status2_pc130:
MOVQ $-1, AX
MOVQ 32(SP), BP
ADDQ $40, SP
RET
main_status2_pc147:
MOVQ main.cmdOutput+48(SP), AX
MOVQ main.cmdOutput+56(SP), BX
LEAQ go:string."activating"(SB), CX
MOVL $10, DI
CALL strings.HasPrefix(SB)
TESTB AL, AL
JNE main_status2_pc243
MOVQ main.cmdOutput+48(SP), AX
MOVQ main.cmdOutput+56(SP), BX
LEAQ go:string."failed"(SB), CX
MOVL $6, DI
PCDATA $1, $1
CALL strings.HasPrefix(SB)
TESTB AL, AL
JEQ main_status2_pc226
MOVQ $-1, AX
MOVQ 32(SP), BP
ADDQ $40, SP
RET
main_status2_pc226:
MOVQ $-2, AX
MOVQ 32(SP), BP
ADDQ $40, SP
RET
main_status2_pc243:
MOVL $1, AX
MOVQ 32(SP), BP
ADDQ $40, SP
RET
main_status2_pc258:
MOVL $1, AX
MOVQ 32(SP), BP
ADDQ $40, SP
RET
main_status2_pc273:
NOP
PCDATA $1, $-1
PCDATA $0, $-2
MOVQ AX, 8(SP)
MOVQ BX, 16(SP)
MOVQ CX, 24(SP)
CALL runtime.morestack_noctxt(SB)
MOVQ 8(SP), AX
MOVQ 16(SP), BX
MOVQ 24(SP), CX
PCDATA $0, $-1
JMP main_status2_pc0
除了函数名子不一样之外,其他是一模一样的,可以说两者在生成代码上也没有区别。
你可以在这里看到代码和他们的编译产物:Compiler Explorer
既然生成代码是一样的,那性能就没必要测量了,因为肯定是一样的。
最后总结一下这种不常用的switch写法,形式如下:
switch
case 表达式1: // 如果是true
do works1
case 表达式2: // 如果是true
do works2
default:
都不是true就会到这里
考虑到在性能上这并没有什么优势,而且对于初次见到这个写法的人可能不能很快理解它的含义,所以这个写法的使用场景我目前能想到的只有一处:
如果你的数据有固定的2种以上的前缀/后缀/某种模式,因为没法用固定的常量去表示这种情况,那么用case加上一个简单的表达式(函数调用之类的)会比用if更紧凑,也能更好地表达语义,case越多效果越明显。比如我在开头举的那个例子。
如果你的代码不符合上述情况,那还是老老实实用if会更好。
话说回来,虽然你机会没啥机会写出这种switch语句,但最好还是得看懂,不然下回看见它就只能干瞪眼了。
参考
Golang 条件循环语句
前言
本文简单描述 golang 中最常用的两种流程控制语句,条件和循环。在其他编程语言的基础上介绍条件语句 if 和 switch 的语法和 fallthrough 关键字,以及循环语句 for 的三种使用形式和 break/continue 关键字,并粗略介绍了 goto 语句的用法。
文章目录
条件语句
golang 的条件语句和其他语言大同小异,仅做一些简单介绍即可。
if 语句
if 语句和其他语言相同,使用 if - elseif - else 的格式,需要注意的是 go 的 condition 只能为表达式,如 if res != 0
,而不能像 python 一样直接使用 if res
if condition
// dosomething
else if condition
// dosomething
else
// dosomething
go 的 if 语句还可以和 ;
结合,成为 if 赋值;判断
的形式,常用于错误处理或者判断 map 元素是否存在等场景,如下:
func f() error
// ...
if err := f(); err != nil
// ...
// 等价于
// err := f()
// if err != nil
//
// 判断字典元素是否存在
// 判断某个 key n 是否在字典中
// 若存在,ok 为 True,否则为 False
if _, ok := map_name[n]; ok
// ...
switch 语句
switch 语句执行的过程从上至下,直到找到匹配项
和 C 语言等不同的是,go 的 switch 默认情况下 case 最后自带 break,不需要再显示的加 break。匹配成功后就不会执行其他 case,如果要执行后面的 case,需要使用 fallthrough 关键字。
switch i
case 0: // 空的 case 体
case 1:
f() // 当i == 0 时,f 不会被调用!
switch i
case 0: fallthrough
case 1:
f() // 当i == 0 时,f 会被调用!
switch i
case 0, 1, 2:
f() // 当 i 为 0 或 1 或 2 时,f 会被调用!
select 语句
select 为 channel 相关的流程控制语句,会在后面介绍 channel 的时候详细介绍。
for 循环语句
go 的循环只有 for 循环这一种方式,没有其他语言中的 while,do while 等。
第一种形式
go 没有逗号表达式,所以如果要执行多个变量,要用平行赋值
go 中 ++,--
是语句而不是表达式,所以只能出现 i++
,而 j = i++
这种写法会报错
for initialization; condition; post
// dosomething
// 不能写为 for i := 0, j := len(a) - 1; i < j; i++, j--
for i, j := 0, len(a) - 1; i < j; i, j = i + 1, j - 1
a[i], a[j] = a[j], a[i]
第二种形式
类似其他语言的 while 循环
condition 也可以省略变成无限循环
for condition
// dosomething
第三种形式
实际开发中最常使用这种形式,用于遍历 array,slice,string, map, channel 等,每次循环迭代, range 产生一对值;索引以及在该索引处的元素值。
类似 python 中的 for i, num in enumerate(nums)
for i, num := range nums
// dosomething
// 如果不需要用到索引,则必须用 _ 空标识符(blank identifier)代替索引变量,因为Go语言不允许使用无用的局部变量(local variables,会导致编译错误
for _, num := range nums
// dosomething
break / continue
go 的循环语句可以使用 break 和 continue 来控制循环,使用方式和其他语言一样,使用 break 来中断循环,使用 continue 跳过本次循环进行下一次循环,此处不做赘述。
比较有特色的是,go 的 break 可以指定跳出的标签,如下, 如果直接用 break,则跳出 j
循环,进入 i
循环,但是在 i
层添加了 tag,并且 break 到 tag 处,则直接跳出 i
循环。
LOOP:
for i < 10
for j < 10
j++
if j = 5
break LOOP
goto 跳转语句
Go 语言的 goto 语句可以无条件地转移到过程中指定的行,通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能。但是,在结构化程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱,使理解和调试程序都产生困难。
var a int = 10
LOOP: for a < 20 // 设置标签
if a == 15
/* 跳过迭代 */
a = a + 1
goto LOOP // 跳到标签 “LOOP” 的位置
fmt.Printf("a的值为 : %d\\n", a)
a++
以上是关于golang中一种不常见的switch语句写法的主要内容,如果未能解决你的问题,请参考以下文章