Appearance
交互式 CLI 与 TUI 基础
传统 CLI 工具通过参数和标志接收输入,但有些场景需要与用户进行交互式对话:确认操作、选择选项、输入密码等。本篇将介绍如何使用 Go 标准库和第三方库构建交互式 CLI 工具,涵盖用户输入、密码读取、确认提示、多选单选、进度条、Spinner、表格输出和彩色输出等技术。
一、交互式 CLI 概述
交互式 CLI 是指在运行过程中与用户进行问答式交互的命令行程序。与"一次性接收所有参数然后执行"的传统 CLI 不同,交互式 CLI 会:
- 逐步引导用户完成操作(类似安装向导)
- 在关键操作前请求确认(如删除前确认)
- 提供选择菜单而非要求用户记住参数
- 实时反馈进度(进度条、Spinner)
典型的交互式 CLI 例子:npm create vite、git init、docker 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.Reader 比 Scanner 更灵活,可以按字节读取、按行读取、读取指定长度等。
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)
}
Scanner和Reader的区别:Scanner更简单,适合逐行扫描;Reader更底层,支持更细粒度的控制。大多数场景用Scanner就够了。
三、密码输入
密码输入不应该在终端回显。golang.org/x/term 包提供了 ReadPassword 函数,在读取密码时会关闭终端回显。
安装:
bash
go get golang.org/x/term@latestgo
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@latest1. 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@latestgo
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@latestgo
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@latestgo
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@latestgo
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 开发的常用技术:
- 读取用户输入:
bufio.Scanner逐行读取,bufio.Reader更灵活的读取。 - 密码输入:
golang.org/x/term的ReadPassword关闭回显。 - 确认提示:手动实现 Y/N 确认,支持默认值。
- 多选与单选:手动实现选择菜单。
- survey 库:提供了 Input、Select、MultiSelect、Confirm、Password 等丰富的交互组件。
- 进度条:
schollz/progressbar展示任务进度。 - Spinner:
briandowns/spinner表示执行中状态。 - 表格输出:
olekukonko/tablewriter格式化表格数据。 - 彩色输出:
fatih/color实现彩色文字和样式。 - 完整示例:项目初始化工具综合运用了 survey、spinner、progressbar、tablewriter、color。
这些技术让 CLI 工具更加用户友好。下一篇我们将进一步探索 Bubbletea——一个基于 Elm 架构的 TUI 框架,能构建更复杂的终端界面应用。