ZCli 是一个基于 Cobra 的现代化企业级命令行应用框架,提供了优雅的 API 设计、完整的系统服务管理、强大的错误处理系统、现代化的多语言支持和生产级的稳定性保障。
设计说明与关键决策见
DESIGN.md。
ServiceRunner 接口,支持依赖注入和模块化设计WithServiceRunner、WithService、WithValidator 等方法BuildWithError 方法,提供更好的错误处理体验QuickService、QuickCLI 等快速创建函数,简化常见使用场景WithInitHook 在命令执行前完成外部系统初始化(与配置系统解耦)zcli.Command(类型别名)与 app.Command() 直通原生 API,减少包装同步成本WithMousetrapDisabled(true) 禁用双击运行提示ErrServiceAlreadyRunningatomic.Bool 等原子类型确保状态一致性RunWait 默认监听 SIGINT/SIGTERM/SIGQUIT,未设置时自动注入;捕获后调用 sm.Stop()Run(ctx),再执行 stop hookWithServiceTimeouts 写入系统服务启动/停止超时go get github.com/darkit/zcli
最低要求: Go 1.23+ (使用了新特性)
完整可运行版本见 examples/README.md:
examples/cli-toolexamples/function-serviceexamples/service-runnerexamples/service-configexamples/flag-exportpackage main
import (
"fmt"
"log/slog"
"os"
"strings"
"github.com/darkit/zcli"
)
func main() {
app, err := zcli.NewBuilder("en").
WithName("notesctl").
WithDisplayName("Notes CLI").
WithDescription("Recommended CLI-only example built with zcli.").
WithVersion("1.0.0").
WithValidator(func(cfg *zcli.Config) error {
if strings.Contains(cfg.Basic().Name, " ") {
return fmt.Errorf("application name must not contain spaces")
}
return nil
}).
WithInitHook(func(cmd *zcli.Command, args []string) error {
profile, err := cmd.Root().PersistentFlags().GetString("profile")
if err != nil {
return err
}
slog.Info("init hook completed", "profile", profile, "command", cmd.CommandPath(), "args", args)
return nil
}).
BuildWithError()
if err != nil {
slog.Error("build failed", "error", err)
os.Exit(1)
}
app.Command().SilenceUsage = true
app.PersistentFlags().String("profile", "dev", "Runtime profile to load")
app.AddCommand(&zcli.Command{
Use: "echo [message]",
Short: "Echo a message",
RunE: func(cmd *zcli.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("message is required")
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), strings.Join(args, " "))
return nil
},
})
if err := app.Execute(); err != nil {
slog.Error("command failed", "error", err)
os.Exit(1)
}
}
package main
import (
"context"
"log/slog"
"os"
"time"
"github.com/darkit/zcli"
)
func main() {
workDir, err := os.Getwd()
if err != nil {
slog.Error("failed to get working directory", "error", err)
os.Exit(1)
}
app, err := zcli.NewBuilder("en").
WithName("mailer-worker").
WithDisplayName("Mailer Worker").
WithDescription("Recommended function-based service example for small applications.").
WithVersion("1.0.0").
WithService(runWorker, stopWorker).
WithWorkDir(workDir).
WithEnvVar("APP_ENV", "production").
WithDependency("network-online.target", zcli.DependencyAfter).
WithServiceTimeouts(15*time.Second, 20*time.Second).
BuildWithError()
if err != nil {
slog.Error("build failed", "error", err)
os.Exit(1)
}
if err := app.Execute(); err != nil {
slog.Error("service command failed", "error", err)
os.Exit(1)
}
}
func runWorker(ctx context.Context) error {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
slog.Info("worker received shutdown signal")
return nil
case <-ticker.C:
slog.Info("processed batch", "count", 25)
}
}
}
func stopWorker() error {
slog.Info("worker stop hook completed")
return nil
}
ZCli 选择 “类型别名 + 直通原生” 的方式降低升级成本:
type Command = cobra.Command,直接使用 zcli.Command 即可获得全部 Cobra 能力。app.Command() 获取底层 *cobra.Command。Cli 上的包装方法仅覆盖常用能力,后续 Cobra 升级无需同步全量封装。示例:
cmd := &zcli.Command{Use: "status"}
app.AddCommand(cmd)
raw := app.Command()
raw.SilenceUsage = true
# 前台运行模式 - 开发调试使用
./myapp run # 在前台运行,可以看到日志,Ctrl+C停止
# 服务管理模式 - 生产环境使用
./myapp install # 安装为系统服务
./myapp start # 启动服务
./myapp status # 查看服务状态
./myapp stop # 停止服务
./myapp restart # 重启服务
./myapp uninstall # 卸载服务
# 其他功能
./myapp help # 查看帮助
./myapp version # 查看版本信息
// 定义你的服务
type MyService struct {
db *Database
cache *Cache
config *Config
}
func (s *MyService) Run(ctx context.Context) error {
slog.Info("enterprise service started")
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
slog.Info("shutdown signal received")
return nil
case <-ticker.C:
if err := s.processBusinessLogic(); err != nil {
return fmt.Errorf("business logic failed: %w", err)
}
}
}
}
func (s *MyService) Stop() error {
slog.Info("stopping service")
// 关闭数据库连接
if err := s.db.Close(); err != nil {
return fmt.Errorf("failed to close database: %w", err)
}
// 关闭缓存连接
if err := s.cache.Close(); err != nil {
return fmt.Errorf("failed to close cache: %w", err)
}
return nil
}
func (s *MyService) Name() string {
return "enterprise-service"
}
func (s *MyService) processBusinessLogic() error {
// 实现业务逻辑
return nil
}
func main() {
db := &Database{}
cache := &Cache{}
service := &MyService{
db: db,
cache: cache,
config: &Config{},
}
app, err := zcli.NewBuilder("en").
WithName("queue-worker").
WithDisplayName("Queue Worker").
WithDescription("Recommended ServiceRunner example for non-trivial services.").
WithVersion("2.1.0").
WithWorkDir("/opt/enterprise-app").
WithEnvVar("APP_ENV", "production").
WithEnvVar("LOG_LEVEL", "info").
WithEnvVar("DB_HOST", "localhost:5432").
WithDependency("network-online.target", zcli.DependencyAfter).
WithStructuredDependencies(
zcli.Dependency{Name: "postgresql.service", Type: zcli.DependencyRequire},
zcli.Dependency{Name: "redis.service", Type: zcli.DependencyWant},
).
WithServiceRunner(service).
WithValidator(func(cfg *zcli.Config) error {
if cfg.Service().EnvVars["DB_HOST"] == "" {
return fmt.Errorf("DB_HOST must not be empty")
}
return nil
}).
BuildWithError()
if err != nil {
slog.Error("build failed", "error", err)
os.Exit(1)
}
if err := app.Execute(); err != nil {
slog.Error("service command failed", "error", err)
os.Exit(1)
}
}
func main() {
app, err := zcli.NewBuilder("en").
WithName("error-demo").
WithService(runWithErrorHandling, stopService).
BuildWithError()
if err != nil {
if serviceErr, ok := zcli.GetServiceError(err); ok {
slog.Error("service error",
"code", serviceErr.Code,
"service", serviceErr.Service,
"operation", serviceErr.Operation,
"message", serviceErr.Message)
} else {
slog.Error("build failed", "error", err)
}
os.Exit(1)
}
app.Execute()
}
func runWithErrorHandling(ctx context.Context) error {
aggregator := zcli.NewErrorAggregator()
if err := initDatabase(); err != nil {
aggregator.Add(zcli.NewError(zcli.ErrServiceCreate).
Service("database").
Operation("init").
Message("database initialization failed").
Cause(err).
Build())
}
if err := initCache(); err != nil {
aggregator.Add(zcli.NewError(zcli.ErrServiceCreate).
Service("cache").
Operation("init").
Message("cache initialization failed").
Cause(err).
Build())
}
if aggregator.HasErrors() {
return aggregator.Error()
}
for {
select {
case <-ctx.Done():
return nil
default:
// 处理业务逻辑
time.Sleep(100 * time.Millisecond)
}
}
}
func initDatabase() error {
// 模拟数据库初始化
return nil
}
func initCache() error {
// 模拟缓存初始化
return nil
}
// 创建配置管理主命令
configCmd := &zcli.Command{
Use: "config",
Short: "Manage application configuration",
Long: "Inspect and update application configuration.",
Run: func(cmd *zcli.Command, args []string) {
fmt.Println("Configuration command")
},
}
// 添加子命令
showCmd := &zcli.Command{
Use: "show",
Short: "Show the current configuration",
Run: func(cmd *zcli.Command, args []string) {
fmt.Println("Current configuration:")
fmt.Println("- Service name: myapp")
fmt.Println("- Working directory: /opt/myapp")
fmt.Println("- Runtime mode: production")
},
}
editCmd := &zcli.Command{
Use: "edit",
Short: "Edit the configuration",
Run: func(cmd *zcli.Command, args []string) {
fmt.Println("Opening configuration editor...")
},
}
// 组织命令层次
configCmd.AddCommand(showCmd, editCmd)
app.AddCommand(configCmd)
// English is the default in the official examples.
englishApp, _ := zcli.NewBuilder("en").BuildWithError()
// Opt in to Chinese explicitly when needed.
chineseApp, _ := zcli.NewBuilder("zh").BuildWithError()
// Or set the language later in the builder chain.
customApp, _ := zcli.NewBuilder().WithLanguage("en").BuildWithError()
_, _, _ = englishApp, chineseApp, customApp
$ mailer-worker --help
███████╗ ██████╗██╗ ██╗
╚══███╔╝██╔════╝██║ ██║
███╔╝ ██║ ██║ ██║
███╔╝ ██║ ██║ ██║
███████╗╚██████╗███████╗██║
╚══════╝ ╚═════╝╚══════╝╚═╝ v1.0.0
Mailer Worker
Usage:
mailer-worker [command]
Available Commands:
help Help about any command
install Install Service
restart Restart Service
run Run Service
start Start Service
status Service Status
stop Stop Service
uninstall Uninstall Service
Flags:
-h, --help help for mailer-worker
-v, --version Show version information
Use "mailer-worker [command] --help" for more information about a command.
ZCli 智能区分两种运行模式:
前台运行模式 (./myapp run):
服务运行模式 (通过系统服务):
并发管理
github.com/darkit/daemon,适合需要系统级安装/守护的场景。ZCli 框架自动管理 Context 生命周期,提供优雅的信号处理和资源清理机制。
框架内部自动创建带信号监听的 Context,无需手动处理:
// 框架内部自动执行(无需用户编写)
ctx, cancel := signal.NotifyContext(
context.Background(),
syscall.SIGINT, // Ctrl+C
syscall.SIGTERM, // 终止信号
syscall.SIGQUIT, // 退出信号
)
用户启动应用 ↓ 框架创建带信号监听的 Context ↓ 自动传递给 runService(ctx) ↓ 用户按 Ctrl+C ↓ ctx.Done()被触发 ↓ 服务优雅退出 ↓ 执行 stopService() 清理资源
✅ 优势:
func runService(ctx context.Context) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done(): // Ctrl+C 时自动触发
slog.Info("shutdown signal received")
return nil
case <-ticker.C:
// 业务逻辑
}
}
}
func runService(ctx context.Context) error {
// Context 传递给依赖组件
db, _ := database.Connect(ctx)
cache, _ := redis.Connect(ctx)
// 当收到停止信号时,所有组件都能感知到
// 数据库、缓存等会自动关闭连接
}
func runService(ctx context.Context) error {
// 为某个操作设置超时
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
return doSomethingWithTimeout(ctx)
}
❌ 如果没有 Context(不推荐):
// 需要手动处理信号
func runService() error {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
for {
select {
case <-sigChan: // 手动处理
return nil
default:
// 业务逻辑
}
}
}
func runService(ctx context.Context) error {
slog.Info("service started")
// 1. 初始化资源(传递 ctx)
db, err := database.Connect(ctx)
if err != nil {
return err
}
defer db.Close()
// 2. 创建子 Context(可选)
workCtx, cancel := context.WithCancel(ctx)
defer cancel()
// 3. 启动后台任务
go backgroundTask(workCtx)
// 4. 主循环
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
// 框架收到 Ctrl+C 时自动触发
slog.Info("shutdown signal received")
// 取消所有子任务
cancel()
// 等待子任务完成
time.Sleep(100 * time.Millisecond)
return nil
case <-ticker.C:
// 正常业务逻辑
if err := doWork(ctx); err != nil {
return err
}
}
}
}
func stopService() error {
slog.Info("running final cleanup")
// 这里执行无法通过 context 传递的清理工作
// 例如:关闭文件句柄、保存状态、刷新缓冲区等
return nil
}
上层业务通常只需要监听 ctx.Done() 来触发优雅退出;如果还需要区分“为什么停止”,可以继续读取 zcli 注入的结构化停止原因:
func runService(ctx context.Context) error {
for {
select {
case <-ctx.Done():
if shutdown, ok := zcli.GetShutdownCause(ctx); ok {
slog.Info("service stopping",
"reason", shutdown.Reason,
"signal", shutdown.Signal,
"cause", shutdown.Cause)
}
// 这里执行主循环内的 drain / flush / close listener 等优雅退出逻辑
return nil
}
}
}
GetShutdownCause(ctx) 会返回统一的跨平台停止原因:
ShutdownReasonSignal:前台运行时收到 Ctrl+C、SIGTERM、SIGQUIT 等系统信号。ShutdownReasonServiceStop:后台服务模式下收到服务管理器停止请求,或框架内部显式调用 Stop()。ShutdownReasonExternalCancel:上层传入的父 Context 被取消,例如外部协调器、测试控制器或宿主进程主动终止。如果你需要底层错误链,也可以继续使用标准库的 context.Cause(ctx);但在业务代码里,优先推荐使用 zcli.GetShutdownCause(ctx),因为它已经把前台信号、后台服务停止和外部取消统一成稳定的应用层语义。
框架默认提供 15+5 秒分级关闭预算,并在必要时触发强制退出:
收到停止信号(Ctrl+C) ↓ 先取消 Run(ctx),让服务主循环开始优雅退出 ↓ 等待 15 秒(主服务退出期) ↓ [超时] 继续执行 stopService() / 最终清理 ↓ 再等待 5 秒(清理期) ↓ [超时] 后台模式触发强制退出超时(默认 20s,可配置)
这确保 Run(ctx) 能先收到停止信号,真实服务有足够时间开始优雅退出;如果仍未结束,再进入最终清理和后台强制退出保护。
ctx.Done()for {
select {
case <-ctx.Done():
return nil // 正常优雅退出;如需区分原因,读取 zcli.GetShutdownCause(ctx)
case data := <-dataChan:
processData(data)
}
}
// 数据库查询
rows, err := db.QueryContext(ctx, sql)
// HTTP 请求
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
// gRPC 调用
conn, err := grpc.DialContext(ctx, addr)
// ❌ 错误:不要将 context 存储在结构体中
type Service struct {
ctx context.Context // 违反 Go 官方建议
}
// ✅ 正确:通过参数传递
func (s *Service) Run(ctx context.Context) error {
// 使用参数传递的 ctx
}
// 传递请求 ID
ctx = context.WithValue(ctx, "requestID", uuid.New())
// 在下游获取
requestID := ctx.Value("requestID").(uuid.UUID)
ZCli 提供了强大的命令行标志导出功能,专门用于与外部包(如 Viper 配置管理)的集成:
// 创建应用并添加各种标志
app, err := zcli.NewBuilder("en").
WithName("bindable-flags").
WithDescription("Export zcli flags to another package.").
BuildWithError()
if err != nil {
return err
}
// 添加业务标志
app.PersistentFlags().String("config", "", "Path to the configuration file")
app.PersistentFlags().Bool("debug", false, "Enable debug logging")
app.Flags().Int("port", 8080, "HTTP port")
// 导出给外部包使用(自动排除系统标志)
flags := app.ExportFlagsForViper()
WithBindPFlags(flags...)(viperConfig)
框架自动排除 23 个系统标志,确保只有业务标志传递给外部包:
// 自动排除的系统标志包括:
// - 帮助系统:help, h
// - 版本系统:version, v
// - 补全系统:completion, completion-*, gen-completion
// - 内部调试:__complete, __completeNoDesc, no-descriptions
// - 配置系统:config-help, print-config, validate-config
// ... 总计23个系统标志
// 检查标志类型
isSystem := app.IsSystemFlag("help") // true - 系统标志
isSystem := app.IsSystemFlag("config") // false - 业务标志
// 获取所有系统标志列表(调试用)
systemFlags := app.GetSystemFlags() // 返回23个系统标志的完整列表
| 方法 | 用途 | 返回值 |
|---|---|---|
ExportFlagsForViper(exclude...) | 导出适用于 Viper 的标志 | []*FlagSet |
GetAllFlagSets() | 获取所有标志集合 | []*FlagSet |
GetBindableFlagSets(exclude...) | 获取可绑定的标志(排除系统标志) | []*FlagSet |
GetFilteredFlags(exclude...) | 获取单个过滤后的标志集合 | *FlagSet |
GetFlagNames(includeInherited) | 获取标志名称列表 | []string |
GetFilteredFlagNames(exclude...) | 获取过滤后的标志名称 | []string |
IsSystemFlag(name) | 检查是否为系统标志 | bool |
GetSystemFlags() | 获取所有系统标志列表 | []string |
DESIGN.mddocs/darkit-zcli/resources/design-audit-2026-03.md| 方法 | 说明 | 示例 |
|---|---|---|
NewBuilder(lang) | 创建新的 Builder 实例 | zcli.NewBuilder("en") |
Build() | 构建 CLI 实例(可能 panic) | builder.Build() |
BuildWithError() | 构建 CLI 实例,返回错误 | app, err := builder.BuildWithError() |
| 方法 | 说明 | 示例 |
|---|---|---|
WithName(name) | 设置应用名称 | .WithName("myapp") |
WithDisplayName(name) | 设置显示名称 | .WithDisplayName("My App") |
WithDescription(desc) | 设置应用描述 | .WithDescription("App CLI") |
WithVersion(version) | 设置版本号 | .WithVersion("1.0.0") |
WithLanguage(lang) | 设置语言 | .WithLanguage("zh") |
| 方法 | 说明 | 示例 |
|---|---|---|
WithService(run, stop...) | 配置服务函数 | .WithService(runFunc, stopFunc) |
WithServiceRunner(service) | 配置服务接口实现;传 nil 时在构建阶段返回错误 | .WithServiceRunner(myService) |
WithServiceTimeouts(start, stop) | 配置服务启动/停止超时 | .WithServiceTimeouts(30*time.Second, 20*time.Second) |
WithValidator(validator) | 添加配置验证器 | .WithValidator(validatorFunc) |
WithWorkDir(dir) | 设置工作目录 | .WithWorkDir("/opt/app") |
WithEnvVar(key, value) | 添加环境变量 | .WithEnvVar("ENV", "prod") |
WithDependencies(deps...) | 设置 require 型服务依赖 | .WithDependencies("postgresql.service") |
WithDependency(name, type) | 添加单个结构化依赖 | .WithDependency("network-online.target", zcli.DependencyAfter) |
WithStructuredDependencies(...) | 批量设置结构化依赖 | .WithStructuredDependencies(zcli.Dependency{Name: "redis.service", Type: zcli.DependencyWant}) |
WithServiceUser(username) | 设置系统服务用户 | .WithServiceUser("svc-app") |
WithArguments(args...) | 设置服务运行参数 | .WithArguments("run", "--profile", "prod") |
WithServiceOption(key, value) | 设置平台特定选项 | .WithServiceOption("Restart", "on-failure") |
WithAllowSudoFallback(enabled) | 配置 sudo/su 回退策略 | .WithAllowSudoFallback(true) |
示例:将服务启动/停止超时写入系统服务配置(daemon Timeout)
app, err := zcli.NewBuilder("en").
WithName("myapp").
WithService(runService, stopService).
WithServiceTimeouts(30*time.Second, 20*time.Second).
BuildWithError()
| 方法 | 说明 | 示例 |
|---|---|---|
QuickService(name, displayName, run) | 快速创建服务应用 | zcli.QuickService("app", "App Service", runFunc) |
QuickServiceWithStop(name, displayName, run, stop) | 快速创建带停止函数的服务 | zcli.QuickServiceWithStop(...) |
QuickCLI(name, display, desc) | 快速创建 CLI 工具 | zcli.QuickCLI("tool", "CLI Tool", "Short description") |
type ServiceRunner interface {
Run(ctx context.Context) error // 运行服务
Stop() error // 停止服务
Name() string // 服务名称
}
type MyService struct{}
func (s *MyService) Run(ctx context.Context) error {
// 服务运行逻辑
for {
select {
case <-ctx.Done():
return nil
default:
// 处理业务逻辑
}
}
}
func (s *MyService) Stop() error {
// 停止逻辑
return nil
}
func (s *MyService) Name() string {
return "my-service"
}
| 函数 | 说明 | 示例 |
|---|---|---|
NewError(code) | 创建错误构建器 | zcli.NewError(zcli.ErrServiceStart) |
NewErrorAggregator() | 创建错误聚合器 | aggregator := zcli.NewErrorAggregator() |
GetServiceError(err) | 获取服务错误 | serviceErr, ok := zcli.GetServiceError(err) |
| 函数 | 说明 | 示例 |
|---|---|---|
ErrServiceAlreadyRunning(name) | 服务已运行错误 | zcli.ErrServiceAlreadyRunning("myapp") |
ErrServiceAlreadyStopped(name) | 服务已停止错误 | zcli.ErrServiceAlreadyStopped("myapp") |
ErrServiceStartTimeout(name, timeout) | 启动超时错误 | zcli.ErrServiceStartTimeout("myapp", 30*time.Second) |
err := zcli.NewError(zcli.ErrServiceStart).
Service("myapp"). // set the service name
Operation("start"). // set the operation
Message("failed to start service"). // set the message
Cause(originalError). // attach the cause
Context("key", "value"). // add contextual fields
Build() // build the final error
// ✅ 推荐:使用BuildWithError
app, err := zcli.NewBuilder("en").
WithName("myapp").
BuildWithError()
if err != nil {
// 处理错误
}
// ❌ 不推荐:可能panic的Build
app := zcli.NewBuilder("en").
WithName("myapp").
Build() // 可能panic
// `WithServiceRunner(nil)` 现在也会在构建阶段暴露错误,
// 推荐统一使用 BuildWithError 处理配置问题。
// ✅ 推荐:使用接口抽象
type DatabaseService interface {
Connect() error
Close() error
}
type MyService struct {
db DatabaseService // 依赖注入
}
app, err := zcli.NewBuilder("en").
WithName(myService.Name()).
WithServiceRunner(myService). // 接口抽象
BuildWithError()
if err != nil {
return err
}
// ✅ 推荐:添加配置验证
app, err := zcli.NewBuilder("en").
WithName("myapp").
WithValidator(func(cfg *zcli.Config) error {
if cfg.Basic().Name == "" {
return fmt.Errorf("application name must not be empty")
}
return nil
}).
BuildWithError()
// ✅ 推荐:使用结构化错误
aggregator := zcli.NewErrorAggregator()
if err := initDB(); err != nil {
aggregator.Add(zcli.NewError(zcli.ErrServiceCreate).
Service("database").
Operation("init").
Cause(err).
Build())
}
if aggregator.HasErrors() {
return aggregator.Error()
}
attachInitHooks 被重复调用的问题sync.Once 确保 stopChan 只关闭一次ManagedService.Run() 现在正确返回生命周期钩子错误WithErrorHandler 方法,支持自定义错误处理链Config.View() 方法,避免频繁深拷贝command.go 为多个文件,提升可维护性ZCLI_TEST_TIMEOUT_MULTIPLIER 环境变量调整测试超时ServiceRunner 接口统一服务管理WithServiceRunner、WithValidator 方法BuildWithError 方法,提供更好的错误处理QuickService、QuickCLI 等快速创建函数欢迎提交 Issue 和 Pull Request!
git clone https://github.com/darkit/zcli.git
cd zcli
go mod tidy
go test -v # 运行测试
# 运行所有测试
go test -v
# 运行并发安全测试
go test -v -run TestServiceConcurrent
# 运行集成测试
go test -v -run TestFinalIntegration
ZCli: 让您的 CLI 应用更专业、更稳定、更易用。