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

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

  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的分支。

它等价于下面这段:

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语句,但最好还是得看懂,不然下回看见它就只能干瞪眼了。

参考

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

golang中一种不常见的switch语句写法的更多相关文章

  1. Java-Annotation的一种用法(消除代码中冗余的if/else或switch语句)

    Java-Annotation的一种用法(消除代码中冗余的if/else或switch语句) 1.冗余的if/else或switch ​ 有没有朋友写过以下的代码结构,大量的if/esle判断,来选择 ...

  2. Go_18: Golang 中三种读取文件发放性能对比

    Golang 中读取文件大概有三种方法,分别为: 1. 通过原生态 io 包中的 read 方法进行读取 2. 通过 io/ioutil 包提供的 read 方法进行读取 3. 通过 bufio 包提 ...

  3. Golang 中三种读取文件发放性能对比

    Golang 中读取文件大概有三种方法,分别为: 1. 通过原生态 io 包中的 read 方法进行读取 2. 通过 io/ioutil 包提供的 read 方法进行读取 3. 通过 bufio 包提 ...

  4. Java中三种比较常见的数组排序

    我们学习数组比较常用的数组排序算法不是为了在工作中使用(这三个算法性能不高),而是为了练习for循环和数组.因为在工作中Java API提供了现成的优化的排序方法,效率很高,以后工作中直接使用即可 . ...

  5. Java中枚举类型与for、switch语句

    1.枚举类型的声明 格式为: enum 枚举类型名{ 常量1,常量2,常量3 } 如: enum Number{ one,two,three,four,five    //常量} 注意:enum内装的 ...

  6. golang中并发sync和channel

    golang中实现并发非常简单,只需在需要并发的函数前面添加关键字"go",但是如何处理go并发机制中不同goroutine之间的同步与通信,golang 中提供了sync包和channel ...

  7. C++中switch 语句中的变量声明和

    switch 内部的变量定义: ; switch(i) { : string str; //error ; //error int val2; //right ; //right : val2 = ; ...

  8. golang中发送http请求的几种常见情况

    整理一下golang中各种http的发送方式 方式一 使用http.Newrequest 先生成http.client -> 再生成 http.request -> 之后提交请求:clie ...

  9. .Net中几种常见的页面跳转传值方法

    1.ASP Server对象Execute方法 ASP Server对象的Execute方法可以在执行当前页面的过程中将另一个页面执行结果的内容插入到当前页面的输出中.Execute方法带一个参数,是 ...

  10. Java 获取*.properties配置文件中的内容 ,常见的两种方法

    import java.io.InputStream; import java.util.Enumeration; import java.util.List; import java.util.Pr ...

随机推荐

  1. 10.Java中Map的entrySet() 详解以及用法

    一.Map.entry是什么? Map是java中的接口,Map.Entry是Map的一个内部接口. 此接口为泛型,定义为Entry<K,V>.它表示Map中的一个实体(一个key-val ...

  2. 《Unix/Linux系统编程》第十二周学习笔记

    <Unix/Linux系统编程>第十二周学习笔记 MySQL数据库简介 MySQL是一个关系型数据库管理系统,是最流行的关系型数据库管理系统之一.在 WEB 应用方面,MySQL 是最好的 ...

  3. MQ(部署模式)

    MQ部署模式 1.master-slave部署模式 1)shared filesystem Master-Slave部署方式 主要是通过共享存储目录来实现master和slave的热备,所有的Acti ...

  4. kettle连接oracle

    连接oracle 10g 驱动classes12.jar 配置一下三项即可: 1.数据库名称:ip:端口/实例 2.用户名 3.密码

  5. unidbgrid默认列排序

    UniDBGrid -> ClientEvents -> ExtEvents ->... function reconfigure(sender, store, columns, o ...

  6. laravel whereHas sum & addSelect sum

    $users = User::select('id', 'username', 'coins', 'cut') ->when(request()->has('agent_tip_sum') ...

  7. 【VSC】ERROR:GDB exited unexpectedly.

    [VSC]ERROR:GDB exited unexpectedly. 记录一次调试所遇bug的查找历程 问题描述 ERROR: GDB exited unexpectedly. Debugging ...

  8. 详解低延时高音质:丢包、抖动与 last mile 优化那些事儿

    本篇是「详解低延时高音质系列」的第三篇技术分享.我们这次要将视角放大,从整个音频引擎链路的角度,来讲讲在时变的网络下,针对不同的应用场景,如何权衡音质和互动的实时性. 当我们在讨论实时互动场景下的低延 ...

  9. webpack原理(1):Webpack热更新实现原理代码分析

    热更新,主要就是把前端工程 文件变更,即时编译,然后通知到浏览器端,刷新代码. 服务单与客户端通信方式有:ajax 轮询,EventSource.websockt. 客户端刷新一般分为两种: 整体页面 ...

  10. 自己动手从零写桌面操作系统GrapeOS系列教程——20.汇编语言读硬盘实战

    学习操作系统原理最好的方法是自己写一个简单的操作系统. 本讲我们设计一个简单的读硬盘实验.通过一定的方法使硬盘第二个扇区的前3个字节依次为1.2.3,最后3个字节依次为3.2.1,中间的506个字节全 ...