go-colorable:让 Go CLI 在 Windows 终端正确显示 ANSI 颜色
项目地址:https://github.com/mattn/go-colorable
1. 为什么需要 go-colorable
先看一个场景:给 CLI 工具加彩色输出,直接用 ANSI 转义码:
package main
import "fmt"
func main() {
// 红色文字
fmt.Println("\033[31m这是红色文字\033[0m")
// 绿色文字
fmt.Println("\033[32m这是绿色文字\033[0m")
// 蓝色文字
fmt.Println("\033[34m这是蓝色文字\033[0m")
}
在 Linux/macOS 上显示彩色,在 Windows 上会显示 \033[31m这是红色文字\033[0m 这样的原始字符。因为 Windows 的 cmd.exe 默认不支持 ANSI 转义码。
用 go-colorable 包装 stdout:
package main
import (
"fmt"
"github.com/mattn/go-colorable"
)
func main() {
// 使用 colorable 包装 stdout
stdout := colorable.NewColorableStdout()
// Windows 也能正确显示颜色
fmt.Fprintln(stdout, "\033[31m这是红色文字\033[0m")
fmt.Fprintln(stdout, "\033[32m这是绿色文字\033[0m")
fmt.Fprintln(stdout, "\033[34m这是蓝色文字\033[0m")
}
跨平台兼容问题解决。
2. go-colorable 是什么
go-colorable 是一个 Go 语言的跨平台彩色终端库。官方描述:
colorable is a wrapper for Windows console that enables ANSI escape sequences.
即 Windows 控制台包装器,启用 ANSI 转义序列,提供跨平台兼容。
核心定位:不是彩色库,也不是 TUI 框架,而是一个 ANSI 兼容层,专门解决 Windows 的 ANSI 支持问题。它做的事:
- 检测是否为 Windows
- 如果是,启用 ANSI 支持
- 包装 stdout/stderr
- 透明处理颜色输出
3. 核心特性:跨平台和透明
跨平台:
// 自动检测平台
// Windows:启用 ANSI 支持
// Linux/macOS:直接透传
stdout := colorable.NewColorableStdout()
stderr := colorable.NewColorableStderr()
透明处理:
// 像使用普通 stdout 一样使用
fmt.Fprintln(stdout, "普通文字")
fmt.Fprintln(stdout, "\033[31m红色文字\033[0m")
fmt.Fprintln(stdout, "\033[1;32m粗体绿色\033[0m")
颜色代码:
// 前景色
fmt.Fprintln(stdout, "\033[30m黑色\033[0m")
fmt.Fprintln(stdout, "\033[31m红色\033[0m")
fmt.Fprintln(stdout, "\033[32m绿色\033[0m")
fmt.Fprintln(stdout, "\033[33m黄色\033[0m")
fmt.Fprintln(stdout, "\033[34m蓝色\033[0m")
fmt.Fprintln(stdout, "\033[35m紫色\033[0m")
fmt.Fprintln(stdout, "\033[36m青色\033[0m")
fmt.Fprintln(stdout, "\033[37m白色\033[0m")
// 背景色
fmt.Fprintln(stdout, "\033[41m红色背景\033[0m")
fmt.Fprintln(stdout, "\033[42m绿色背景\033[0m")
// 样式
fmt.Fprintln(stdout, "\033[1m粗体\033[0m")
fmt.Fprintln(stdout, "\033[4m下划线\033[0m")
4. 安装和使用
安装:
go get github.com/mattn/go-colorable
包装 stdout/stderr
package main
import (
"fmt"
"github.com/mattn/go-colorable"
)
func main() {
stdout := colorable.NewColorableStdout()
stderr := colorable.NewColorableStderr()
fmt.Fprintln(stdout, "\033[32m成功信息\033[0m")
fmt.Fprintln(stderr, "\033[31m错误信息\033[0m")
}
配合 log 包使用
package main
import (
"log"
"github.com/mattn/go-colorable"
)
func main() {
// 设置日志输出到 colorable
log.SetOutput(colorable.NewColorableStdout())
log.Println("\033[32m[INFO]\033[0m 这是信息日志")
log.Println("\033[33m[WARN]\033[0m 这是警告日志")
log.Println("\033[31m[ERROR]\033[0m 这是错误日志")
}
自定义 Writer
package main
import (
"fmt"
"os"
"github.com/mattn/go-colorable"
)
func main() {
// 包装任意 writer
writer := colorable.NewColorable(os.Stdout)
fmt.Fprintln(writer, "\033[36m青色文字\033[0m")
fmt.Fprintln(writer, "\033[35m紫色文字\033[0m")
}
检测是否支持颜色
package main
import (
"fmt"
"os"
"github.com/mattn/go-isatty"
)
func main() {
// 检测 stdout 是否为终端
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Println("\033[32m终端支持颜色\033[0m")
} else {
fmt.Println("非终端,不输出颜色")
}
}
5. 核心功能
go-colorable 定位是 ANSI 兼容层,配合其他库可以完成更完整的功能。
配合 go-color
package main
import (
"github.com/fatih/color"
)
func main() {
// go-color 内部使用 go-colorable
color.Red("这是红色")
color.Green("这是绿色")
color.Blue("这是蓝色")
// 自定义样式
customStyle := color.New(color.FgCyan, color.Bold)
customStyle.Println("这是粗体青色")
}
256 色支持
func main() {
stdout := colorable.NewColorableStdout()
// 256 色
for i := 0; i < 256; i++ {
fmt.Fprintf(stdout, "\033[38;5;%dm%d ", i, i)
if (i+1)%16 == 0 {
fmt.Fprintln(stdout, "\033[0m")
}
}
}
RGB 真彩色
func main() {
stdout := colorable.NewColorableStdout()
// RGB 真彩色
fmt.Fprintln(stdout, "\033[38;2;255;0;0m红色\033[0m")
fmt.Fprintln(stdout, "\033[38;2;0;255;0m绿色\033[0m")
fmt.Fprintln(stdout, "\033[38;2;0;0;255m蓝色\033[0m")
}
光标控制
func main() {
stdout := colorable.NewColorableStdout()
// 移动光标
fmt.Fprint(stdout, "\033[2J") // 清屏
fmt.Fprint(stdout, "\033[H") // 移动到家
fmt.Fprintln(stdout, "第一行")
fmt.Fprint(stdout, "\033[2;1H") // 移动到第2行第1列
fmt.Fprintln(stdout, "第二行")
}
6. 实战场景
场景一:彩色日志系统
package main
import (
"fmt"
"log"
"time"
"github.com/mattn/go-colorable"
)
type Logger struct {
infoLog *log.Logger
warnLog *log.Logger
errorLog *log.Logger
}
func NewLogger() *Logger {
stdout := colorable.NewColorableStdout()
stderr := colorable.NewColorableStderr()
return &Logger{
infoLog: log.New(stdout, "", 0),
warnLog: log.New(stdout, "", 0),
errorLog: log.New(stderr, "", 0),
}
}
func (l *Logger) Info(format string, args ...interface{}) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
msg := fmt.Sprintf(format, args...)
l.infoLog.Printf("\033[32m[INFO]\033[0m %s %s", timestamp, msg)
}
func (l *Logger) Warn(format string, args ...interface{}) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
msg := fmt.Sprintf(format, args...)
l.warnLog.Printf("\033[33m[WARN]\033[0m %s %s", timestamp, msg)
}
func (l *Logger) Error(format string, args ...interface{}) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
msg := fmt.Sprintf(format, args...)
l.errorLog.Printf("\033[31m[ERROR]\033[0m %s %s", timestamp, msg)
}
func main() {
logger := NewLogger()
logger.Info("服务器启动成功")
logger.Warn("内存使用率超过 80%%")
logger.Error("数据库连接失败")
}
效果:INFO 绿色,WARN 黄色,ERROR 红色。
场景二:测试结果展示
package main
import (
"fmt"
"github.com/mattn/go-colorable"
)
type TestResult struct {
Name string
Passed bool
}
func displayResults(results []TestResult) {
stdout := colorable.NewColorableStdout()
passed := 0
failed := 0
for _, r := range results {
if r.Passed {
fmt.Fprintf(stdout, "\033[32m✓\033[0m %s\n", r.Name)
passed++
} else {
fmt.Fprintf(stdout, "\033[31m✗\033[0m %s\n", r.Name)
failed++
}
}
fmt.Fprintln(stdout)
fmt.Fprintf(stdout, "总计:%d 通过,%d 失败\n", passed, failed)
if failed == 0 {
fmt.Fprintf(stdout, "\033[32m所有测试通过!\033[0m\n")
} else {
fmt.Fprintf(stdout, "\033[31m有测试失败\033[0m\n")
}
}
func main() {
results := []TestResult{
{"测试用户登录", true},
{"测试数据验证", true},
{"测试权限检查", false},
{"测试数据持久化", true},
}
displayResults(results)
}
效果:通过绿色 ✓,失败红色 ✗。
场景三:进度显示
package main
import (
"fmt"
"time"
"github.com/mattn/go-colorable"
)
func showProgress(total int) {
stdout := colorable.NewColorableStdout()
for i := 0; i <= total; i++ {
percent := float64(i) / float64(total) * 100
// 根据进度显示不同颜色
var color string
if percent < 30 {
color = "\033[31m" // 红色
} else if percent < 70 {
color = "\033[33m" // 黄色
} else {
color = "\033[32m" // 绿色
}
fmt.Fprintf(stdout, "\r%s%.1f%%\033[0m", color, percent)
time.Sleep(50 * time.Millisecond)
}
fmt.Fprintln(stdout)
}
func main() {
fmt.Println("下载进度:")
showProgress(100)
fmt.Println("\033[32m下载完成!\033[0m")
}
效果:进度从红到黄到绿。
7. 设计亮点
- 跨平台兼容:自动处理 Windows ANSI 支持,一套代码到处运行,无需平台判断,透明处理。
- 零配置:开箱即用,无需配置,集成快,减少错误。
- 轻量级:代码量小,无依赖,编译快,体积小,易于维护。
- 标准兼容:使用标准 ANSI 转义码,学习成本低,文档丰富,工具支持广。
- 生态完善:可配合 go-color、go-isatty 等库使用,功能丰富,灵活扩展,社区支持。
8. 和类似方案对比
vs. go-color
| 维度 | go-colorable | go-color |
|---|---|---|
| 定位 | ANSI 兼容 | 彩色 API |
| 使用 | 底层包装 | 高层 API |
| 关系 | 基础设施 | 基于 colorable |
go-colorable 是基础,go-color 更高级。
vs. 直接 ANSI
| 维度 | go-colorable | 直接 ANSI |
|---|---|---|
| 跨平台 | 支持 | Windows 不支持 |
| 兼容性 | 好 | 差 |
| 易用性 | 高 | 低 |
在跨平台场景下 go-colorable 更合适。
vs. 其他方案
| 维度 | go-colorable | ansicolor |
|---|---|---|
| Star | 1.3K | 500 |
| 维护 | 活跃 | 较少 |
| 生态 | 完善 | 有限 |
在维护活跃度和生态上 go-colorable 更占优。
9. 局限性
- 功能单一:只做 ANSI 兼容
- 需要配合:需要配合其他库使用
- Windows 限制:某些旧 Windows 不支持
- 终端限制:某些终端不支持 256 色
- 性能开销:包装有一定开销
- 调试困难:颜色代码调试较难
10. 总结
go-colorable 解决的问题很具体:Windows 终端的 ANSI 兼容。它的设计思路:
- 跨平台很重要:一套代码到处运行
- 兼容性是基础:解决平台差异
- 简单设计有价值:专注解决一个问题
- 生态完善很重要:配合其他库使用
- 小工具解决大问题:看似简单,实则关键
如果正在处理 Windows 的 ANSI 支持问题,go-colorable 是一个可用的方案。它让 CLI 工具在不同平台上输出一致的颜色。
参考资料: