golang中一种不常见的switch语句写法

打印 上一主题 下一主题

主题 914|帖子 914|积分 2742

最近翻开源代码的时候看到了一种很有意思的switch用法,分享一下。
注意这里讨论的不是typed switch,也就是case语句后面是类型的那种。
直接看代码:
  1. func (s *systemd) Status() (Status, error) {
  2.         exitCode, out, err := s.runWithOutput("systemctl", "is-active", s.unitName())
  3.         if exitCode == 0 && err != nil {
  4.                 return StatusUnknown, err
  5.         }
  6.         switch {
  7.         case strings.HasPrefix(out, "active"):
  8.                 return StatusRunning, nil
  9.         case strings.HasPrefix(out, "inactive"):
  10.                 // inactive can also mean its not installed, check unit files
  11.                 exitCode, out, err := s.runWithOutput("systemctl", "list-unit-files", "-t", "service", s.unitName())
  12.                 if exitCode == 0 && err != nil {
  13.                         return StatusUnknown, err
  14.                 }
  15.                 if strings.Contains(out, s.Name) {
  16.                         // unit file exists, installed but not running
  17.                         return StatusStopped, nil
  18.                 }
  19.                 // no unit file
  20.                 return StatusUnknown, ErrNotInstalled
  21.         case strings.HasPrefix(out, "activating"):
  22.                 return StatusRunning, nil
  23.         case strings.HasPrefix(out, "failed"):
  24.                 return StatusUnknown, errors.New("service in failed state")
  25.         default:
  26.                 return StatusUnknown, ErrNotInstalled
  27.         }
  28. }
复制代码
你也可以在这找到它:代码链接
简单解释下这段代码在做什么:调用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的分支。
它等价于下面这段:
  1. func (s *systemd) Status() (Status, error) {
  2.         exitCode, out, err := s.runWithOutput("systemctl", "is-active", s.unitName())
  3.         if exitCode == 0 && err != nil {
  4.                 return StatusUnknown, err
  5.         }
  6.     if strings.HasPrefix(out, "active") {
  7.         return StatusRunning, nil
  8.     }
  9.     if strings.HasPrefix(out, "inactive") {
  10.         // inactive can also mean its not installed, check unit files
  11.                 exitCode, out, err := s.runWithOutput("systemctl", "list-unit-files", "-t", "service", s.unitName())
  12.                 if exitCode == 0 && err != nil {
  13.                         return StatusUnknown, err
  14.                 }
  15.                 if strings.Contains(out, s.Name) {
  16.                         // unit file exists, installed but not running
  17.                         return StatusStopped, nil
  18.                 }
  19.                 // no unit file
  20.                 return StatusUnknown, ErrNotInstalled
  21.     }
  22.     if strings.HasPrefix(out, "activating") {
  23.                 return StatusRunning, nil
  24.     }
  25.     if strings.HasPrefix(out, "failed") {
  26.         return StatusUnknown, errors.New("service in failed state")
  27.     }
  28.         return StatusUnknown, ErrNotInstalled
  29. }
复制代码
可以看到,光从可读性上来说的话两者很难说谁更优秀;两者同样需要注意把常见的情况放在最前面来减少不必要的匹配(这里的switch-case不能像给整数常量时那样直接进行跳转,实际执行和上面给出的if语句是差不多的)。
那么我们再来看看两者的生成代码,通常我不喜欢去研究编译器生成的代码,但这次是个小例外,对于执行流程上很接近的两段代码,编译器会怎么处理呢?
我们做个简化版的例子:
  1. func status1(cmdOutput string, flag int) int {
  2.     switch {
  3.     case strings.HasPrefix(cmdOutput, "active"):
  4.         return 1
  5.     case strings.HasPrefix(cmdOutput, "inactive"):
  6.         if flag > 0 {
  7.             return 2
  8.         }
  9.         return -1
  10.     case strings.HasPrefix(cmdOutput, "activating"):
  11.         return 1
  12.     case strings.HasPrefix(cmdOutput, "failed"):
  13.         return -1
  14.     default:
  15.         return -2
  16.     }
  17. }
  18. func status2(cmdOutput string, flag int) int {
  19.     if strings.HasPrefix(cmdOutput, "active") {
  20.         return 1
  21.     }
  22.     if strings.HasPrefix(cmdOutput, "inactive") {
  23.         if flag > 0 {
  24.             return 2
  25.         }
  26.         return -1
  27.     }
  28.     if strings.HasPrefix(cmdOutput, "activating") {
  29.         return 1
  30.     }
  31.     if strings.HasPrefix(cmdOutput, "failed") {
  32.         return -1
  33.     }
  34.     return -2
  35. }
复制代码
这是switch版本的汇编:
  1. main_status1_pc0:
  2.         TEXT    main.status1(SB), ABIInternal, $40-24
  3.         CMPQ    SP, 16(R14)
  4.         PCDATA  $0, $-2
  5.         JLS     main_status1_pc273
  6.         PCDATA  $0, $-1
  7.         SUBQ    $40, SP
  8.         MOVQ    BP, 32(SP)
  9.         LEAQ    32(SP), BP
  10.         FUNCDATA        $0, gclocals·wgcWObbY2HYnK2SU/U22lA==(SB)
  11.         FUNCDATA        $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB)
  12.         FUNCDATA        $5, main.status1.arginfo1(SB)
  13.         FUNCDATA        $6, main.status1.argliveinfo(SB)
  14.         PCDATA  $3, $1
  15.         MOVQ    CX, main.flag+64(SP)
  16.         MOVQ    AX, main.cmdOutput+48(SP)
  17.         MOVQ    BX, main.cmdOutput+56(SP)
  18.         PCDATA  $3, $-1
  19.         MOVL    $6, DI
  20.         LEAQ    go:string."active"(SB), CX
  21.         PCDATA  $1, $0
  22.         CALL    strings.HasPrefix(SB)
  23.         NOP
  24.         TESTB   AL, AL
  25.         JNE     main_status1_pc258
  26.         MOVQ    main.cmdOutput+48(SP), AX
  27.         MOVQ    main.cmdOutput+56(SP), BX
  28.         LEAQ    go:string."inactive"(SB), CX
  29.         MOVL    $8, DI
  30.         NOP
  31.         CALL    strings.HasPrefix(SB)
  32.         TESTB   AL, AL
  33.         JEQ     main_status1_pc147
  34.         MOVQ    main.flag+64(SP), CX
  35.         TESTQ   CX, CX
  36.         JLE     main_status1_pc130
  37.         MOVL    $2, AX
  38.         MOVQ    32(SP), BP
  39.         ADDQ    $40, SP
  40.         RET
  41. main_status1_pc130:
  42.         MOVQ    $-1, AX
  43.         MOVQ    32(SP), BP
  44.         ADDQ    $40, SP
  45.         RET
  46. main_status1_pc147:
  47.         MOVQ    main.cmdOutput+48(SP), AX
  48.         MOVQ    main.cmdOutput+56(SP), BX
  49.         LEAQ    go:string."activating"(SB), CX
  50.         MOVL    $10, DI
  51.         CALL    strings.HasPrefix(SB)
  52.         TESTB   AL, AL
  53.         JNE     main_status1_pc243
  54.         MOVQ    main.cmdOutput+48(SP), AX
  55.         MOVQ    main.cmdOutput+56(SP), BX
  56.         LEAQ    go:string."failed"(SB), CX
  57.         MOVL    $6, DI
  58.         PCDATA  $1, $1
  59.         CALL    strings.HasPrefix(SB)
  60.         TESTB   AL, AL
  61.         JEQ     main_status1_pc226
  62.         MOVQ    $-1, AX
  63.         MOVQ    32(SP), BP
  64.         ADDQ    $40, SP
  65.         RET
  66. main_status1_pc226:
  67.         MOVQ    $-2, AX
  68.         MOVQ    32(SP), BP
  69.         ADDQ    $40, SP
  70.         RET
  71. main_status1_pc243:
  72.         MOVL    $1, AX
  73.         MOVQ    32(SP), BP
  74.         ADDQ    $40, SP
  75.         RET
  76. main_status1_pc258:
  77.         MOVL    $1, AX
  78.         MOVQ    32(SP), BP
  79.         ADDQ    $40, SP
  80.         RET
  81. main_status1_pc273:
  82.         NOP
  83.         PCDATA  $1, $-1
  84.         PCDATA  $0, $-2
  85.         MOVQ    AX, 8(SP)
  86.         MOVQ    BX, 16(SP)
  87.         MOVQ    CX, 24(SP)
  88.         CALL    runtime.morestack_noctxt(SB)
  89.         MOVQ    8(SP), AX
  90.         MOVQ    16(SP), BX
  91.         MOVQ    24(SP), CX
  92.         PCDATA  $0, $-1
  93.         JMP     main_status1_pc0
复制代码
我把inline给关了,不然hasprefix内联出来的东西会导致整个汇编代码难以阅读。
上面的代码还是很好理解的,“active”和“inactive”的case被放在一起,如果匹配到了就跳转进入对应的分支;“activing”和“failed”的case也放在了一起,匹配到之后的操作与前面两个case一样(实际上上面两个case的匹配执行完就会跳转到这两个,至于为啥要多一次跳转我没深究,可能是为了提高L1d的命中率,一大块指令可能会导致缓存里放不下从而付出更新缓存的代价,而有流水线优化的情况下一个jmp带来的开销可能低于缓存未命中的惩罚,不过这在实践里很难测量,权当我在自言自语也行)。最后那一串带ret的语句块就是对应的case的分支。
再来看看if的代码:
  1. main_status2_pc0:
  2.         TEXT    main.status2(SB), ABIInternal, $40-24
  3.         CMPQ    SP, 16(R14)
  4.         PCDATA  $0, $-2
  5.         JLS     main_status2_pc273
  6.         PCDATA  $0, $-1
  7.         SUBQ    $40, SP
  8.         MOVQ    BP, 32(SP)
  9.         LEAQ    32(SP), BP
  10.         FUNCDATA        $0, gclocals·wgcWObbY2HYnK2SU/U22lA==(SB)
  11.         FUNCDATA        $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB)
  12.         FUNCDATA        $5, main.status2.arginfo1(SB)
  13.         FUNCDATA        $6, main.status2.argliveinfo(SB)
  14.         PCDATA  $3, $1
  15.         MOVQ    CX, main.flag+64(SP)
  16.         MOVQ    AX, main.cmdOutput+48(SP)
  17.         MOVQ    BX, main.cmdOutput+56(SP)
  18.         PCDATA  $3, $-1
  19.         MOVL    $6, DI
  20.         LEAQ    go:string."active"(SB), CX
  21.         PCDATA  $1, $0
  22.         CALL    strings.HasPrefix(SB)
  23.         NOP
  24.         TESTB   AL, AL
  25.         JNE     main_status2_pc258
  26.         MOVQ    main.cmdOutput+48(SP), AX
  27.         MOVQ    main.cmdOutput+56(SP), BX
  28.         LEAQ    go:string."inactive"(SB), CX
  29.         MOVL    $8, DI
  30.         NOP
  31.         CALL    strings.HasPrefix(SB)
  32.         TESTB   AL, AL
  33.         JEQ     main_status2_pc147
  34.         MOVQ    main.flag+64(SP), CX
  35.         TESTQ   CX, CX
  36.         JLE     main_status2_pc130
  37.         MOVL    $2, AX
  38.         MOVQ    32(SP), BP
  39.         ADDQ    $40, SP
  40.         RET
  41. main_status2_pc130:
  42.         MOVQ    $-1, AX
  43.         MOVQ    32(SP), BP
  44.         ADDQ    $40, SP
  45.         RET
  46. main_status2_pc147:
  47.         MOVQ    main.cmdOutput+48(SP), AX
  48.         MOVQ    main.cmdOutput+56(SP), BX
  49.         LEAQ    go:string."activating"(SB), CX
  50.         MOVL    $10, DI
  51.         CALL    strings.HasPrefix(SB)
  52.         TESTB   AL, AL
  53.         JNE     main_status2_pc243
  54.         MOVQ    main.cmdOutput+48(SP), AX
  55.         MOVQ    main.cmdOutput+56(SP), BX
  56.         LEAQ    go:string."failed"(SB), CX
  57.         MOVL    $6, DI
  58.         PCDATA  $1, $1
  59.         CALL    strings.HasPrefix(SB)
  60.         TESTB   AL, AL
  61.         JEQ     main_status2_pc226
  62.         MOVQ    $-1, AX
  63.         MOVQ    32(SP), BP
  64.         ADDQ    $40, SP
  65.         RET
  66. main_status2_pc226:
  67.         MOVQ    $-2, AX
  68.         MOVQ    32(SP), BP
  69.         ADDQ    $40, SP
  70.         RET
  71. main_status2_pc243:
  72.         MOVL    $1, AX
  73.         MOVQ    32(SP), BP
  74.         ADDQ    $40, SP
  75.         RET
  76. main_status2_pc258:
  77.         MOVL    $1, AX
  78.         MOVQ    32(SP), BP
  79.         ADDQ    $40, SP
  80.         RET
  81. main_status2_pc273:
  82.         NOP
  83.         PCDATA  $1, $-1
  84.         PCDATA  $0, $-2
  85.         MOVQ    AX, 8(SP)
  86.         MOVQ    BX, 16(SP)
  87.         MOVQ    CX, 24(SP)
  88.         CALL    runtime.morestack_noctxt(SB)
  89.         MOVQ    8(SP), AX
  90.         MOVQ    16(SP), BX
  91.         MOVQ    24(SP), CX
  92.         PCDATA  $0, $-1
  93.         JMP     main_status2_pc0
复制代码
除了函数名子不一样之外,其他是一模一样的,可以说两者在生成代码上也没有区别。
你可以在这里看到代码和他们的编译产物:Compiler Explorer
既然生成代码是一样的,那性能就没必要测量了,因为肯定是一样的。
最后总结一下这种不常用的switch写法,形式如下:
  1. switch {
  2. case 表达式1: // 如果是true
  3.     do works1
  4. case 表达式2: // 如果是true
  5.     do works2
  6. default:
  7.     都不是true就会到这里
  8. }
复制代码
考虑到在性能上这并没有什么优势,而且对于初次见到这个写法的人可能不能很快理解它的含义,所以这个写法的使用场景我目前能想到的只有一处:
如果你的数据有固定的2种以上的前缀/后缀/某种模式,因为没法用固定的常量去表示这种情况,那么用case加上一个简单的表达式(函数调用之类的)会比用if更紧凑,也能更好地表达语义,case越多效果越明显。比如我在开头举的那个例子。
如果你的代码不符合上述情况,那还是老老实实用if会更好。
话说回来,虽然你机会没啥机会写出这种switch语句,但最好还是得看懂,不然下回看见它就只能干瞪眼了。
参考

https://go.dev/ref/spec#Switch_statements

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

盛世宏图

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表