Go CLI開発実戦:Cobraで本番級コマンドラインツールを構築する5つのコアパターン
编程语言
Go CLI開発:なぜCobraが2026年の最適解なのか
コマンドラインツールを開発する際、こんな問題に直面していませんか?引数解析のためにif-elseを大量に書く、サブコマンドのネストが混乱する、設定ファイルと環境変数の統一が難しい、シェル補完を数百行手書きする必要がある。CobraはGoエコシステムで最も成熟したCLIフレームワークで、Kubernetes、Hugo、GitHub CLIなどの有名プロジェクトで採用されています。2026年、Cobra v1.8+とViper v1.19+の組み合わせにより、コマンド定義→設定管理→シェル補完→インタラクティブプロンプト→クロスプラットフォーム配布の完全な本番級ソリューションが完成しています。
本記事では5つのコアパターンから、プロジェクト初期化→コマンドアーキテクチャ→設定統合→シェル補全→リリース配布のフルパイプライン実戦を解説します。
コア概念
| 概念 | 説明 |
|---|---|
| Cobra | Go CLIフレームワーク、コマンド・フラグ・ヘルプ生成を提供 |
| Command | コマンド単位、Run・Flags・Subcommandsを含む |
| Viper | 設定管理ライブラリ、ファイル・環境変数・Flagsをサポート |
| Persistent Flags | 親コマンドで定義されたFlags、子コマンドに自動継承 |
| Local Flags | 現在のコマンドのみ有効なFlags |
| Shell Completion | bash/zsh/fish/powershell補完スクリプトの自動生成 |
| GoReleaser | Goプロジェクトのクロスプラットフォームコンパイルとリリースツール |
| pflag | POSIX/GNUスタイルのコマンドライン引数解析ライブラリ |
問題分析:Go CLI開発の5つの課題
- コマンドアーキテクチャの混乱:サブコマンドのネストが深く、Flagsの継承関係が不明確
- 設定ソースの分散:設定ファイル・環境変数・コマンドライン引数の優先順位統一が困難
- シェル補完の欠落:ユーザーがすべてのコマンドとフラグを記憶する必要がある
- インタラクティブ体験の不足:純粋なCLIにはプログレスバーや選択プロンプトが欠如
- 配布の難しさ:マルチプラットフォームコンパイル、バージョン管理、Homebrew/SCOOP公開フローが複雑
ステップバイステップ:5つのGo CLIコアパターン
パターン1:Cobraプロジェクト構造とルートコマンド
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var cfgFile string
var verbose bool
var rootCmd = &cobra.Command{
Use: "mycli",
Short: "A production-grade CLI tool built with Cobra",
Long: `MyCLI is a powerful command-line tool that demonstrates
best practices for building Go CLI applications with Cobra.
Find more information at: https://github.com/example/mycli`,
Version: "1.0.0",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if verbose {
fmt.Println("Verbose mode enabled")
}
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.mycli.yaml)")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
cobra.CheckErr(err)
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".mycli")
}
viper.AutomaticEnv()
viper.SetEnvPrefix("MYCLI")
if err := viper.ReadInConfig(); err == nil && verbose {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
// main.go
package main
import "github.com/example/mycli/cmd"
func main() {
cmd.Execute()
}
パターン2:サブコマンド、FlagsとViper設定統合
// cmd/greet.go
package cmd
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
name string
times int
template string
)
var greetCmd = &cobra.Command{
Use: "greet [name]",
Short: "Send a greeting message",
Long: "Send a customizable greeting message with template support.",
Args: cobra.MaximumNArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
if len(args) > 0 {
name = args[0]
}
if name == "" {
name = viper.GetString("greet.default_name")
}
if template == "" {
template = viper.GetString("greet.template")
}
},
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < times; i++ {
msg := strings.ReplaceAll(template, "{{name}}", name)
fmt.Println(msg)
}
},
}
func init() {
rootCmd.AddCommand(greetCmd)
greetCmd.Flags().StringVarP(&name, "name", "n", "", "Name to greet")
greetCmd.Flags().IntVarP(×, "times", "t", 1, "Number of times to greet")
greetCmd.Flags().StringVarP(&template, "template", "", "Hello, {{name}}!", "Greeting template")
viper.BindPFlag("greet.name", greetCmd.Flags().Lookup("name"))
viper.BindPFlag("greet.times", greetCmd.Flags().Lookup("times"))
viper.BindPFlag("greet.template", greetCmd.Flags().Lookup("template"))
}
# ~/.mycli.yaml
greet:
default_name: "World"
template: "Hello, {{name}}! Welcome to MyCLI!"
times: 1
パターン3:シェル自動補完
// cmd/completion.go
package cmd
import (
"github.com/spf13/cobra"
)
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion script",
Long: `To load completions:
Bash:
$ source <(mycli completion bash)
Zsh:
$ mycli completion zsh > "${fpath[1]}/_mycli"
fish:
$ mycli completion fish | source
PowerShell:
PS> mycli completion powershell | Out-String | Invoke-Expression`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}
func init() {
rootCmd.AddCommand(completionCmd)
}
パターン4:インタラクティブプロンプトとプログレスバー
// cmd/init.go
package cmd
import (
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/manifoldco/promptui"
"github.com/schollz/progressbar/v3"
)
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize a new project interactively",
Run: func(cmd *cobra.Command, args []string) {
projectName := promptForName()
projectType := promptForType()
features := promptForFeatures()
fmt.Printf("Creating %s project '%s' with features: %v\n", projectType, projectName, features)
bar := progressbar.NewOptions(100,
progressbar.OptionSetDescription("Initializing project..."),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionShowCount(),
)
for i := 0; i < 100; i++ {
bar.Add(1)
time.Sleep(20 * time.Millisecond)
}
fmt.Println("\nProject initialized successfully!")
},
}
func promptForName() string {
prompt := promptui.Prompt{
Label: "Project Name",
Default: "my-project",
Validate: func(input string) error {
if len(input) < 2 {
return fmt.Errorf("project name must be at least 2 characters")
}
return nil
},
}
result, err := prompt.Run()
cobra.CheckErr(err)
return result
}
func promptForType() string {
prompt := promptui.Select{
Label: "Select Project Type",
Items: []string{"Web API", "CLI Tool", "Library", "Microservice"},
Size: 4,
}
_, result, err := prompt.Run()
cobra.CheckErr(err)
return result
}
func promptForFeatures() []string {
prompt := promptui.MultiSelect{
Label: "Select Features",
Items: []string{"Docker", "CI/CD", "Database", "Auth", "Logging", "Monitoring"},
}
result, err := prompt.Run()
cobra.CheckErr(err)
return result
}
func init() {
rootCmd.AddCommand(initCmd)
}
パターン5:GoReleaserクロスプラットフォームコンパイルと配布
# .goreleaser.yaml
project_name: mycli
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
main: ./main.go
archives:
- format: tar.gz
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "arm64" }}aarch64
{{- else }}{{ .Arch }}{{ end }}
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
brews:
- name: mycli
homepage: "https://github.com/example/mycli"
description: "A production-grade CLI tool"
repository:
owner: example
name: homebrew-tap
scoop:
url_template: "https://github.com/example/mycli/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
bucket:
owner: example
name: scoop-bucket
homepage: "https://github.com/example/mycli"
license: MIT
よくある落とし穴
落とし穴1:Flagsバインド順序のエラー
// ❌ 間違い:設定を読んでからFlagsをバインド
name := viper.GetString("name")
viper.BindPFlag("name", cmd.Flags().Lookup("name"))
// ✅ 正しい:先にFlagsをバインドしてから設定を読む
viper.BindPFlag("name", cmd.Flags().Lookup("name"))
name := viper.GetString("name")
落とし穴2:PersistentFlagsとLocalFlagsの混同
// ❌ 間違い:グローバル設定にLocalFlagsを使用
greetCmd.Flags().StringVar(&cfgFile, "config", "", "config file")
// ✅ 正しい:グローバル設定にはPersistentFlagsを使用、サブコマンドに自動継承
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
落とし穴3:PreRunとRunの実行タイミング
// ❌ 間違い:init()で引数検証
func init() {
greetCmd.Flags().IntVarP(&port, "port", "p", 0, "port number")
if port < 0 || port > 65535 {
log.Fatal("invalid port")
}
}
// ✅ 正しい:PreRunEで引数検証
greetCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
if port < 0 || port > 65535 {
return fmt.Errorf("invalid port: %d", port)
}
return nil
}
落とし穴4:シェル補完の登録タイミング
// ❌ 間違い:completionコマンドの後に新しいコマンドを追加
rootCmd.AddCommand(completionCmd)
rootCmd.AddCommand(newCmd)
// ✅ 正しい:すべてのコマンドを追加してからcompletionを追加
rootCmd.AddCommand(newCmd)
rootCmd.AddCommand(completionCmd)
落とし穴5:GoReleaserのldflags変数がエクスポートされていない
// ❌ 間違い:小文字の変数はldflagsで設定できない
var version = "dev"
// ✅ 正しい:mainパッケージでエクスポート変数を使用
var (
version = "dev"
commit = "none"
date = "unknown"
)
func main() {
rootCmd.Version = version
cmd.Execute()
}
エラートラブルシューティング
| # | エラーメッセージ | 原因 | 解決方法 |
|---|---|---|---|
| 1 | unknown flag: --config |
Flagsが未登録 | init()でFlags()登録を確認 |
| 2 | required flag(s) "name" not set |
必須フラグ未指定 | MarkFlagRequired()で必須マーク |
| 3 | unknown command "server" for "mycli" |
サブコマンド未追加 | rootCmd.AddCommand()を確認 |
| 4 | config file not found |
設定ファイルパスエラー | viper.AddConfigPath()のパスを確認 |
| 5 | completion command already exists |
completionの重複登録 | completionコマンドは1回のみ登録 |
| 6 | flag needs an argument: --port |
Flagの値が不足 | コマンドライン引数が完全か確認 |
| 7 | invalid argument "abc" for "--port" |
型の不一致 | Flagの型と入力が一致するか確認 |
| 8 | Error: unknown shorthand flag |
ショートフラグ未定義 | Flags()でショート名を設定 |
| 9 | goReleaser: multiple binaries |
builds設定の重複 | .goreleaser.yamlのbuildsフィールドを確認 |
| 10 | permission denied: /usr/local/bin |
インストールディレクトリの権限不足 | sudoを使用またはパスを変更 |
高度な最適化
- カスタムヘルプテンプレート:
cmd.SetHelpTemplate()でヘルプ形式をカスタマイズ、例とリンクを追加 - コマンドグループ化:
cmd.GroupIDとrootCmd.AddGroup()でサブコマンドをグループ表示 - プラグインアーキテクチャ:
pluginパッケージやRPCでCLIプラグインシステムを実現 - TUIインターフェース:Bubble Teaを統合してリッチなターミナルUIを実現
- 自動更新:go-selfupdateを統合してCLIの自動バージョン検出と更新を実現
比較分析
| 次元 | Cobra | urfave/cli | cliche | kong |
|---|---|---|---|---|
| 学習曲線 | 中程度 | 低い | 低い | 中程度 |
| サブコマンドネスト | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
| シェル補完 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐ |
| 設定統合 | ⭐⭐⭐⭐⭐(Viper) | ⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐ |
| ヘルプ生成 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| エコシステム成熟度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐ |
| コード生成 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐ | ⭐⭐⭐ |
| ドキュメント品質 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
まとめ:Cobra + Viper + GoReleaserは2026年のGo CLI開発の黄金トライアングルを形成します。コマンド定義→設定管理→シェル補完→インタラクティブプロンプト→クロスプラットフォーム配布の5つが一体となり、ゼロからリリースまで週末1つで完了します。Cobraを選ぶ理由は、機能の網羅性だけでなく、KubernetesやGitHub CLIなどのトッププロジェクトによる裏付けがあります。同じ落とし穴を踏む人が多ければ、自然と埋まっていくものです。
オンラインツール推奨
- JSONフォーマッター:/ja/json/format
- Hash計算:/ja/encode/hash
- cURL→コード変換:/ja/dev/curl-to-code
ブラウザローカルツールを無料で試す →
#Go CLI开发#Cobra框架#命令行工具#Go CLI最佳实践#2026#编程语言