跳至主要内容

指令集 Command

承自上一章節,為了方便讓專案運行不同性質的需求,我們須要將不同的組態封裝成啟動指令以利調用。在這邊會使用Cobra做示範,Cobra截至2026/01為止,仍是golang最主流的CLI封裝工具,以下代碼是某套系統遷移資料庫表的指令封裝:

package database

import (
"fmt"
"log"
"mahjongserver/config"
"mahjongserver/database"
"mahjongserver/utils/logger"

"github.com/spf13/cobra"
"go.uber.org/zap/zapcore"
)

var preRunWithError = func(cmd *cobra.Command, args []string) error {
config.InitConfig()
//Init Loggers
writers := []logger.WriterConfig{
//info log would only recording info level log
{
Level: zapcore.InfoLevel,
Path: fmt.Sprintf("%s/info.log", config.Logger().Path),
Enabler: func(lvl zapcore.Level) bool {
return lvl == zapcore.InfoLevel
},
RotateTime: config.Logger().RotateTime,
},
//error log would recording level higher than error level log
{
Level: zapcore.ErrorLevel,
Path: fmt.Sprintf("%s/error.log", config.Logger().Path),
Enabler: func(lvl zapcore.Level) bool {
return lvl >= zapcore.ErrorLevel
},
RotateTime: config.Logger().RotateTime,
},
}
err := logger.Init(int8(config.Logger().Level), nil, writers...)
if err != nil {
log.Fatalf("could not start the server: %v", err)
}

database.InitDatabase(true)
return nil
}

var Migrate = &cobra.Command{
Use: "migrate",
Short: "migrate repository",
Long: "migrate the repository for mahjong game server",
PreRunE: preRunWithError,
Run: func(cmd *cobra.Command, args []string) {
logger.InfoLog("running the migrate for repository...")

err := database.Migrate()
if err != nil {
logger.FatalLog("could not migrate the database", err)
return
}
logger.InfoLog("migrate the repository successfully")
},
}

可以看到,我們需要創建一個 cobra.Command 物件,在物件中設定它的調用別名、描述、以及流程。 我們一般會在PreRun中配置並初始化系統的基礎服務,如日誌器、資料庫等。 並在Run內部開啟主要的功能/外部服務,最後當主要功能運作完成後,則會在PostRun(如有)回收/關閉相關的資源。

cobra 允許樹狀地往下註冊指令,舉例來說:假設這一個指令 Migratedatabase 的其中一個功能,且database 是這個專案的其中一個指令集,那它則會被按照順序的加入parentCmd.AddCommand(&cmd)父節點的指令物件。

package main

import (
"fmt"
"os"

"github.com/spf13/cobra"
)

func main() {
Execute()
}

// 1. 根指令 (Root)
var rootCmd = &cobra.Command{
Use: "app",
Short: "我的主應用程式",
}

// 2. 中間層指令 (Database) - 通常這層只是一個容器,不執行具體動作
var dbCmd = &cobra.Command{
Use: "database",
Short: "資料庫相關指令集",
Run: func(cmd *cobra.Command, args []string) {
// 如果使用者只輸入 app database,可以顯示說明
cmd.Help()
},
}

// 3. 葉子節點指令 (Migrate) - 真正的功能執行處
var migrateCmd = &cobra.Command{
Use: "migrate",
Short: "執行資料庫遷移",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("正在執行資料庫遷移 (Migrating database...)")
},
}

func init() {
// --- 樹狀註冊的核心 ---

// 將 migrate 註冊為 database 的子指令
dbCmd.AddCommand(migrateCmd)

// 將 database 註冊為根指令的子指令
rootCmd.AddCommand(dbCmd)

// 註:你可以繼續往下加,例如 dbCmd.AddCommand(rollbackCmd)
}

func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

但請注意,它僅會執行最後呼叫的指令,舉例來說:

>> app database migrate

它只會執行migrate所註冊的相關函數,如果你希望在父節點做的一些操作能夠被子節點重用,可以在parent.PersistentPre/PostRun註冊初始化/結束函數,它的執行順序如下:

順序函數名稱性質說明
1PersistentPreRun繼承從目標往根節點找,執行最近的一個。
2PreRun當前只執行目標指令定義的邏輯。
3Run當前核心邏輯。通常你在這裡寫指令的主要功能。
4PostRun當前執行完後的清理工作。
5PersistentPostRun繼承從目標往根節點找,執行最近的一個。

詳細的用例與更多功能請參考官方文件