Go: Go CLI 工具开发实战

最后更新:2026-08-26

build CLI 工具是 Go 的强项——编译为单个二进制、跨平台、零依赖。用 Go 写 CLI 已经成为了云原生时代的标配。

dockerkubectlgh,主流 CLI 工具几乎都用 Go 编写。这节课你将掌握 CLI 开发的全部核心技能,并构建一个完整的密码管理器。

1. 你将学到


2. 一个运维工程师的真实故事

(1) 痛点:每次都手动生成密码

Bob 是运维工程师,他经常需要为各种服务生成复杂密码:

"今天给 PostgreSQL 生成密码,明天给 Redis,后天给管理员账号。每次我都打开密码生成网站,复制粘贴到 1Password——一天搞 5 次,烦死了。而且不同网站要求不同规则:12 位含特殊字符、16 位无歧义字符……"

Bob 想找一个像这样的 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 包 + 文件 IO

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) 收益:手动 vs CLI

方式 速度 可重复 集成 CI/CD 安全性
网站生成 30s/次 ❌(隐私风险)
手动创建 60s/次 ✅ 可复制
Go CLI 0.5s/次 ✅ 可脚本化 ✅ 可集成 ✅ 本地运行
💡 提示: Go 一个二进制 ≈ 10MB(不依赖 runtime),go build 后直接拷贝到任何 Linux/Mac/Windows 服务器就能跑。这是 Go 做 CLI 的核心优势。


3. flag 包:命令行参数解析

(1) flag 基础

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

▶ 示例:flag 短参数 + 非标志参数

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 两种形式
    // 支持 -o=file 和 -o file 两种写法
}
▶ 试一试
BASH
$ go run main.go -o result.txt -n 100 data.csv
处理文件: data.csv
输出: result.txt
行数: 100
debug: 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, "第二行")
}
▶ 试一试

(3) 输入源对比

输入方式 命令示例 适用场景
命令行参数 mytool --name Alice 配置选项、文件名
管道 stdin cat data | mytool 大量数据、链式处理
交互输入 mytool 等待用户输入 需要引导的输入
环境变量 MYTOOL_DEBUG=1 mytool 敏感配置

5. cobra 库:子命令模式

(1) cobra 简介

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
package 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) flag vs cobra

维度 flag 包 cobra
内置 ✅ 标准库 ❌ 第三方
子命令 ❌ 不支持 ✅ 原生支持
自动帮助 ✅ 基础 ✅ 美观
自动补全 ✅ bash/zsh/fish
适用场景 简单脚本 复杂多命令工具

6. 完整示例:密码管理器 CLI

把所有知识整合为一个完整的密码管理器:

GO
// main.go
package main

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

// ---------- 数据结构 ----------

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

type PasswordStore struct {
    entries map[string]PasswordEntry
    filePath string
}

const defaultStoreFile = "passwords.json"

// ---------- 存储层 ----------

func NewPasswordStore(filePath string) *PasswordStore {
    store := &PasswordStore{
        entries:  make(map[string]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() error {
    data, err := json.MarshalIndent(s.entries, "", "  ")
    if err != nil {
        return err
    }
    return os.WriteFile(s.filePath, data, 0600)
}

func (s *PasswordStore) Add(name, password string) {
    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 string) (PasswordEntry, bool) {
    entry, ok := s.entries[name]
    return entry, ok
}

func (s *PasswordStore) Delete(name string) bool {
    _, ok := s.entries[name]
    if ok {
        delete(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) (string, error) {
    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 string(pwd), nil
}

// ---------- CLI 命令 ----------

func cmdGenerate(args []string) {
    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 []string, 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 error
        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 []string, 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 []string, 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 []string, store *PasswordStore) {
    if len(args) < 1 {
        fmt.Fprintln(os.Stderr, "用法: passgen delete <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 []string, 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        列出所有密码
  delete      删除密码
  export      导出密码到文件

选项:
  --store     指定存储文件路径(默认: passwords.json)

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

func main() {
    // 全局 --store parameter
    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 parameter
                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 "delete":
        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 集。flag.FlagSet.Parse 只解析自己的参数,不会动 os.Args。主命令用 os.Args 手动分派。


❓ 常见问题

Q flag 和 cobra 怎么选?
A 简单脚本(1 个命令、几个参数)→ flag;多命令工具(git-style:tool <command> [options])→ cobra。flag 是标准库,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 文件 IO 在 CLI 中如何用?
A 三个模式:(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 密码生成器如何保证安全性?
Acrypto/rand 而非 math/rand——crypto/rand 使用操作系统 CSPRNG(如 /dev/urandom),不可预测。math/rand 是伪随机,不适合密码学用途。
Q CLI 工具如何做简单的交互输入?
Abufio.NewScanner(os.Stdin) 逐行读取,或 fmt.Scanf 读取格式化输入。生产工具建议 gopass 库(不回显密码)。
Q 如何处理 CLI 的错误信息输出?
A 错误信息应该输出到 os.Stderr(而非 os.Stdout),这样 mytool 2>/dev/null 才能静默。用 fmt.Fprintln(os.Stderr, "error")

📖 小节


📝 作业

  1. 基础题(难度⭐):用 flag 包写一个 wc 克隆:mywc 支持 --lines --words --chars 三个 flag,从 stdin 或文件读取,输出统计结果。

  2. 进阶题(难度⭐⭐):为本课的密码管理器添加 --store 环境变量支持:如果设置了 PASSGEN_STORE 环境变量,优先使用它作为存储路径,否则用 --store 参数,最后才用默认路径。

  3. 挑战题(难度⭐⭐⭐):为密码管理器添加一个 stats 命令:统计总密码数、最长/最短密码、最常用字符、按创建时间分组统计(今天/本周/本月/更早)。要求用 sort.Slice + strings.Count + time.Time

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏