Go: Go CLI 工具开发实战
最后更新:2026-08-26
build CLI 工具是 Go 的强项——编译为单个二进制、跨平台、零依赖。用 Go 写 CLI 已经成为了云原生时代的标配。
从 docker 到 kubectl 到 gh,主流 CLI 工具几乎都用 Go 编写。这节课你将掌握 CLI 开发的全部核心技能,并构建一个完整的密码管理器。
1. 你将学到
flag包解析命令行参数os.Args原始参数访问os.Stdin/os.Stdout标准流- cobra 库与子命令模式
- CLI 与文件 IO 整合
- 项目:密码管理器 CLI(生成/存储/检索/导出密码)
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
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 密码生成器如何保证安全性?
A 用
crypto/rand 而非 math/rand——crypto/rand 使用操作系统 CSPRNG(如 /dev/urandom),不可预测。math/rand 是伪随机,不适合密码学用途。Q CLI 工具如何做简单的交互输入?
A 用
bufio.NewScanner(os.Stdin) 逐行读取,或 fmt.Scanf 读取格式化输入。生产工具建议 gopass 库(不回显密码)。Q 如何处理 CLI 的错误信息输出?
A 错误信息应该输出到
os.Stderr(而非 os.Stdout),这样 mytool 2>/dev/null 才能静默。用 fmt.Fprintln(os.Stderr, "error")。📖 小节
flag包解析命令行参数:flag.String/flag.Int/flag.Boolos.Args访问原始参数,灵活手动解析os.Stdin/os.Stdout/os.Stderr标准 I/O 流flag.NewFlagSet实现子命令风格的 CLI- cobra 库提供完整的子命令框架
crypto/rand生成密码学安全的随机数- CLI 工具的数据持久化:JSON 文件 + map 内存缓存
go build编译为单二进制,跨平台发布
📝 作业
-
基础题(难度⭐):用
flag包写一个wc克隆:mywc支持--lines--words--chars三个 flag,从 stdin 或文件读取,输出统计结果。 -
进阶题(难度⭐⭐):为本课的密码管理器添加
--store环境变量支持:如果设置了PASSGEN_STORE环境变量,优先使用它作为存储路径,否则用--store参数,最后才用默认路径。 -
挑战题(难度⭐⭐⭐):为密码管理器添加一个
stats命令:统计总密码数、最长/最短密码、最常用字符、按创建时间分组统计(今天/本周/本月/更早)。要求用sort.Slice+strings.Count+time.Time。