🐵

実践形式で学ぶ! Go言語で作成するCLIツール 1章: 基礎編

に公開

この記事について

本記事では、Go 言語で CLI ツールをハンズオン形式で作ります。
Go の基本文法は理解している前提で進めるため、不安な方は先に A Tour of Go をひと通り確認してから読み進めてください。
教材コードは GitHub に公開しています → github.com/orchidee9392/go-cli-tutorial


1. 目標

1.1 全体の目標

  • Go で CLI ツールを作り、テスト / CI/CD / 配布まで見据えた形で運用できるようになる。
  • (発展)中規模でも管理しやすいフォルダ構成とドキュメントを整備できる。

1.2 本記事の目標(基礎編)

  • cobra を用いた最小構成の CLI(挨拶コマンド)を作れるようになる。
  • コマンドの構造分割(main / root / subcommand)を理解する。

次回以降は、文字列エンコード変換ツール(例:UTF‑8 ⇄ Base64)を段階的に拡張していきます。


2. 作る予定のツール

将来的には、テキストのエンコードを変換するツールを作ります。例:

textkit b64 encode "hello"
# => aGVsbG8=

本稿ではその前段として、“挨拶するだけ” の最小 CLIを題材に cobra の基本を押さえます。

textkit hello
# => Hello, World!

3. 前提

3.1 環境

  • Windows11
  • Go 1.24
    (※Go 1.x の後方互換性の方針により、より新しい 1.x 系であれば問題ありません)

3.2 使用ライブラリ

  • コマンドライン実装に Cobra を使用します。
  • cobraを使用する理由は下記です。
    • サブコマンド階層が自然(git / kubectl 型)
    • help / version / completion などが最初から揃っている
    • コマンドを1 ファイル単位で拡張しやすく、学習コストが低い

3.3 本記事の最終フォルダ構成

go-cli-tutorial/
├── main.go
├── cmd/
│   ├── root.go
│   └── hello.go
├── go.mod
└── go.sum

4. ハンズオン

4.1. Go プロジェクトの初期化

作業用フォルダを作成し、モジュールを初期化します。公開予定なら モジュールパス は GitHub のリポジトリ URL に合わせます。

go mod init github.com/orchidee9392/go-cli-tutorial

公開予定がなければgo-cli-tutorialのようにモジュール名だけでも問題ありません。

4.2. 仮のmain.go を作る

実際にCLIアプリケーションを作成する前に1行出力するだけの簡単なプログラムを実装してみます。

main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello textkit!")
}

実行してみます。

go run .
# => Hello textkit!

ビルドする方法も試してみましょう。

# 実行ファイルを bin/textkit.exe に出力
go build -o bin/textkit.exe .
bin\textkit.exe
# => Hello textkit!

4.3. cobra の導入

プロジェクトのルートフォルダで下記コマンドを実行してcobraを導入します。

go get github.com/spf13/cobra@latest

go.sumが作成され、go.modに下記のような記述があれば成功です。

go.mod
require (
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/spf13/cobra v1.10.1 // indirect
	github.com/spf13/pflag v1.0.9 // indirect
)

4.4. 単一ファイルで最小 CLI を実装

いよいよ実際にCLIアプリケーションを実装していきます。

main.go
package main

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
)

func main() {
    //ルートコマンド
    rootCmd := &cobra.Command{
        Use:          "textkit",
        Short:        "Minimal hello CLI",
        SilenceUsage: true,
    }

    //挨拶サブコマンド
    helloCmd := &cobra.Command{
        Use:   "hello [name]",
        Short: "挨拶します",
        Args:  cobra.MaximumNArgs(1),
        Run: func(cmd *cobra.Command, args []string) {
            name := "world"
            if len(args) == 1 {
                name = args[0]
            }
            fmt.Fprintf(cmd.OutOrStdout(), "Hello, %s\n", name)
        },
    }

    //サブコマンドの追加
    rootCmd.AddCommand(helloCmd)

    //実行
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(2)
    }
}
初心者向けの丁寧なコード解説

まず、cobra ではコマンドツリーという発想で CLI を組み立てます。

  • ルートコマンド: textkit 本体(textkit --help で出る最初の画面を司る)
  • サブブコマンド: 具体的な機能(ここでは hello)。今後は b64 encode などを増やしていきます。

1) ルートコマンドを作る

rootCmd := &cobra.Command{
    Use:   "textkit",     // コマンド名や基本の使い方(先頭語がコマンド名)
    Short: "Minimal hello CLI", // --help に出る 1 行説明
    SilenceUsage: true,  // エラー時にUsageを出力しない
}
  • Use は「このコマンドをどう呼ぶか」です。textkit [flags] のように書くこともできます。
  • Shorttextkit --help の最初の行に出ます。1 行で端的に書きます。
  • SilenceUsagetrue にすると、エラー時にUsageを出力せず、エラー内容に集中できます。

2) サブコマンド hello を定義する

helloCmd := &cobra.Command{
    Use:   "hello [name]",   // 角括弧 [ ] は「省略可能」を表す(<name> は必須)
    Short: "挨拶します",
    Args:  cobra.MaximumNArgs(1), // 受け取る位置引数は最大 1 個に制限
    Run: func(cmd *cobra.Command, args []string) {
        name := "textkit"             // 引数が無いときのデフォルト
        if len(args) == 1 { name = args[0] }
        // ▼ 出力先に注目
        fmt.Fprintf(cmd.OutOrStdout(), "Hello, %s
", name)
    },
}
  • Use: "hello [name]"角括弧は「その引数はあっても無くてもよい」の意味です。
    • <>の括弧にすることで「その引数は必須」の意味になります。
  • Args: cobra.MaximumNArgs(1)は入力の規則。2 個以上の引数が来たらcobraが自動でエラー+使い方を表示します。
    Argsで使用できる型などについては次記事以降で取り扱います。
  • Run は「実際にやること」。args に位置引数が入り、cmd からは 出力先 を取得できます。
  • cmd.OutOrStdout() は「標準出力(stdout)を抽象化した Writer」。これに出力しておくと、
    テストで bytes.Buffer に差し替えらえることができ、テスト容易性が上がります。

3) サブコマンドを登録し、実行を開始する

rootCmd.AddCommand(helloCmd)           // これで `textkit hello` が有効に

if err := rootCmd.Execute(); err != nil {
    fmt.Fprintln(os.Stderr, err)       // エラーは stderr へ(stdout と混ぜない)
    os.Exit(2)                         // 失敗を終了コードで伝える
}
  • AddCommand でルート配下に hello をぶら下げます。多段(例: textkit b64 encode)も同様に増やしていきます。
  • Execute()エントリーポイント。ここで引数解析 → 該当コマンドの Run/RunE が呼ばれます。
  • RunE を使うと error を返せるので、失敗時に一貫した扱いができます(次節で採用予定)。

まとめ: Use → Args → Run → Execute の流れが分かれば、どのサブコマンドでも考え方は同じです。

実行例:

go build -o bin/textkit.exe .
bin\textkit.exe hello
# => Hello, world
bin\textkit.exe hello Gophers
# => Hello, Gophers

4.6 ファイルを分けて可読性を上げる

単一ファイルで動くことを確認したら、役割ごとに分割します。

なぜ役割ごとに分けるの?

責務分離 / 拡張しやすさ / テスト容易性 などいろんなメリットがあるためです。
小さいアプリケーションなら1ファイルでも問題ありませんが、規模が大きくなるとファイルを分けた方が管理・可読性などいろんな面でメリットが大きくなります。

解説:役割ごとに分ける理由
  • 責務分離で迷子にならない: main.go は「起動だけ」。CLI の定義は cmd/、実際の処理(業務ロジック)は internal/ に置くと、どこを触れば良いかが明確になります。
  • 拡張しやすい: サブコマンド 1 つ = ファイル 1 枚(cmd/hello.go など)にすると、あとから b64.go を足すのも簡単。PR の差分も小さくレビューが速いです。
  • テストしやすい: コマンドごとに入出力を差し替えてテストできます。例えば:
// tests/hello_test.go (抜粋)
buf := &bytes.Buffer{}
cmd := HelloCmd
cmd.SetOut(buf)
cmd.SetArgs([]string{"Gopher"})
_ = cmd.Execute()
if got := buf.String(); got != "Hello, Gopher!
" { t.Fatalf("got %q", got) }
  • ヘルプ/補完が自然に育つ: Cobra は登録済みサブコマンドからヘルプを自動生成。ファイル分割しておくと説明文の管理もしやすいです。
  • 依存の向きが健全: cmdinternal の一方向。internal は CLI に依存しないので 再利用 も可能です。

目安: サブコマンドが 2 つ以上 or 1 ファイルが ~150 行を超えたら分割を検討すると、将来の保守が楽になります。

ファイル構造は下記のようにします。

go-cli-tutorial
├── cmd/
│   ├── root.go     # ルートコマンドの定義 / サブコマンド登録
│   └── hello.go    # サブコマンド(hello)の定義
└── main.go         # 実行部だけ

cmd/hello.go

hello.go
package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
)

// 挨拶メソッド
func runHello(cmd *cobra.Command, args []string) error {
    name := "textkit"
    if len(args) == 1 {
        name = args[0]
    }
    fmt.Fprintf(cmd.OutOrStdout(), "Hello, %s!\n", name)
    return nil
}

// 挨拶サブコマンド
var HelloCmd = &cobra.Command{
    Use:   "hello [name]",
    Short: "挨拶します",
    Args:  cobra.MaximumNArgs(1),
    RunE:  runHello,
}

ここでは、前回の実装と違い、RunプロパティではなくRunEを使用し、中身も別関数に切り出しています。RunERunとほぼ同じですが、戻り値としてエラーを返せるようになります。

cmd/root.go

root.go
package cmd

import "github.com/spf13/cobra"

//ルートコマンド
var RootCmd = &cobra.Command{
    Use:   "textkit",
    Short: "Minimal hello CLI",
}

//サブコマンドの登録
func init() {
    RootCmd.AddCommand(HelloCmd)
}

main.go

main.go
package main

import (
    "fmt"
    "os"
    "github.com/orchidee9392/go-cli-tutorial/cmd"
)

func main() {
    if err := cmd.RootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(2)
    }
}

ビルド & 実行:

go build -o bin/textkit.exe .
bin\textkit.exe hello
# => Hello, textkit!

付録:cobra.Command よく使うプロパティ(抜粋)

付録を開く(Cobra のプロパティ一覧)
区分 フィールド(型) 役割・使いどころ
識別/ヘルプ Use string 1 行の使用法。先頭語がコマンド名のマッチキー。例: add [-f] <name>
Aliases []string 同義の別名(完全一致)。
Short string / Long string --help 用の短文/長文説明。
Example string 使い方例。
実行 Run(E) / PreRun(E) / PostRun(E) 本体/前後処理。未設定だと Pre/PostRun は実行されない点に注意。
引数 Args PositionalArgs 位置引数のバリデータ(例:cobra.ExactArgs(1) など)。
補完 ValidArgsFunction 動的補完関数。ValidArgs と同時併用不可。
表示制御 Hidden / SilenceErrors / SilenceUsage ヘルプ非表示やエラー時の静音化。
その他 Version 値を入れると --version が自動追加。

公式ドキュメント: https://pkg.go.dev/github.com/spf13/cobra


まとめ

  • cobraCobra.Commandを入れ子にしてCLIアプリケーションを実装する。
  • まずは 1 ファイルで動かす → 役割ごとに分割 の流れを掴む。
  • cmd.OutOrStdout() を使ってテストしやすい出力する。

次回は、ここで作った骨格にエンコード変換コマンド(Base64 など)を足していきます。

Discussion