最近翻開原始碼的時候看到了一種很有意思的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語句,但最好還是得看懂,不然下回看見它就只能乾瞪眼了。