4. 用 os.Args 与 flag 对比实现子命令分发

用 os.Args 与 flag 对比实现子命令分发 学习卡片

任务管理器命令设计

本案例实现一个简易任务管理器,支持三个子命令:add 添加任务、list 列出任务、remove 删除任务。任务数据保存在内存中,程序每次运行独立,不涉及持久化。重点在于对比两种子命令分发方式:手动解析 os.Args 与使用 flag 包为每个子命令建立独立的 FlagSet。

方式一:手动解析 os.Args

os.Args[0] 是程序名,os.Args[1] 是子命令,后续参数按位置约定解析。这种方式完全由代码控制,适合参数少、格式固定的场景。

package main

import (
	"fmt"
	"os"
	"strconv"
)

type Task struct {
	ID   int
	Name string
}

var tasks []Task
var nextID = 1

func main() {
	if len(os.Args) < 2 {
		usage()
		return
	}

	switch os.Args[1] {
	case "add":
		if len(os.Args) < 3 {
			fmt.Println("用法: task add <任务名>")
			return
		}
		name := os.Args[2]
		tasks = append(tasks, Task{ID: nextID, Name: name})
		fmt.Printf("已添加任务 #%d: %s\n", nextID, name)
		nextID++
	case "list":
		if len(tasks) == 0 {
			fmt.Println("暂无任务")
			return
		}
		for _, t := range tasks {
			fmt.Printf("#%d  %s\n", t.ID, t.Name)
		}
	case "remove":
		if len(os.Args) < 3 {
			fmt.Println("用法: task remove <任务ID>")
			return
		}
		id, err := strconv.Atoi(os.Args[2])
		if err != nil {
			fmt.Println("任务ID必须是整数")
			return
		}
		found := false
		for i, t := range tasks {
			if t.ID == id {
				tasks = append(tasks[:i], tasks[i+1:]...)
				fmt.Printf("已删除任务 #%d\n", id)
				found = true
				break
			}
		}
		if !found {
			fmt.Printf("未找到任务 #%d\n", id)
		}
	default:
		usage()
	}
}

func usage() {
	fmt.Println("用法: task <add|list|remove> [参数]")
	fmt.Println("  add <任务名>     添加任务")
	fmt.Println("  list              列出所有任务")
	fmt.Println("  remove <任务ID>   删除指定任务")
}

手动解析的优点是零依赖、逻辑透明;缺点是参数校验、帮助信息、错误提示都需要手写,子命令一多就容易重复。

方式二:flag 子命令集

为每个子命令创建一个独立的 flag.FlagSet,各自定义参数和默认值。分发时根据 os.Args[1] 选择对应的 FlagSet 并调用 Parse。这种方式把参数定义和解析交给标准库,帮助信息自动生成。

package main

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

type Task struct {
	ID   int
	Name string
}

var tasks []Task
var nextID = 1

func main() {
	if len(os.Args) < 2 {
		usage()
		return
	}

	switch os.Args[1] {
	case "add":
		addCmd := flag.NewFlagSet("add", flag.ExitOnError)
		name := addCmd.String("name", "", "任务名称")
		addCmd.Parse(os.Args[2:])
		if *name == "" {
			fmt.Println("必须通过 -name 指定任务名称")
			addCmd.Usage()
			return
		}
		tasks = append(tasks, Task{ID: nextID, Name: *name})
		fmt.Printf("已添加任务 #%d: %s\n", nextID, *name)
		nextID++
	case "list":
		listCmd := flag.NewFlagSet("list", flag.ExitOnError)
		listCmd.Parse(os.Args[2:])
		if len(tasks) == 0 {
			fmt.Println("暂无任务")
			return
		}
		for _, t := range tasks {
			fmt.Printf("#%d  %s\n", t.ID, t.Name)
		}
	case "remove":
		removeCmd := flag.NewFlagSet("remove", flag.ExitOnError)
		id := removeCmd.Int("id", 0, "任务ID")
		removeCmd.Parse(os.Args[2:])
		if *id == 0 {
			fmt.Println("必须通过 -id 指定任务ID")
			removeCmd.Usage()
			return
		}
		found := false
		for i, t := range tasks {
			if t.ID == *id {
				tasks = append(tasks[:i], tasks[i+1:]...)
				fmt.Printf("已删除任务 #%d\n", *id)
				found = true
				break
			}
		}
		if !found {
			fmt.Printf("未找到任务 #%d\n", *id)
		}
	default:
		usage()
	}
}

func usage() {
	fmt.Println("用法: task <add|list|remove> [参数]")
	fmt.Println("  add -name <任务名>   添加任务")
	fmt.Println("  list                  列出所有任务")
	fmt.Println("  remove -id <任务ID>   删除指定任务")
}

flag 子命令集把参数定义、类型转换、默认值和帮助信息统一交给标准库处理,代码更规范,但每个子命令需要单独创建 FlagSet,分发逻辑仍然要手写。

两种方式的关键差异

os.Args 手动解析 vs flag 子命令集 方式一:手动解析 os.Args os.Args[1] 取子命令 switch 分发到 add / list / remove os.Args[2] 按位置取参数 add 取任务名,remove 取 ID 字符串 手动类型转换与校验 strconv.Atoi、len 检查、错误提示 手写 usage 帮助信息 每个子命令的用法需自行维护 特点 零依赖 · 逻辑透明 · 校验全手写 参数格式固定 · 扩展需改 switch 方式二:flag 子命令集 os.Args[1] 取子命令 switch 分发到对应 FlagSet flag.NewFlagSet 定义参数 -name string / -id int,含默认值 Parse 自动解析与类型转换 -id 自动转 int,非法值报错退出 自动生成 -h 帮助信息 FlagSet.Usage 可自定义覆盖 特点 标准库 · 类型安全 · 帮助自动生成 参数带 - 前缀 · 扩展需加 FlagSet 分发逻辑相同,参数处理方式不同

运行验证

两种方式编译后运行效果一致,但参数写法不同:

操作 os.Args 方式 flag 方式
添加任务 task add 写周报 task add -name 写周报
列出任务 task list task list
删除任务 task remove 1 task remove -id 1
查看帮助 task 或 task help task add -h 自动输出
选择建议:参数少且格式固定时,手动解析 os.Args 更直接;参数需要类型校验、默认值或自动帮助时,flag 子命令集更规范。实际项目中,子命令分发常配合 cobra 等库,但理解这两种基础方式有助于排查参数解析问题。
注意:flag 方式中,flag.ExitOnError 会在解析失败时直接退出程序。若需要自定义错误处理,可改用 flag.ContinueOnError 并检查 Parse 返回的 error。