golang中一種不常見的switch語句寫法示例詳解
最近翻開源代碼的時(shí)候看到了一種很有意思的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 } }
你也可以在這找到它:代碼鏈接
簡單解釋下這段代碼在做什么:調(diào)用systemctl命令檢查指定的服務(wù)的運(yùn)行狀態(tài),具體做法是過濾systemctl的輸出然后根據(jù)得到的字符串的前綴判斷當(dāng)前的運(yùn)行狀態(tài)。
有意思的在于這個(gè)switch,首先它后面沒有任何表達(dá)式;其次在每個(gè)case后面都是個(gè)函數(shù)調(diào)用表達(dá)式,返回值都是bool類型的。
雖然看起來很怪異,但這段代碼肯定沒有語法問題,可以編譯通過;也沒有語義或者邏輯問題,因?yàn)槿思矣玫暮煤玫?,這個(gè)項(xiàng)目接近4000個(gè)星星不是大家亂點(diǎn)的。
這里就不賣關(guān)子了,直接公布答案:
如果switch
后面沒有任何表達(dá)式,那么它等價(jià)于這個(gè):switch true
;case表達(dá)式按從上到下從左到右的順序求值;如果case后面的表達(dá)式求出來的值和switch后面的表達(dá)式的值一樣,那么就進(jìn)入這個(gè)分支,其他case被忽略(除非用了fallthrough,但這會(huì)直接跳進(jìn)下一個(gè)case的分支,不會(huì)執(zhí)行下一個(gè)case上的表達(dá)式)。
那么上面那一串代碼就好理解了:
首先是switch true
,期待有個(gè)case能求出true這個(gè)值;從上到下執(zhí)行strings.HasPrefix
,如果是false就往下到下一個(gè)case,如果是true就進(jìn)入這個(gè)case的分支。
它等價(jià)于下面這段:
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 }
可以看到,光從可讀性上來說的話兩者很難說誰更優(yōu)秀;兩者同樣需要注意把常見的情況放在最前面來減少不必要的匹配(這里的switch-case不能像給整數(shù)常量時(shí)那樣直接進(jìn)行跳轉(zhuǎn),實(shí)際執(zhí)行和上面給出的if語句是差不多的)。
那么我們?cè)賮砜纯磧烧叩纳纱a,通常我不喜歡去研究編譯器生成的代碼,但這次是個(gè)小例外,對(duì)于執(zhí)行流程上很接近的兩段代碼,編譯器會(huì)怎么處理呢?
我們做個(gè)簡化版的例子:
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給關(guān)了,不然hasprefix內(nèi)聯(lián)出來的東西會(huì)導(dǎo)致整個(gè)匯編代碼難以閱讀。
上面的代碼還是很好理解的,“active”和“inactive”的case被放在一起,如果匹配到了就跳轉(zhuǎn)進(jìn)入對(duì)應(yīng)的分支;“activing”和“failed”的case也放在了一起,匹配到之后的操作與前面兩個(gè)case一樣(實(shí)際上上面兩個(gè)case的匹配執(zhí)行完就會(huì)跳轉(zhuǎn)到這兩個(gè),至于為啥要多一次跳轉(zhuǎn)我沒深究,可能是為了提高L1d
的命中率,一大塊指令可能會(huì)導(dǎo)致緩存里放不下從而付出更新緩存的代價(jià),而有流水線優(yōu)化的情況下一個(gè)jmp帶來的開銷可能低于緩存未命中的懲罰,不過這在實(shí)踐里很難測量,權(quán)當(dāng)我在自言自語也行)。最后那一串帶ret的語句塊就是對(duì)應(yīng)的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
除了函數(shù)名子不一樣之外,其他是一模一樣的,可以說兩者在生成代碼上也沒有區(qū)別。
你可以在這里看到代碼和他們的編譯產(chǎn)物:Compiler Explorer
既然生成代碼是一樣的,那性能就沒必要測量了,因?yàn)榭隙ㄊ且粯拥摹?/p>
最后總結(jié)一下這種不常用的switch寫法,形式如下:
switch { case 表達(dá)式1: // 如果是true do works1 case 表達(dá)式2: // 如果是true do works2 default: 都不是true就會(huì)到這里 }
考慮到在性能上這并沒有什么優(yōu)勢,而且對(duì)于初次見到這個(gè)寫法的人可能不能很快理解它的含義,所以這個(gè)寫法的使用場景我目前能想到的只有一處:
如果你的數(shù)據(jù)有固定的2種以上的前綴/后綴/某種模式,因?yàn)闆]法用固定的常量去表示這種情況,那么用case加上一個(gè)簡單的表達(dá)式(函數(shù)調(diào)用之類的)會(huì)比用if更緊湊,也能更好地表達(dá)語義,case越多效果越明顯。比如我在開頭舉的那個(gè)例子。
如果你的代碼不符合上述情況,那還是老老實(shí)實(shí)用if會(huì)更好。
話說回來,雖然你機(jī)會(huì)沒啥機(jī)會(huì)寫出這種switch語句,但最好還是得看懂,不然下回看見它就只能干瞪眼了。
參考
https://go.dev/ref/spec#Switch_statements
到此這篇關(guān)于golang中一種不常見的switch語句寫法的文章就介紹到這了,更多相關(guān)golang switch語句寫法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang實(shí)現(xiàn)AES對(duì)稱加密的過程詳解
AES是一個(gè)對(duì)稱密碼,旨在取代DES成為廣泛使用的標(biāo)準(zhǔn),本文給大家分享Golang實(shí)現(xiàn)AES對(duì)稱加密的過程,本文附有Golang實(shí)現(xiàn)AES加密ECB模式的源碼,感興趣的朋友跟隨小編一起學(xué)習(xí)下吧2021-05-05Golang?統(tǒng)計(jì)字符串中數(shù)字字母數(shù)量的實(shí)現(xiàn)方法
這篇文章主要介紹了Golang?統(tǒng)計(jì)字符串中數(shù)字字母數(shù)量,本文給出了兩種從字符串獲取數(shù)字與字母數(shù)量的方法,分別是ASCII 碼值和正則表達(dá)式,需要的朋友可以參考下2022-06-06go語言實(shí)現(xiàn)兩個(gè)協(xié)程交替打印
這篇文章主要介紹了go語言實(shí)現(xiàn)兩個(gè)協(xié)程交替打印,文章主要分享了兩種方法使用兩個(gè)channel和使用一個(gè)channel,內(nèi)容介紹詳細(xì)具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-03-03在?Golang?中使用?Cobra?創(chuàng)建?CLI?應(yīng)用
這篇文章主要介紹了在?Golang?中使用?Cobra?創(chuàng)建?CLI?應(yīng)用,來看下?Cobra?的使用,這里我們使用的?go1.13.3?版本,使用?Go?Modules?來進(jìn)行包管理,需要的朋友可以參考下2022-01-01Go語言實(shí)現(xiàn)棧與隊(duì)列基本操作學(xué)家
go語言中,并沒有棧與隊(duì)列相關(guān)的數(shù)據(jù)結(jié)構(gòu),但是我們可以借助切片來實(shí)現(xiàn)棧與隊(duì)列的操作;接下來我們一起實(shí)現(xiàn)棧與隊(duì)列基本操作,感興趣的可以了解一下2022-11-11