実践編

CLIツールの構築はGoの強みのひとつです。単一のバイナリにコンパイルされ、クロスプラットフォームに対応しており、依存関係も一切ありません。クラウドネイティブ時代において、GoでCLIを作成することは標準となっています。

docker から kubectl、そして gh に至るまで、主流の CLI ツールのほぼすべてが Go 言語で記述されています。このレッスンでは、CLI 開発のコアスキルをすべて習得し、完全なパスワードマネージャーをビルドします。

1. 学習内容


2. 運用エンジニアの実話

(1) 課題:毎回手動でパスワードを生成しなければならないこと

ボブは運用エンジニアで、さまざまなサービス用に複雑なパスワードを頻繁に生成する必要があります:

「今日はPostgreSQL用のパスワードを生成し、明日はRedis用、明後日は管理者アカウント用。そのたびにパスワード生成サイトを開いて、1Passwordにコピー&ペーストしなければならない――1日に5回も。本当に面倒だ。それに、サイトごとに要件が異なるんだ。『12文字で特殊文字を含む』とか、『16文字で曖昧な文字を含まない』とか……」

ボブは、次のようなCLIツールを探しています:

BASH
# 欲しいもの CLI
$ passgen generate --length 16 --special
Generated: Kd9#mP2$xL7qR!vB

$ passgen save mydb -p Kd9#mP2$xL7qR!vB
Saved: mydb

$ passgen list
mydb    (created: 2026-07-08)
admin   (created: 2026-07-05)
api-key (created: 2026-07-01)

$ passgen get mydb
mydb: Kd9#mP2$xL7qR!vB

しかし、彼のニーズを完全に満たすツールは市場には存在しなかった。そこで彼は、Goを使って自分でツールを作成することにした。

(2) Goの解決策:flagパッケージ + ファイルI/O

GO
// passgen.go — 初版:基本パスワードの生成
package main

import (
    "crypto/rand"
    "flag"
    "fmt"
    "math/big"
)

var (
    length  = flag.Int("length", 12, "パスワードの長さ")
    special = flag.Bool("special", false, "特殊文字を含む")
    count   = flag.Int("count", 1, "生成数")
)

const (
    lowerChars   = "abcdefghijklmnopqrstuvwxyz"
    upperChars   = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    digitChars   = "0123456789"
    specialChars = "!@#$%^&*()-_=+[]{}|;:,.<>?"
)

func generatePassword(length int, includeSpecial bool) string {
    charset := lowerChars + upperChars + digitChars
    if includeSpecial {
        charset += specialChars
    }

    pwd := make([]byte, length)
    for i := range pwd {
        n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
        pwd[i] = charset[n.Int64()]
    }
    return string(pwd)
}

func main() {
    flag.Parse()

    for i := 0; i < *count; i++ {
        fmt.Println(generatePassword(*length, *special))
    }
}
BASH
$ go run passgen.go --length 16 --special
Kd9#mP2$xL7qR!vB

$ go run passgen.go --length 8
aB3xK9mP

(3) パフォーマンス:マニュアル操作とCLIの比較

手法 速度 再現性 CI/CD との連携 セキュリティ
ウェブサイトの生成 1回あたり30秒 ❌ (プライバシーリスク)
手動で作成 インスタンスあたり60秒 ✅ 複製可能
Go CLI 1操作あたり0.5秒 ✅ スクリプト化可能 ✅ 統合可能 ✅ ローカルで実行可能
💡 ヒント: Goのバイナリファイルのサイズは約10 MB(ランタイムを除く)です。go buildを実行した後、そのファイルをLinux、Mac、またはWindowsのサーバーにコピーするだけで実行できます。これが、CLIアプリケーションにGoを使用する最大の利点です。


3. flag パッケージ:コマンドライン引数の解析

(1) 旗の基本

GO
package main

import (
    "flag"
    "fmt"
)

func main() {
    // 登録パラメータ(ポインタに戻る)
    name := flag.String("name", "World", "挨拶する相手の名前")
    age := flag.Int("age", 0, "年齢")
    verbose := flag.Bool("verbose", false, "詳細な出力")

    // 分析(読み込み前に必ず呼び出す必要がある)
    flag.Parse()

    if *verbose {
        fmt.Printf("name=%s, age=%d\n", *name, *age)
    }
    fmt.Printf("Hello, %s!\n", *name)
}
BASH
$ go run main.go --name Alice --age 28 --verbose
name=Alice, age=28
Hello, Alice!

$ go run main.go
Hello, World!

(2) flag タイプのクイックリファレンス

関数
flag.String(name, default, usage) *string --name Alice
flag.Int(name, default, usage) *int --port 8080
flag.Float64(name, default, usage) *float64 --ratio 0.5
flag.Bool(name, default, usage) *bool --verbose
flag.Duration(name, default, usage) *time.Duration --timeout 5s

▶ サンプル:フラグ付きパラメータ + フラグなしパラメータ

GO
package main

import (
    "flag"
    "fmt"
    "os"
)

func main() {
    // カスタマイズ Usage 情報
    flag.Usage = func() {
        fmt.Fprintf(os.Stderr, "使用方法: mytool [オプション] <ファイルのパス>\n\n")
        fmt.Fprintf(os.Stderr, "オプション:\n")
        flag.PrintDefaults()
    }

    // 登録パラメータ
    output := flag.String("o", "output.txt", "出力ファイルのパス")
    limit := flag.Int("n", 0, "処理行数(0=すべて)")
    debug := flag.Bool("debug", false, "デバッグモードを有効にする")

    // 分析
    flag.Parse()

    // 非フラグ引数へのアクセス(残りのパラメータ)
    args := flag.Args()
    if len(args) < 1 {
        flag.Usage()
        os.Exit(1)
    }

    filePath := args[0]
    fmt.Printf("ファイルの処理: %s\n", filePath)
    fmt.Printf("出力: %s\n", *output)
    fmt.Printf("行数: %d\n", *limit)
    fmt.Printf("デバッグ: %v\n", *debug)

    // 支持 -o と --o 2つの形式
    // 支持 -o=file と -o file 2つの書き方
}
▶ 試してみよう
BASH
$ go run main.go -o result.txt -n 100 data.csv
ファイルの処理: data.csv
出力: result.txt
行数: 100
デバッグ: false

4. os.Args および os.Stdin/Stdout

(1) os.Args: 生の引数

GO
package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Printf("プログラム名: %s\n", os.Args[0])
    fmt.Printf("パラメータ数: %d\n", len(os.Args)-1)

    for i, arg := range os.Args[1:] {
        fmt.Printf("パラメータ %d: %s\n", i+1, arg)
    }
}
BASH
$ go run main.go hello world --debug
プログラム名: /tmp/main
パラメータ数: 3
パラメータ 1: hello
パラメータ 2: world
パラメータ 3: --debug

▶ サンプル:flag 変数(IntVar / StringVar)のバインディング

GO
package main

import (
    "flag"
    "fmt"
)

func main() {
    // flag.Int ポインタに戻る vs flag.IntVar 既存の変数にバインドする
    var (
        name   string
        port   int
        debug  bool
    )

    flag.StringVar(&name, "name", "World", "氏名")
    flag.IntVar(&port, "port", 8080, "ポート")
    flag.BoolVar(&debug, "debug", false, "デバッグモード")

    flag.Parse()

    fmt.Printf("name=%s, port=%d, debug=%v\n", name, port, debug)
}
▶ 試してみよう
💡 ヒント: flag.IntVar(&dest, ...) は既存の変数にバインドされますが、flag.Int(...) はポインタを返します。両者は完全に同等ですので、好みのスタイルを選んでください。IntVar は、一箇所で変数を宣言する場合に適しています。

(2) os.Stdin: パイプ入力

GO
package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    stat, _ := os.Stdin.Stat()
    hasStdin := (stat.Mode() & os.ModeCharDevice) == 0

    if hasStdin {
        // パイプから読み込む(echo "hello" | go run main.go)
        scanner := bufio.NewScanner(os.Stdin)
        for scanner.Scan() {
            line := strings.ToUpper(scanner.Text())
            fmt.Println(line)
        }
    } else {
        // パラメータからの読み取り(go run main.go hello)
        if len(os.Args) > 1 {
            fmt.Println(strings.ToUpper(strings.Join(os.Args[1:], " ")))
        } else {
            fmt.Println("入力してください(パイプラインまたはパラメータ)")
        }
    }
}
BASH
$ echo "hello world" | go run main.go
HELLO WORLD

$ go run main.go hello world
HELLO WORLD

▶ サンプル:対話型入力 (fmt.Scanf)

GO
package main

import "fmt"

func main() {
    var name string
    var age int

    fmt.Print("お名前を入力してください: ")
    fmt.Scanf("%s", &name)

    fmt.Print("年齢を入力してください: ")
    fmt.Scanf("%d", &age)

    fmt.Printf("こんにちは %s,あなた %d 歳\n", name, age)
}
▶ 試してみよう

▶ サンプル:os.Stdout のリダイレクト

GO
package main

import (
    "fmt"
    "os"
)

func main() {
    // 直接書き込み stdout
    fmt.Fprintln(os.Stdout, "標準出力")
    fmt.Fprintln(os.Stderr, "エラー出力")

    // ファイルへのリダイレクト
    file, _ := os.Create("output.log")
    defer file.Close()

    // fmt.Fprintln 何でも書き込むことができます io.Writer
    fmt.Fprintln(file, "これがファイルに書き込まれました")
    fmt.Fprintln(file, "2行目")
}
▶ 試してみよう

(3) 入力ソースの比較

入力方式 コマンド例 使用例
コマンドライン引数 mytool --name Alice 設定オプション、ファイル名
stdin パイプ cat data | mytool 大量のデータ、連鎖処理
対話型入力 mytool はユーザーからの入力を待機しています ガイダンスが必要な入力
環境変数 MYTOOL_DEBUG=1 mytool 機密設定

5. cobra ライブラリ:サブコマンドモード

(1) コブラの概要

cobraは、Go言語で最も人気のあるCLIフレームワーク(Kubernetesの開発者が作成)であり、サブコマンド、ヘルプメッセージ、オートコンプリートをサポートしています。

BASH
# インストール cobra-cli
$ go install github.com/spf13/cobra-cli@latest

# プロジェクトの初期化
$ cobra-cli init passgen
$ cobra-cli add generate
$ cobra-cli add save
$ cobra-cli add list

▶ サンプル:「cobra」サブコマンドの構造

GO
// cmd/root.go
パッケージ cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
    Use:   "passgen",
    Short: "パスワードマネージャー",
    Long:  `生成、パスワードの保存と管理 CLI ツール。`,
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
▶ 試してみよう
GO
// cmd/generate.go
package cmd

import (
    "fmt"
    "crypto/rand"
    "math/big"

    "github.com/spf13/cobra"
)

var (
    genLength  int
    genSpecial bool
    genCount   int
)

var generateCmd = &cobra.Command{
    Use:   "generate",
    Short: "ランダムなパスワードを生成する",
    Long:  `指定された長さと複雑さを持つランダムなパスワードを生成する。`,
    Run: func(cmd *cobra.Command, args []string) {
        for i := 0; i < genCount; i++ {
            fmt.Println(generatePassword(genLength, genSpecial))
        }
    },
}

func init() {
    rootCmd.AddCommand(generateCmd)
    generateCmd.Flags().IntVarP(&genLength, "length", "l", 12, "パスワードの長さ")
    generateCmd.Flags().BoolVarP(&genSpecial, "special", "s", false, "特殊文字を含む")
    generateCmd.Flags().IntVarP(&genCount, "count", "c", 1, "生成数")
}

func generatePassword(length int, special bool) string {
    charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    if special {
        charset += "!@#$%^&*()-_=+[]{}|;:,.<>?"
    }
    pwd := make([]byte, length)
    for i := range pwd {
        n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
        pwd[i] = charset[n.Int64()]
    }
    return string(pwd)
}
GO
// main.go
package main

import "passgen/cmd"

func main() {
    cmd.Execute()
}
BASH
$ go build -o passgen
$ ./passgen generate --length 16 --special
Kd9#mP2$xL7qR!vB
$ ./passgen generate -l 8
aB3xK9mP

(2) フラッグ対コブラ

ディメンション フラグパッケージ コブラ
組み込み ✅ 標準ライブラリ ❌ サードパーティ製
サブコマンド ❌ 未対応 ✅ ネイティブ対応
自動車のトラブルシューティング ✅ 基礎知識 ✅ 外観
オートコンプリート ✅ bash/zsh/fish
ユースケース 簡単なスクリプト 複雑なマルチコマンドツール

6. 完全な例:パスワードマネージャーのCLI

これらすべての知識を統合して、包括的なパスワードマネージャーを作りましょう:

GO
// main.go
パッケージ main

import (
    "bufio"
    "crypto/rand"
    "encoding/json"
    "flag"
    "fmt"
    "math/big"
    "os"
    "strings"
    "time"
)

// ---------- データ構造 ----------

type PasswordEntry struct {
    Name      文字列    `json:"name"`
    Password  文字列    `json:"password"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at,omitempty"`
}

type PasswordStore struct {
    entries map[文字列]PasswordEntry
    filePath 文字列
}

const defaultStoreFile = "passwords.json"

// ---------- ストレージ層 ----------

func NewPasswordStore(filePath 文字列) *PasswordStore {
    store := &PasswordStore{
        entries:  make(map[文字列]PasswordEntry),
        filePath: filePath,
    }
    store.load()
    return store
}

func (s *PasswordStore) load() {
    data, err := os.ReadFile(s.filePath)
    if err != nil {
        return
    }
    json.Unmarshal(data, &s.entries)
}

func (s *PasswordStore) save() エラー {
    data, err := json.MarshalIndent(s.entries, "", "  ")
    if err != nil {
        return err
    }
    return os.WriteFile(s.filePath, data, 0600)
}

func (s *PasswordStore) Add(name, password 文字列) {
    now := time.Now()
    if existing, ok := s.entries[name]; ok {
        existing.Password = password
        existing.UpdatedAt = now
        s.entries[name] = existing
    } else {
        s.entries[name] = PasswordEntry{
            Name:      name,
            Password:  password,
            CreatedAt: now,
        }
    }
    s.save()
}

func (s *PasswordStore) Get(name 文字列) (PasswordEntry, bool) {
    entry, ok := s.entries[name]
    return entry, ok
}

func (s *PasswordStore) Delete(name 文字列) bool {
    _, ok := s.entries[name]
    if ok {
        削除(s.entries, name)
        s.save()
    }
    return ok
}

func (s *PasswordStore) List() []PasswordEntry {
    result := make([]PasswordEntry, 0, len(s.entries))
    for _, entry := range s.entries {
        result = append(result, entry)
    }
    return result
}

// ---------- パスワードの生成 ----------

const (
    letters   = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    digits    = "0123456789"
    specials  = "!@#$%^&*()-_=+[]{}|;:,.<>?"
    noAmbigu  = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"
)

func generatePassword(length int, useSpecial bool) (文字列, エラー) {
    if length < 4 {
        return "", fmt.Errorf("password length must be at least 4")
    }

    charset := letters + digits
    if useSpecial {
        charset += specials
    }

    pwd := make([]byte, length)
    for i := range pwd {
        n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
        if err != nil {
            return "", err
        }
        pwd[i] = charset[n.Int64()]
    }
    return 文字列(pwd), nil
}

// ---------- CLI コマンド ----------

func cmdGenerate(args []文字列) {
    cmd := flag.NewFlagSet("generate", flag.ExitOnError)
    length := cmd.Int("length", 12, "パスワードの長さ")
    special := cmd.Bool("special", false, "特殊文字を含む")
    count := cmd.Int("count", 1, "生成数")
    noAmbiguous := cmd.Bool("no-ambiguous", false, "混同されやすい文字を除外する(0OIl1)")
    cmd.Parse(args)

    for i := 0; i < *count; i++ {
        pwd, err := generatePassword(*length, *special)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Error: %v\n", err)
            os.Exit(1)
        }
        fmt.Println(pwd)
    }
}

func cmdSave(args []文字列, store *PasswordStore) {
    cmd := flag.NewFlagSet("save", flag.ExitOnError)
    password := cmd.String("password", "", "パスワード(空欄のままにすると自動的に生成されます)")
    length := cmd.Int("length", 16, "自動生成時のパスワードの長さ")
    cmd.Parse(args)

    name := cmd.Arg(0)
    if name == "" {
        fmt.Fprintln(os.Stderr, "使用方法: passgen save <name> [--password <pwd>]")
        os.Exit(1)
    }

    pwd := *password
    if pwd == "" {
        var err エラー
        pwd, err = generatePassword(*length, true)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Error: %v\n", err)
            os.Exit(1)
        }
    }

    store.Add(name, pwd)
    fmt.Printf("✅ 保存済み: %s = %s\n", name, pwd)
}

func cmdGet(args []文字列, store *PasswordStore) {
    if len(args) < 1 {
        fmt.Fprintln(os.Stderr, "使用方法: passgen get <name>")
        os.Exit(1)
    }

    entry, ok := store.Get(args[0])
    if !ok {
        fmt.Fprintf(os.Stderr, "❌ 見つかりませんでした: %s\n", args[0])
        os.Exit(1)
    }
    fmt.Printf("%s: %s\n", entry.Name, entry.Password)
}

func cmdList(args []文字列, store *PasswordStore) {
    entries := store.List()
    if len(entries) == 0 {
        fmt.Println("(保存済みのパスワードはありません)")
        return
    }

    fmt.Printf("%-20s %-20s %s\n", "名称", "パスワード", "作成日時")
    fmt.Println(strings.Repeat("-", 60))
    for _, e := range entries {
        pwd := e.Password
        if len(pwd) > 16 {
            pwd = pwd[:16] + "..."
        }
        fmt.Printf("%-20s %-20s %s\n",
            e.Name, pwd, e.CreatedAt.Format("2006-01-02"))
    }
}

func cmdDelete(args []文字列, store *PasswordStore) {
    if len(args) < 1 {
        fmt.Fprintln(os.Stderr, "使用方法: passgen 削除 <name>")
        os.Exit(1)
    }

    if store.Delete(args[0]) {
        fmt.Printf("🗑️ 削除済み: %s\n", args[0])
    } else {
        fmt.Fprintf(os.Stderr, "❌ 見つかりませんでした: %s\n", args[0])
    }
}

func cmdExport(args []文字列, store *PasswordStore) {
    output := "passwords_export.json"
    if len(args) > 0 {
        output = args[0]
    }

    data, _ := json.MarshalIndent(store.List(), "", "  ")
    os.WriteFile(output, data, 0600)
    fmt.Printf("📤 にエクスポート済み: %s\n", output)
}

// ---------- メイン関数 ----------

func printUsage() {
    fmt.Println(`パスワードマネージャー CLI

使用方法:
  passgen <command> [オプション]

コマンド:
  generate    ランダムなパスワードを生成する
  save        パスワードを保存する
  get         パスワードを取得する
  list        すべてのパスワードを表示する
  削除      パスワードを削除する
  export      パスワードをファイルにエクスポートする

オプション:
  --store     ファイルの保存先を指定する(デフォルト: passwords.json)

例:
  passgen generate --length 16 --special
  passgen save mydb --length 20
  passgen get mydb
  passgen list
  passgen 削除 mydb`)
}

func main() {
    // 全体 --store パラメータ
    storeFile := defaultStoreFile
    if len(os.Args) > 1 {
        for i := 1; i < len(os.Args); i++ {
            if os.Args[i] == "--store" && i+1 < len(os.Args) {
                storeFile = os.Args[i+1]
                // 削除 store パラメータ
                os.Args = append(os.Args[:i], os.Args[i+2:]...)
                break
            }
        }
    }

    store := NewPasswordStore(storeFile)

    if len(os.Args) < 2 {
        printUsage()
        return
    }

    command := os.Args[1]
    args := os.Args[2:]

    switch command {
    case "generate":
        cmdGenerate(args)
    case "save":
        cmdSave(args, store)
    case "get":
        cmdGet(args, store)
    case "list":
        cmdList(args, store)
    case "削除":
        cmdDelete(args, store)
    case "export":
        cmdExport(args, store)
    case "help", "--help", "-h":
        printUsage()
    default:
        fmt.Fprintf(os.Stderr, "不明なコマンド: %s\n", command)
        fmt.Fprintf(os.Stderr, "実行 'passgen help' 利用可能なコマンドを表示する\n")
        os.Exit(1)
    }
}

使用目的:

BASH
# パスワードを生成する
$ go run main.go generate --length 16 --special
Kd9#mP2$xL7qR!vB

# パスワードを保存する(自動生成)
$ go run main.go save mydb --length 20
✅ 保存済み: mydb = aB3xK9mP$L7qR!vD2nFg

# パスワードを保存する(手動で指定する)
$ go run main.go save admin --password "MyStr0ng!Pass"
✅ 保存済み: admin = MyStr0ng!Pass

# パスワードを取得する
$ go run main.go get mydb
mydb: aB3xK9mP$L7qR!vD2nFg

# 一覧表示
$ go run main.go list
名称                 パスワード                  作成日時
------------------------------------------------------------
mydb                 aB3xK9mP$L7qR!v...   2026-07-08
admin                MyStr0ng!Pass         2026-07-08

# エクスポート
$ go run main.go export
📤 にエクスポート済み: passwords_export.json

# 保存先を指定する
$ go run main.go --store vault.json generate -l 10
100%
flowchart TD
    A[passgen <command>] --> B{switch command}
    B -->|generate| C[flag.NewFlagSet 分析<br/>--length --special --count]
    B -->|save| D[分析 name + --password<br/>パスワードの自動生成または指定]
    B -->|get| E[クリック name から map 検索]
    B -->|list| F[走査 map 表を出力する]
    B -->|delete| G[delete(map, name)]
    B -->|export| H[json.Marshal + WriteFile]
    C --> I[rand.Int 安全なランダム]
    D --> J[store.Add → json → save]
    E --> K[store.Get → 出力]
    F --> L[store.List → フォーマット]
    G --> M[store.Delete → json → save]
    H --> N[store.List → json.Marshal]
🔥 よくある間違い: flag.NewFlagSet はサブコマンドに使用され、各サブコマンドには独自のフラグセットがあります。flag.FlagSet.Parse は自身の引数のみを解析し、os.Args を変更することはありません。メインコマンドは、os.Args を使用して手動で値を割り当てる必要があります。


❓ よくある質問

Q flagcobra のどちらを選べばいいですか?
A 単純なスクリプト(1つのコマンド、パラメータが数個)→ flag;複数コマンドのツール(gitスタイル:tool <command> [options])→ cobraflagは標準ライブラリの一部ですが、cobraはより多くの機能を提供する一方で、依存関係の追加が必要となります。
Q サブコマンドはどのように解析すればよいですか?
A 手動での方法:os.Args[1] を使用してコマンドを識別し、flag.NewFlagSet("sub", ...) を使用してサブコマンドの引数を解析します。Cobra での方法:rootCmd.AddCommand(subCmd) を使用すると、フレームワークが自動的に処理します。
Q os.Args をどのように反復処理すればよいですか?
A os.Args[0] はプログラムのパス、os.Args[1:] は引数です。これらを反復処理するには、os.Args[i] または range os.Args[1:] を使用します。
Q CLI ではファイル I/O はどのように使用されますか?
A 3 つのモードがあります:(1) 固定パス (~/.config/passgen.json) への保存、(2) --store パラメータを使用したパスの指定、(3) 環境変数(PASSGEN_STORE など)。
Q CLIをGitHubに公開するにはどうすればよいですか?
A (1) go build -o mytoolでコンパイルします;(2) GitHub Releasesにアップロードします;(3) go install github.com/user/mytool@latestを使用してインストールします。Goの静的バイナリを使えば、公開作業が非常に簡単になります。
Q パスワード生成ツールはどのようにセキュリティを確保しているのですか?
A math/rand の代わりに crypto/rand を使用してください。—crypto/rand は、オペレーティングシステムの CSPRNG(/dev/urandom など)を使用しており、その出力は予測不可能です。math/rand は擬似乱数であり、暗号用途には適していません。
Q CLIツールで簡単なユーザー入力を処理するにはどうすればよいですか?
A bufio.NewScanner(os.Stdin) を使用すると行単位で読み取ることができ、fmt.Scanf を使用するとフォーマットされた入力を読み取ることができます。本番環境向けのツールには、gopass ライブラリ(パスワードをエコーしない)の使用をお勧めします。
Q CLI ではエラーメッセージをどのように扱うべきですか?
A mytool 2>/dev/null がサイレントモードで実行されるように、エラーメッセージは os.Stderros.Stdout ではなく)に出力する必要があります。fmt.Fprintln(os.Stderr, "error") を使用してください。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐)flag パッケージを使用して、wc のクローンを作成してください。mywc は、--lines--words--chars の 3 つのフラグをサポートし、標準入力またはファイルから入力を読み取り、統計情報を出力します。

  2. 上級演習(難易度 ⭐⭐):このレッスンで取り上げるパスワードマネージャーに、--store 環境変数のサポートを追加してください。PASSGEN_STORE 環境変数が設定されている場合は、まずそれを保存パスとして使用し、設定されていない場合は --store パラメータを使用し、最後にデフォルトのパスを使用するようにしてください。

  3. 課題(難易度:⭐⭐⭐):パスワードマネージャーに stats コマンドを追加する。パスワードの総数、最長および最短のパスワード、最も頻繁に使用されている文字、および作成日時(今日/今週/今月/それ以前)ごとに分類した統計情報を集計する。sort.Slice + strings.Count + time.Time を必ず使用する必要があります。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%