golang中一種不常見的switch語句寫法

来源:https://www.cnblogs.com/apocelipes/archive/2023/05/03/17368617.html
-Advertisement-
Play Games

最近翻開源代碼的時候看到了一種很有意思的switch用法,分享一下。 註意這裡討論的不是typed switch,也就是case語句後面是類型的那種。 直接看代碼: func (s *systemd) Status() (Status, error) { exitCode, out, err := ...


最近翻開源代碼的時候看到了一種很有意思的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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Spring的Bean定義環節是Spring IoC容器中的核心流程之一。在這個過程中,Spring會掃描指定的包路徑,找到符合條件的Bean,並將其轉換為Bean定義。在這個過程中,Spring使用了ASM技術來解析類的註解信息,判斷當前類是否符合要求。然後,Spring將符合條件的Bean定義加... ...
  • 簡單字元串加密 編寫一個應用程式用來輸入的字元串進行加密,對於字母字元串加密規則如下: 'a→d’ ‘b'→’e’ ‘w’→z' ...... x'→’a’ ‘y'→b' ‘z→c’ ‘A’→’D’ ‘B'→’E’ ‘W’→’Z’ ‘X’→’A’ ‘Y’→’B’ ‘Z’→’C’ ?對於其他字元,不進 ...
  • 先聲明一下前期的一些手欠欠兒的操作導致oracl登錄不進去了,起先是清理磁碟空間的時候誤刪除了orcle DBF數據文件後無法進入系統,plsql登錄報錯如下: 一般情況下,刪除表空間的正確方法是:DROP TABLESPACE BDCDJ INCLUDING CONTENTS AND DATAFI ...
  • (DML語言) 前言 前面的兩篇文章中,我們已經對MySQL有了基本瞭解。 並且知道了怎麼用工具連接資料庫?怎麼創建資料庫?怎麼創建表? 這一篇呢我們就來看看怎麼在我們創建的表中插入數據、刪除數據和修改數據。也就是上一篇文章中提到的DML 數據操作語言 準備 根據上一章所說的,我們創建一個db_xi ...
  • 功能04-達人探店 5.功能04-達人探店 5.1發佈&查看探店筆記 5.1.1發佈探店筆記 探店筆記類似點評網站的評價,往往是圖文結合。對應的表有兩個: tb_blog:探店筆記表,包含筆記中的標題、文字、圖片等 tb_blog_comments:其他用戶對探店筆記的評價 /*表: tb_blog ...
  • (使用MySQL) 前言 根據上一篇文章【必知必會的MySQL知識】①初探MySQL的內容,想必您對MySQL資料庫有了一個整體的瞭解了,並且應該在自己電腦上已經安裝上了MySQL。 這一篇呢我們來說一說這麼連接上資料庫並且使用它。 啟動MySQL服務 前面MySQL安裝的文章手把手教你安裝MySQ ...
  • html基礎標簽 學習網站:https://www.acwing.com/ 學習查詢網站:https://developer.mozilla.org/zh-CN/ !+tab自動出現框架 1.1 文檔結構 html的所有標簽都為樹形結構,例如: <!DOCTYPE html> <html lang= ...
  • 圖片 png 無損壓縮,尺寸體積要比jpg的大,適合做小圖標 jpg 採用壓縮演算法,有一點失真,比png體積要小,適合做中大圖片 gif 一般是做動圖的 webp 同時支持有損或者無損壓縮,相同質量的圖片,webp具有更小的體積 css的盒子模型 標準盒子模型 margin/border/paddi ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...