经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Go语言 » 查看文章
golang中一种不常见的switch语句写法
来源:cnblogs  作者:apocelipes  时间:2023/5/4 9:17:31  对本文有异议

最近翻开源代码的时候看到了一种很有意思的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个星星不是大家乱点的。

这里就不卖关子了,直接公布答案:

  1. 如果switch后面没有任何表达式,那么它等价于这个:switch true
  2. case表达式按从上到下从左到右的顺序求值;
  3. 如果case后面的表达式求出来的值和switch后面的表达式的值一样,那么就进入这个分支,其他case被忽略(除非用了fallthrough,但这会直接跳进下一个case的分支,不会执行下一个case上的表达式)。

那么上面那一串代码就好理解了:

  1. 首先是switch true,期待有个case能求出true这个值;
  2. 从上到下执行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

原文链接:https://www.cnblogs.com/apocelipes/p/17368617.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号