Skip to content

交互式 CLI 与 TUI 基础

传统 CLI 工具通过参数和标志接收输入,但有些场景需要与用户进行交互式对话:确认操作、选择选项、输入密码等。本篇将介绍如何使用 Go 标准库和第三方库构建交互式 CLI 工具,涵盖用户输入、密码读取、确认提示、多选单选、进度条、Spinner、表格输出和彩色输出等技术。

一、交互式 CLI 概述

交互式 CLI 是指在运行过程中与用户进行问答式交互的命令行程序。与"一次性接收所有参数然后执行"的传统 CLI 不同,交互式 CLI 会:

  • 逐步引导用户完成操作(类似安装向导)
  • 在关键操作前请求确认(如删除前确认)
  • 提供选择菜单而非要求用户记住参数
  • 实时反馈进度(进度条、Spinner)

典型的交互式 CLI 例子:npm create vitegit initdocker login

交互式 CLI 适用于:

  • 初始化/脚手架工具(创建项目、生成配置)
  • 配置向导(数据库初始化、SSH 配置)
  • 需要用户确认的危险操作
  • 需要用户从列表中选择的场景

二、读取用户输入

1. bufio.Scanner

bufio.Scanner 是读取用户输入最简单的方式,适合逐行读取文本。

go
package main

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

func main() {
	scanner := bufio.NewScanner(os.Stdin)

	fmt.Print("请输入你的名字: ")
	scanner.Scan()
	name := strings.TrimSpace(scanner.Text())

	fmt.Print("请输入你的职业: ")
	scanner.Scan()
	job := strings.TrimSpace(scanner.Text())

	fmt.Printf("\n你好,%s!你的职业是 %s\n", name, job)

	// 读取多行输入
	fmt.Println("\n请输入你的自我介绍(输入空行结束):")
	var lines []string
	for scanner.Scan() {
		line := scanner.Text()
		if line == "" {
			break
		}
		lines = append(lines, line)
	}

	fmt.Println("\n你的自我介绍:")
	for _, line := range lines {
		fmt.Println("  " + line)
	}

	if err := scanner.Err(); err != nil {
		fmt.Fprintf(os.Stderr, "读取输入错误: %v\n", err)
		os.Exit(1)
	}
}

2. bufio.Reader

bufio.ReaderScanner 更灵活,可以按字节读取、按行读取、读取指定长度等。

go
package main

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

func main() {
	reader := bufio.NewReader(os.Stdin)

	fmt.Print("请输入项目名称: ")
	name, _ := reader.ReadString('\n')
	name = strings.TrimSpace(name)

	fmt.Print("请输入项目描述: ")
	desc, _ := reader.ReadString('\n')
	desc = strings.TrimSpace(desc)

	fmt.Printf("\n项目: %s\n", name)
	fmt.Printf("描述: %s\n", desc)

	// 读取单个字符
	fmt.Print("\n继续吗?(y/n): ")
	char, _, _ := reader.ReadRune()
	fmt.Printf("你输入了: %c\n", char)
}

ScannerReader 的区别:Scanner 更简单,适合逐行扫描;Reader 更底层,支持更细粒度的控制。大多数场景用 Scanner 就够了。

三、密码输入

密码输入不应该在终端回显。golang.org/x/term 包提供了 ReadPassword 函数,在读取密码时会关闭终端回显。

安装:

bash
go get golang.org/x/term@latest
go
package main

import (
	"fmt"
	"os"
	"strings"

	"golang.org/x/term"
)

func main() {
	fmt.Print("请输入用户名: ")
	var username string
	fmt.Scanln(&username)

	fmt.Print("请输入密码: ")
	// term.ReadPassword 从 stdin 读取密码,关闭回显
	// 参数 0 表示 stdin 的文件描述符
	passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
	if err != nil {
		fmt.Fprintf(os.Stderr, "\n读取密码失败: %v\n", err)
		os.Exit(1)
	}
	password := strings.TrimSpace(string(passwordBytes))
	fmt.Println() // ReadPassword 不会输出换行,需要手动补一个

	// 验证
	if len(password) < 6 {
		fmt.Fprintln(os.Stderr, "密码长度不能少于 6 位")
		os.Exit(1)
	}

	fmt.Printf("用户名: %s\n", username)
	fmt.Printf("密码长度: %d\n", len(password))
	fmt.Println("登录成功!")
}

运行时,密码输入不会在终端显示任何字符(也不会显示星号),这是最安全的做法。

四、确认提示:Y/N

在执行危险操作前(删除文件、覆盖数据),应该让用户确认。

go
package main

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

func main() {
	fmt.Println("即将删除以下文件:")
	fmt.Println("  - /data/important.txt")
	fmt.Println("  - /data/config.yaml")
	fmt.Println()

	if !confirm("确定要删除吗?", false) {
		fmt.Println("操作已取消")
		return
	}

	fmt.Println("正在删除...")
	// 执行删除操作
}

// confirm 询问用户确认,defaultYes 表示直接回车时的默认值
func confirm(prompt string, defaultYes bool) bool {
	reader := bufio.NewReader(os.Stdin)

	hint := "[y/N]"
	if defaultYes {
		hint = "[Y/n]"
	}

	for {
		fmt.Printf("%s %s: ", prompt, hint)
		input, _ := reader.ReadString('\n')
		input = strings.TrimSpace(strings.ToLower(input))

		switch input {
		case "y", "yes":
			return true
		case "n", "no":
			return false
		case "":
			return defaultYes
		default:
			fmt.Println("请输入 y (yes) 或 n (no)")
		}
	}
}

五、多选与单选

手动实现选择菜单比较繁琐。下面是一个简单的单选实现:

go
package main

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

func main() {
	options := []string{
		"使用 Gin 框架",
		"使用 Echo 框架",
		"使用 Fiber 框架",
		"使用 Chi 框架",
		"不使用框架",
	}

	choice := selectOption("选择你的 Web 框架", options)
	fmt.Printf("\n你选择了: %s\n", options[choice])
}

// selectOption 显示选项列表并让用户选择一个
func selectOption(prompt string, options []string) int {
	reader := bufio.NewReader(os.Stdin)

	for {
		fmt.Printf("\n%s:\n", prompt)
		for i, opt := range options {
			fmt.Printf("  %d) %s\n", i+1, opt)
		}
		fmt.Print("\n请输入序号: ")

		input, _ := reader.ReadString('\n')
		input = strings.TrimSpace(input)

		num, err := strconv.Atoi(input)
		if err != nil || num < 1 || num > len(options) {
			fmt.Printf("无效输入,请输入 1-%d 的数字\n", len(options))
			continue
		}

		return num - 1
	}
}

六、survey 库

手动实现交互式输入虽然可行,但代码繁琐且体验一般。AlecAivazis/survey 库提供了丰富的交互式问题类型,是目前 Go 生态中最流行的交互式 CLI 库。

安装:

bash
go get github.com/AlecAivazis/survey/v2@latest

1. Input:文本输入

go
package main

import (
	"fmt"

	"github.com/AlecAivazis/survey/v2"
)

func main() {
	// 简单的文本输入
	var name string
	prompt := &survey.Input{
		Message: "你叫什么名字?",
		Default: "匿名用户",
	}
	survey.AskOne(prompt, &name)

	fmt.Printf("你好,%s\n", name)
}

2. Select:单选

go
package main

import (
	"fmt"

	"github.com/AlecAivazis/survey/v2"
)

func main() {
	var framework string
	prompt := &survey.Select{
		Message: "选择你喜欢的 Go Web 框架:",
		Options: []string{"Gin", "Echo", "Fiber", "Chi", "Beego"},
		Default: "Gin",
	}
	survey.AskOne(prompt, &framework)

	fmt.Printf("你选择了: %s\n", framework)
}

Select 会渲染一个可上下移动的选择列表,用户用方向键选择,回车确认。

3. MultiSelect:多选

go
package main

import (
	"fmt"

	"github.com/AlecAivazis/survey/v2"
)

func main() {
	var features []string
	prompt := &survey.MultiSelect{
		Message: "选择你需要的功能(空格选择,回车确认):",
		Options: []string{
			"用户认证",
			"日志记录",
			"数据缓存",
			"API 限流",
			"链路追踪",
			"配置热更新",
		},
		Default: []string{"用户认证", "日志记录"},
	}
	survey.AskOne(prompt, &features)

	fmt.Println("\n已选择的功能:")
	for _, f := range features {
		fmt.Printf("  - %s\n", f)
	}
}

4. Confirm:确认

go
package main

import (
	"fmt"

	"github.com/AlecAivazis/survey/v2"
)

func main() {
	var deploy bool
	prompt := &survey.Confirm{
		Message: "是否立即部署到生产环境?",
		Default: false,
	}
	survey.AskOne(prompt, &deploy)

	if deploy {
		fmt.Println("开始部署...")
	} else {
		fmt.Println("部署已取消")
	}
}

5. Password:密码输入

go
package main

import (
	"fmt"

	"github.com/AlecAivazis/survey/v2"
)

func main() {
	var password string
	prompt := &survey.Password{
		Message: "请输入数据库密码:",
	}
	survey.AskOne(prompt, &password)

	fmt.Printf("密码已设置(长度: %d\n", len(password))
}

6. 综合示例:调查问卷

go
package main

import (
	"fmt"

	"github.com/AlecAivazis/survey/v2"
)

// qs 是一组问题
var qs = []*survey.Question{
	{
		Name: "name",
		Prompt: &survey.Input{
			Message: "你的名字?",
			Default: "开发者",
		},
		Validate: survey.Required,
	},
	{
		Name: "framework",
		Prompt: &survey.Select{
			Message: "常用的 Go 框架?",
			Options: []string{"Gin", "Echo", "Fiber", "Chi", "其他"},
		},
	},
	{
		Name: "features",
		Prompt: &survey.MultiSelect{
			Message: "你希望 CLI 工具具备哪些功能?",
			Options: []string{"自动补全", "彩色输出", "进度条", "配置管理", "日志记录"},
		},
	},
	{
		Name: "experience",
		Prompt: &survey.Input{
			Message: "你有多少年 Go 开发经验?",
			Default: "1",
		},
	},
	{
		Name: "subscribe",
		Prompt: &survey.Confirm{
			Message: "是否订阅技术周报?",
			Default: true,
		},
	},
}

// Answers 存储所有答案
type Answers struct {
	Name       string   `survey:"name"`
	Framework  string   `survey:"framework"`
	Features   []string `survey:"features"`
	Experience string   `survey:"experience"`
	Subscribe  bool     `survey:"subscribe"`
}

func main() {
	var answers Answers
	// Ask 一次询问多个问题
	err := survey.Ask(qs, &answers)
	if err != nil {
		fmt.Println("问卷中断:", err)
		return
	}

	fmt.Println("\n========== 问卷结果 ==========")
	fmt.Printf("名字: %s\n", answers.Name)
	fmt.Printf("框架: %s\n", answers.Framework)
	fmt.Printf("经验: %s\n", answers.Experience)
	fmt.Printf("功能: %v\n", answers.Features)
	if answers.Subscribe {
		fmt.Println("订阅: 已订阅")
	} else {
		fmt.Println("订阅: 未订阅")
	}
	fmt.Println("==============================")
}

七、进度条

长时间操作需要进度条反馈。schollz/progressbar 是一个流行的进度条库。

安装:

bash
go get github.com/schollz/progressbar/v3@latest
go
package main

import (
	"fmt"
	"time"

	"github.com/schollz/progressbar/v3"
)

func main() {
	// 创建一个有 100 步的进度条
	bar := progressbar.NewOptions(
		100,
		progressbar.OptionSetDescription("正在处理文件"),
		progressbar.OptionSetWriter(nil), // 默认输出到 stderr
		progressbar.OptionShowCount(),
		progressbar.OptionSetWidth(40),
		progressbar.OptionOnCompletion(func() {
			fmt.Println("\n处理完成!")
		}),
		progressbar.OptionSetTheme(progressbar.Theme{
			Saucer:        "=",
			SaucerHead:    ">",
			SaucerPadding: " ",
			BarStart:      "[",
			BarEnd:        "]",
		}),
	)

	// 模拟处理 100 个文件
	for i := 0; i < 100; i++ {
		bar.Add(1)
		time.Sleep(20 * time.Millisecond)
	}
}

注意:实际运行时进度条会在终端实时刷新显示百分比和进度条。OptionSetWriter(nil) 表示使用默认的 stderr 输出。

八、Spinner

Spinner 用于表示"正在执行"的状态,适用于不确定耗时的操作。

安装:

bash
go get github.com/briandowns/spinner@latest
go
package main

import (
	"fmt"
	"time"

	"github.com/briandowns/spinner"
)

func main() {
	// 创建 spinner
	s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
	s.Prefix = "正在构建项目 "
	s.Suffix = " 请稍候..."

	s.Start()

	// 模拟构建过程
	time.Sleep(3 * time.Second)

	s.Stop()

	fmt.Println("构建完成!")
}

CharSets 提供了多种 spinner 样式(数字索引 0-90),常用的有:

  • CharSets[9]⣾⣽⣻⢿⡿⣟⣯⣷(经典 Braille 点阵)
  • CharSets[11]⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏(紧凑 Braille)
  • CharSets[14]◐◓◑◒(旋转圆圈)
  • CharSets[39]|/-\(经典旋转杠)

九、表格输出

tablewriter 库可以格式化输出表格数据,让 CLI 输出更美观。

安装:

bash
go get github.com/olekukonko/tablewriter@latest
go
package main

import (
	"os"

	"github.com/olekukonko/tablewriter"
)

func main() {
	data := [][]string{
		{"1", "Gin", "radix tree", "极高", "是"},
		{"2", "Echo", "radix tree", "极高", "是"},
		{"3", "Fiber", "fasthttp", "极高", "否"},
		{"4", "Chi", "net/http", "高", "是"},
		{"5", "Beego", "树形路由", "中", "是"},
	}

	table := tablewriter.NewWriter(os.Stdout)
	table.SetHeader([]string{"序号", "框架", "路由", "性能", "兼容 net/http"})

	// 设置表格样式
	table.SetAutoWrapText(false)
	table.SetAutoFormatHeaders(true)
	table.SetHeaderAlignment(tablewriter.ALIGN_CENTER)
	table.SetAlignment(tablewriter.ALIGN_LEFT)
	table.SetCenterSeparator("")
	table.SetColumnSeparator("")
	table.SetRowSeparator("")
	table.SetHeaderLine(false)
	table.SetBorder(false)
	table.SetTablePadding("\t")
	table.SetNoWhiteSpace(true)

	// 添加数据
	for _, row := range data {
		table.Append(row)
	}

	// 渲染表格
	table.Render()
}

输出效果:

  序号   框架     路由         性能    兼容 net/http
  1      Gin      radix tree   极高    是
  2      Echo     radix tree   极高    是
  3      Fiber    fasthttp     极高    否
  4      Chi      net/http     高      是
  5      Beego    树形路由     中      是

十、彩色输出

fatih/color 库让 CLI 输出支持彩色文字、加粗、下划线等样式。

安装:

bash
go get github.com/fatih/color@latest
go
package main

import (
	"fmt"

	"github.com/fatih/color"
)

func main() {
	// 基本颜色
	color.Red("这是红色文字")
	color.Green("这是绿色文字")
	color.Yellow("这是黄色文字")
	color.Blue("这是蓝色文字")
	color.Cyan("这是青色文字")
	color.Magenta("这是洋红色文字")

	// 加粗和下划线
	color.New(color.Bold).Println("这是加粗文字")
	color.New(color.Underline).Println("这是下划线文字")

	// 组合样式
	color.New(color.FgGreen, color.Bold).Println("绿色加粗")
	color.New(color.FgRed, color.BgYellow).Println("红字黄底")

	// 创建可复用的颜色打印机
	success := color.New(color.FgGreen, color.Bold)
	warning := color.New(color.FgYellow)
	error_ := color.New(color.FgRed, color.Bold)
	info := color.New(color.FgCyan)

	success.Println("✓ 操作成功")
	warning.Println("⚠ 警告:磁盘空间不足")
	error_.Println("✗ 错误:连接超时")
	info.Println("ℹ 信息:正在重试...")

	// 混合普通输出和彩色输出
	fmt.Print("构建状态: ")
	success.Print("成功")
	fmt.Println(" (耗时 2.3s)")

	// 条件彩色输出
	status := "failed"
	switch status {
	case "success":
		success.Printf("状态: %s\n", status)
	case "failed":
		error_.Printf("状态: %s\n", status)
	case "pending":
		warning.Printf("状态: %s\n", status)
	}
}

注意:彩色输出在非 TTY 环境(如管道重定向到文件)下会自动禁用颜色码,确保输出内容不受影响。可以通过 color.NoColor = false 强制启用。

十一、完整示例:交互式项目初始化工具

下面综合使用上述技术,实现一个类似 create-vite 的交互式项目初始化工具。

go
package main

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/AlecAivazis/survey/v2"
	"github.com/briandowns/spinner"
	"github.com/fatih/color"
	"github.com/olekukonko/tablewriter"
	progressbar "github.com/schollz/progressbar/v3"
)

// ProjectConfig 存储项目配置
type ProjectConfig struct {
	Name      string
	Template  string
	GoVersion string
	Modules   []string
	UseGit    bool
	Author    string
}

func main() {
	cyan := color.New(color.FgCyan, color.Bold)
	green := color.New(color.FgGreen, color.Bold)

	cyan.Println("╔══════════════════════════════════════╗")
	cyan.Println("║     Go 项目初始化工具 v1.0.0         ║")
	cyan.Println("╚══════════════════════════════════════╝")
	fmt.Println()

	// 收集用户输入
	config, err := collectConfig()
	if err != nil {
		color.Red("配置收集失败: %v", err)
		os.Exit(1)
	}

	// 显示配置摘要
	printSummary(config)

	// 确认创建
	var confirm bool
	survey.AskOne(&survey.Confirm{
		Message: "确认以上配置并创建项目?",
		Default: true,
	}, &confirm)

	if !confirm {
		color.Yellow("已取消")
		return
	}

	// 创建项目
	if err := createProject(config); err != nil {
		color.Red("项目创建失败: %v", err)
		os.Exit(1)
	}

	green.Println("\n✓ 项目创建成功!")
	color.Cyan("  cd %s && go mod tidy", config.Name)
}

// collectConfig 通过交互式问答收集项目配置
func collectConfig() (*ProjectConfig, error) {
	qs := []*survey.Question{
		{
			Name: "name",
			Prompt: &survey.Input{
				Message: "项目名称:",
				Default: "my-project",
			},
			Validate: func(val interface{}) error {
				s, ok := val.(string)
				if !ok || len(s) < 2 {
					return fmt.Errorf("项目名称至少 2 个字符")
				}
				return nil
			},
		},
		{
			Name: "template",
			Prompt: &survey.Select{
				Message: "选择项目模板:",
				Options: []string{
					"CLI 工具 (Cobra)",
					"Web API (Gin)",
					"gRPC 服务",
					"Worker (后台任务)",
					"库 (Library)",
				},
				Default: "Web API (Gin)",
			},
		},
		{
			Name: "goVersion",
			Prompt: &survey.Select{
				Message: "Go 版本:",
				Options: []string{"1.21", "1.22", "1.23"},
				Default: "1.23",
			},
		},
		{
			Name: "modules",
			Prompt: &survey.MultiSelect{
				Message: "选择需要的模块:",
				Options: []string{
					"配置管理 (Viper)",
					"日志 (Zap)",
					"数据库 (GORM)",
					"Redis 客户端",
					"HTTP 客户端",
					"测试框架",
				},
				Default: []string{"配置管理 (Viper)", "日志 (Zap)"},
			},
		},
		{
			Name: "useGit",
			Prompt: &survey.Confirm{
				Message: "初始化 Git 仓库?",
				Default: true,
			},
		},
		{
			Name: "author",
			Prompt: &survey.Input{
				Message: "作者名称:",
				Default: "unknown",
			},
		},
	}

	var config ProjectConfig
	if err := survey.Ask(qs, &config); err != nil {
		return nil, err
	}

	return &config, nil
}

// printSummary 打印配置摘要表格
func printSummary(config *ProjectConfig) {
	fmt.Println()
	table := tablewriter.NewWriter(os.Stdout)
	table.SetHeader([]string{"配置项", "值"})
	table.SetAutoWrapText(false)
	table.SetBorder(true)
	table.SetCenterSeparator("+")
	table.SetColumnSeparator("|")
	table.SetRowSeparator("-")

	rows := [][]string{
		{"项目名称", config.Name},
		{"模板", config.Template},
		{"Go 版本", config.GoVersion},
		{"模块", strings.Join(config.Modules, ", ")},
		{"Git 初始化", fmt.Sprintf("%v", config.UseGit)},
		{"作者", config.Author},
	}
	for _, row := range rows {
		table.Append(row)
	}
	table.Render()
	fmt.Println()
}

// createProject 创建项目文件
func createProject(config *ProjectConfig) error {
	// Step 1: 创建目录
	s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
	s.Prefix = "创建目录结构 "
	s.Start()
	time.Sleep(500 * time.Millisecond)

	dir := config.Name
	if err := os.MkdirAll(filepath.Join(dir, "cmd", config.Name), 0755); err != nil {
		return fmt.Errorf("创建目录失败: %w", err)
	}
	os.MkdirAll(filepath.Join(dir, "internal"), 0755)
	os.MkdirAll(filepath.Join(dir, "pkg"), 0755)
	os.MkdirAll(filepath.Join(dir, "configs"), 0755)

	s.Stop()
	color.Green(" ✓ 目录结构已创建")

	// Step 2: 生成 go.mod
	s = spinner.New(spinner.CharSets[11], 100*time.Millisecond)
	s.Prefix = "生成 go.mod "
	s.Start()
	time.Sleep(300 * time.Millisecond)

	modContent := fmt.Sprintf(`module %s

go %s

require (
`, config.Name, config.GoVersion)

	for _, mod := range config.Modules {
		switch {
		case strings.Contains(mod, "Viper"):
			modContent += "\tgithub.com/spf13/viper v1.18.0\n"
		case strings.Contains(mod, "Zap"):
			modContent += "\tgo.uber.org/zap v1.27.0\n"
		case strings.Contains(mod, "GORM"):
			modContent += "\tgorm.io/gorm v1.25.0\n"
		case strings.Contains(mod, "Redis"):
			modContent += "\tgithub.com/redis/go-redis/v9 v9.5.0\n"
		}
	}
	modContent += ")\n"

	os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modContent), 0644)
	s.Stop()
	color.Green(" ✓ go.mod 已生成")

	// Step 3: 生成 main.go
	s = spinner.New(spinner.CharSets[11], 100*time.Millisecond)
	s.Prefix = "生成 main.go "
	s.Start()
	time.Sleep(300 * time.Millisecond)

	mainContent := fmt.Sprintf(`package main

import "fmt"

func main() {
	fmt.Println("Hello, %s!")
}
`, config.Name)

	os.WriteFile(filepath.Join(dir, "cmd", config.Name, "main.go"), []byte(mainContent), 0644)
	s.Stop()
	color.Green(" ✓ main.go 已生成")

	// Step 4: 生成配置文件
	s = spinner.New(spinner.CharSets[11], 100*time.Millisecond)
	s.Prefix = "生成配置文件 "
	s.Start()
	time.Sleep(300 * time.Millisecond)

	configContent := `app:
  name: ` + config.Name + `
  env: development
server:
  host: 0.0.0.0
  port: 8080
`
	os.WriteFile(filepath.Join(dir, "configs", "config.yaml"), []byte(configContent), 0644)
	s.Stop()
	color.Green(" ✓ 配置文件已生成")

	// Step 5: 生成 .gitignore
	if config.UseGit {
		s = spinner.New(spinner.CharSets[11], 100*time.Millisecond)
		s.Prefix = "生成 .gitignore "
		s.Start()
		time.Sleep(200 * time.Millisecond)

		gitignore := `# Binaries
*.exe
*.dll
*.so
*.dylib
bin/

# Test
*.test
*.out
coverage.txt

# Dependency
vendor/

# IDE
.idea/
.vscode/
*.swp

# OS
.DS_Store
Thumbs.db
`
		os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(gitignore), 0644)
		s.Stop()
		color.Green(" ✓ .gitignore 已生成")
	}

	// Step 6: 安装依赖(模拟)
	bar := progressbar.NewOptions(
		10,
		progressbar.OptionSetDescription("安装依赖"),
		progressbar.OptionSetWidth(30),
		progressbar.OptionShowCount(),
	)
	for i := 0; i < 10; i++ {
		bar.Add(1)
		time.Sleep(100 * time.Millisecond)
	}
	fmt.Println()

	return nil
}

运行示例:

bash
$ go run main.go
╔══════════════════════════════════════╗
     Go 项目初始化工具 v1.0.0
╚══════════════════════════════════════╝

? 项目名称: my-api
? 选择项目模板: Web API (Gin)
? Go 版本: 1.23
? 选择需要的模块: [配置管理 (Viper), 日志 (Zap), 数据库 (GORM)]
? 初始化 Git 仓库? Yes
? 作者名称: alice

+--------+------------------+
| 配置项 |               |
+--------+------------------+
| 项目名 | my-api           |
| 模板   | Web API (Gin)    |
| Go 版本| 1.23             |
| 模块   | 配置管理, 日志... |
| Git    | true             |
| 作者   | alice            |
+--------+------------------+

? 确认以上配置并创建项目? Yes
 创建目录结构 目录结构已创建
 生成 go.mod go.mod 已生成
...
安装依赖 100% |10| (10/10, 100%)

 项目创建成功!
  cd my-api && go mod tidy

十二、小结

本篇介绍了交互式 CLI 开发的常用技术:

  1. 读取用户输入bufio.Scanner 逐行读取,bufio.Reader 更灵活的读取。
  2. 密码输入golang.org/x/termReadPassword 关闭回显。
  3. 确认提示:手动实现 Y/N 确认,支持默认值。
  4. 多选与单选:手动实现选择菜单。
  5. survey 库:提供了 Input、Select、MultiSelect、Confirm、Password 等丰富的交互组件。
  6. 进度条schollz/progressbar 展示任务进度。
  7. Spinnerbriandowns/spinner 表示执行中状态。
  8. 表格输出olekukonko/tablewriter 格式化表格数据。
  9. 彩色输出fatih/color 实现彩色文字和样式。
  10. 完整示例:项目初始化工具综合运用了 survey、spinner、progressbar、tablewriter、color。

这些技术让 CLI 工具更加用户友好。下一篇我们将进一步探索 Bubbletea——一个基于 Elm 架构的 TUI 框架,能构建更复杂的终端界面应用。