摘要:func(s *systemd) Status (Status,error) {exitCode, out, err := s.runWithOutput("systemctl","is-active", s.unitName)if exitCode ==0
最近翻开源代码的时候看到了一种很有意思的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,nilcase strings.HasPrefix(out,"inactive"):// inactive can also mean its not installed, check unit filesexitCode, 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 runningreturn StatusStopped,nil}// no unit filereturn StatusUnknown, ErrNotInstalledcase strings.HasPrefix(out,"activating"):return StatusRunning,nilcase 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上的表达式)。那么上面那一串代码就好理解了:
它等价于下面这段:
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 filesexitCode, 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 runningreturn StatusStopped,nil}// no unit filereturn 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语句是差不多的)。
那么我们再来看看两者的生成代码,通常我不喜欢去研究编译器生成的代码,但这次是个小例外,对于执行流程上很接近的两段代码,编译器会怎么处理呢?
我们做个简化版的例子:
funcstatus1(cmdOutput string, flag int)int {switch {case strings.HasPrefix(cmdOutput,"active"):return1case strings.HasPrefix(cmdOutput,"inactive"):if flag >0 {return2}return-1case strings.HasPrefix(cmdOutput,"activating"):return1case strings.HasPrefix(cmdOutput,"failed"):return-1default:return-2}}funcstatus2(cmdOutput string, flag int)int {if strings.HasPrefix(cmdOutput,"active") {return1}if strings.HasPrefix(cmdOutput,"inactive") {if flag >0 {return2}return-1}if strings.HasPrefix(cmdOutput,"activating") {return1}if strings.HasPrefix(cmdOutput,"failed") {return-1}return-2}这是switch版本的汇编:
main_status1_pc0:TEXT main.status1(SB), ABIInternal,$40-24CMPQSP,16(R14)PCDATA$0, $-2JLS main_status1_pc273PCDATA$0, $-1SUBQ$40,SPMOVQBP,32(SP)LEAQ32(SP),BPFUNCDATA$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,$1MOVQCX, main.flag+64(SP)MOVQAX, main.cmdOutput+48(SP)MOVQBX, main.cmdOutput+56(SP)PCDATA$3, $-1MOVL$6,DILEAQ go:string."active"(SB),CXPCDATA$1,$0CALL strings.HasPrefix(SB)NOPTESTBAL,ALJNE main_status1_pc258MOVQ main.cmdOutput+48(SP),AXMOVQ main.cmdOutput+56(SP),BXLEAQ go:string."inactive"(SB),CXMOVL$8,DINOPCALL strings.HasPrefix(SB)TESTBAL,ALJEQ main_status1_pc147MOVQ main.flag+64(SP),CXTESTQCX,CXJLE main_status1_pc130MOVL$2,AXMOVQ32(SP),BPADDQ$40,SPretmain_status1_pc130:MOVQ $-1,AXMOVQ32(SP),BPADDQ$40,SPRETmain_status1_pc147:MOVQ main.cmdOutput+48(SP),AXMOVQ main.cmdOutput+56(SP),BXLEAQ go:string."activating"(SB),CXMOVL$10,DICALL strings.HasPrefix(SB)TESTBAL,ALJNE main_status1_pc243MOVQ main.cmdOutput+48(SP),AXMOVQ main.cmdOutput+56(SP),BXLEAQ go:string."failed"(SB),CXMOVL$6,DIPCDATA$1,$1CALL strings.HasPrefix(SB)TESTBAL,ALJEQ main_status1_pc226MOVQ $-1,AXMOVQ32(SP),BPADDQ$40,SPRETmain_status1_pc226:MOVQ $-2,AXMOVQ32(SP),BPADDQ$40,SPRETmain_status1_pc243:MOVL$1,AXMOVQ32(SP),BPADDQ$40,SPRETmain_status1_pc258:MOVL$1,AXMOVQ32(SP),BPADDQ$40,SPRETmain_status1_pc273:NOPPCDATA$1, $-1PCDATA$0, $-2MOVQAX,8(SP)MOVQBX,16(SP)MOVQCX,24(SP)CALL runtime.morestack_noctxt(SB)MOVQ8(SP),AXMOVQ16(SP),BXMOVQ24(SP),CXPCDATA$0, $-1JMP 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-24CMPQ SP, 16(R14)PCDATA $0, $-2JLS main_status2_pc273PCDATA $0, $-1SUBQ $40, SPMOVQ BP, 32(SP)LEAQ 32(SP), BPFUNCDATA $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, $1MOVQ CX, main.flag+64(SP)MOVQ AX, main.cmdOutput+48(SP)MOVQ BX, main.cmdOutput+56(SP)PCDATA $3, $-1MOVL $6, DILEAQ go:string."active"(SB), CXPCDATA $1, $0CALL strings.HasPrefix(SB)NOPTESTB AL, ALJNE main_status2_pc258MOVQ main.cmdOutput+48(SP), AXMOVQ main.cmdOutput+56(SP), BXLEAQ go:string."inactive"(SB), CXMOVL $8, DINOPCALL strings.HasPrefix(SB)TESTB AL, ALJEQ main_status2_pc147MOVQ main.flag+64(SP), CXTESTQ CX, CXJLE main_status2_pc130MOVL $2, AXMOVQ 32(SP), BPADDQ $40, SPRETmain_status2_pc130:MOVQ $-1, AXMOVQ 32(SP), BPADDQ $40, SPRETmain_status2_pc147:MOVQ main.cmdOutput+48(SP), AXMOVQ main.cmdOutput+56(SP), BXLEAQ go:string."activating"(SB), CXMOVL $10, DICALL strings.HasPrefix(SB)TESTB AL, ALJNE main_status2_pc243MOVQ main.cmdOutput+48(SP), AXMOVQ main.cmdOutput+56(SP), BXLEAQ go:string."failed"(SB), CXMOVL $6, DIPCDATA $1, $1CALL strings.HasPrefix(SB)TESTB AL, ALJEQ main_status2_pc226MOVQ $-1, AXMOVQ 32(SP), BPADDQ $40, SPRETmain_status2_pc226:MOVQ $-2, AXMOVQ 32(SP), BPADDQ $40, SPRETmain_status2_pc243:MOVL $1, AXMOVQ 32(SP), BPADDQ $40, SPRETmain_status2_pc258:MOVL $1, AXMOVQ 32(SP), BPADDQ $40, SPRETmain_status2_pc273:NOPPCDATA $1, $-1PCDATA $0, $-2MOVQ AX, 8(SP)MOVQ BX, 16(SP)MOVQ CX, 24(SP)CALL runtime.morestack_noctxt(SB)MOVQ 8(SP), AXMOVQ 16(SP), BXMOVQ 24(SP), CXPCDATA $0, $-1JMP main_status2_pc0除了函数名子不一样之外,其他是一模一样的,可以说两者在生成代码上也没有区别。
你可以在这里看到代码和他们的编译产物:Compiler Explorer
既然生成代码是一样的,那性能就没必要测量了,因为肯定是一样的。
最后总结一下这种不常用的switch写法,形式如下:
switch {case 表达式1:// 如果是truedo works1case 表达式2:// 如果是truedo works2default:都不是true就会到这里}考虑到在性能上这并没有什么优势,而且对于初次见到这个写法的人可能不能很快理解它的含义,所以这个写法的使用场景我目前能想到的只有一处:
如果你的数据有固定的2种以上的前缀/后缀/某种模式,因为没法用固定的常量去表示这种情况,那么用case加上一个简单的表达式(函数调用之类的)会比用if更紧凑,也能更好地表达语义,case越多效果越明显。比如我在开头举的那个例子。
如果你的代码不符合上述情况,那还是老老实实用if会更好。
话说回来,虽然你机会没啥机会写出这种switch语句,但最好还是得看懂,不然下回看见它就只能干瞪眼了。
来源:散文随风想一点号