前言

enum其實是enumeration的縮寫,讓使用者自己定義data type而避免定義一大堆固定的常數,通常用來定義一群相關的屬性,上禮拜在公司資深工程師提到golang如何實現enum的寫法,之前只有透過使用python的enum套件,沒看過用Go來實作覺得很有趣,因此想寫篇文章做個簡單的紀錄,此文會先用python說明沒有使用enum的缺點,再透過enum套件實作enum,最後再用Go實踐。

Python簡單範例

在公司開發的應用程式需要針對k8s的資源做操作,這裡用一個比較簡單沒有使用enum的寫法

# 定義常數
DEPLOYMENT = "Deployment"
STATEFULSET = "Statefulset"
DAEMONSET = "Daemonset"

def handle_resource(resource: str) -> None:
    if resource == DEPLOYMENT:
        print("處理資源:Deployment")
    elif resource == STATEFULSET:
        print("處理資源:Statefulset")
    elif resource == DAEMONSET:
        print("處理資源:Daemonset")
    else:
        print("未知的資源類型")

resource = "statefulset"
handle_resource(resource)

可以看到如果沒使用enum直接定義常數,主要可能會有三個問題

  • 缺乏類型安全,當傳入的變數是resource = "statefulset",因為第一個字沒有大寫我就會印出未知的資源,要在執行程式碼的時候才會發現錯誤
  • IDE不支援自動補齊的功能,當在resource=要輸入值時,IDE不會知道可能要填什麼
  • 常數散落在global空間,如果其他地方需要已定義的常數可能會發生衝突
from enum import Enum

class ResourceType(Enum):
    DEPLOYMENT = "Deployment"
    STATEFULSET = "Statefulset"
    DAEMONSET = "Daemonset"


def handle_resource(resource: ResourceType) -> None:
    if resource == ResourceType.DEPLOYMENT:
        print("處理資源:Deployment")
    elif resource == ResourceType.STATEFULSET:
        print("處理資源:Statefulset")
    elif resource == ResourceType.DAEMONSET:
        print("處理資源:Daemonset")
    else:
        print("未知的資源類型")

resource = ResourceType.STATEFULSET
handle_resource(resource)

在使用enum這個函式庫後,可以更輕鬆建立枚舉,並解決第一個範例沒有使用enum,因為打字錯誤而造成的未預期結果。 此外也分別解決上述遇到的問題

  • 不會接受沒有定義過的值,達到類型安全
  • IDE可以直接列出所有可用的選項,或是直接用tab鍵自動補齊
  • 所有使用的常數都會在Enum類別,不會汙染到其他命名空間

最後補充一個很實用的auto()函數,自動賦予遞增的整數值,從 1 開始類似Go的iota可以自動賦值,在做一些狀態管理或是錯誤碼定義也很常會用到

from enum import Enum, auto
class Status(Enum):
    PENDING = auto()    # 1
    RUNNING = auto()    # 2
    COMPLETED = auto()  # 3
    FAILED = auto()     # 4

print(Status.PENDING.value)    # 輸出: 1
print(Status.RUNNING.value)    # 輸出: 2
print(Status.COMPLETED.value)  # 輸出: 3
print(Status.FAILED.value)     # 輸出: 4

Go實現

在Go裡面可以透過自定義型別ResourceType實現enum

package main

import "fmt"

type ResourceType string

const (
	Deployment  ResourceType = "Deployment"
	Statefulset ResourceType = "Statefulset"
	Daemonset   ResourceType = "Daemonset"
)

func (r ResourceType) IsValid() bool {
	switch r {
	case Deployment, Statefulset, Daemonset:
		return true
	}
	return false
}

func handleResourceEnum(resource ResourceType) {
	if !resource.IsValid() {
		fmt.Println("無效的資源類型")
		return
	}

	switch resource {
	case Deployment:
		fmt.Println("處理資源:Deployment")
	case Statefulset:
		fmt.Println("處理資源:Statefulset")
	case Daemonset:
		fmt.Println("處理資源:Daemonset")
	}
}

func main() {
	handleResourceEnum(Statefulset)
}

這樣在呼叫handleResourceEnum函式傳入參數的時候,可以用到IDE自動補齊的功能,添加新的資源類型只需要在const裡面增加,更容易管理。此外也提供更好的封裝性,可以對自定義的型別增加IsValidz方法

最後也補上面提到的golang iota用法

package main

import "fmt"

type Status int

const (
	Pending   Status = iota // 0
	Running                 // 1
	Completed               // 2
	Failed                  // 3
)

func (s Status) String() string {
	switch s {
	case Pending:
		return "Pending"
	case Running:
		return "Running"
	case Completed:
		return "Completed"
	case Failed:
		return "Failed"
	default:
		return "Unknown"
	}
}

func main() {
	fmt.Printf(int(Pending))      // 輸出: 0
	fmt.Printf(int(Running))      // 輸出: 1
	fmt.Printf(int(Completed))    // 輸出: 2
	fmt.Printf(int(Failed))       // 輸出: 3
}

結論

透過這次比較 Python 和 Go 實現 enum 的方式,可以發現兩種語言各有特色。 Python透過內建的enum函式庫提供完整的列舉功能,而 Go 則是利用型別定義(Type Definition)和 iota 來實現類似功能。 在這兩種語言透過enum,都能實現型別安全、IDE 自動補齊和避免命名空間污染等優點,希望透過這個小技巧,再之後開發時的產品可以更容易維護和擴展~