Go 夜读群讨论实录:println 与 fmt.Print 输出乱序之谜及 Go 终端彩色输出实战
Go 夜读群讨论实录println 与 fmt.Print 输出乱序之谜及 Go 终端彩色输出实战【免费下载链接】nightWeekly Go Online Meetup via BilibiliGo 夜读通过 bilibili 在线直播的方式分享 Go 相关的技术话题每天大家在微信/telegram/Slack 上及时沟通交流编程技术话题。项目地址: https://gitcode.com/gh_mirrors/ni/night本文是『Go 夜读』微信群 2019-03-07 技术讨论的深度整理与扩展聚焦两个高频问题一是println与fmt.Print混用时输出顺序为何不确定涉及 stdout 与 stderr 两套输出流的本质二是如何基于 ANSI 转义序列在 Go 终端输出彩色文字、并封装一个带时间戳的分级彩色日志工具。读完本文你将彻底理解 Go 内置println与标准库fmt.Println的底层差异并掌握一套可直接复制到项目中的彩色日志封装方案。1 乱序之谜println 与 fmt.Print 谁先输出1.1 问题复现在 Goland 中运行下面的代码输出顺序竟然是不确定的package main import fmt func main() { println(hello) println(world) fmt.Print(go\n) }同样的代码在终端用go run运行输出顺序却又总是确定的。这是为什么1.2 答案它们写入了两个不同的输出流关键在于println输出到标准错误流 stderr而fmt.Print/fmt.Println输出到标准输出流 stdout。Goland 等 IDE 会区分 stderr 与 stdout 两个通道典型特征是 stderr 内容在 IDE 控制台中显示为红色IDE 自行渲染这两个通道时谁先被读取、被渲染并不保证与写入顺序一致因此出现乱序。而通过非 IDE如 console直接运行程序时stderr 与 stdout 最终都汇聚到同一个终端文件描述符上写入顺序得以保留因此看起来顺序确定。注意这里的顺序确定仅指同一终端合并渲染的结果从操作系统层面看两条输出流是互相独立、彼此不同步的两个文件描述符本质上不存在绝对的先后保证。1.3 从源码看两个函数的本质区别这一结论并非猜测Go 官方文档与源码注释写得很清楚。在 content/discuss/2018-07-31-println-Println-and_context.md 中『Go 夜读』群友已经扒过两个函数的源码注释println内置函数将其参数以实现相关implementation-specific的方式格式化并写入标准错误流用于引导bootstrapping和调试且不保证在语言中永久保留。// The println built-in function formats its arguments in an // implementation-specific way and writes the result to standard error. // Spaces are always added between arguments and a newline is appended. // Println is useful for bootstrapping and debugging; it is not guaranteed // to stay in the language. func println(args ...Type)fmt.Println使用默认格式格式化操作数并写入标准输出流返回写入字节数与可能出现的错误。// Println formats using the default formats for its operands and writes to standard output. // Spaces are always added between operands and a newline is appended. // It returns the number of bytes written and any write error encountered. func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...) }1.4 更深一层为什么会有两个不同步的管道在类 Unix 系统中标准输入、标准输出、标准错误分别对应编号为 0、1、2 的三个文件描述符/管道。不同管道的读写是并发的彼此之间不存在互相等待因此也就没有跨管道的先后顺序可言。println走 fd2stderrfmt.Println走 fd1stdout两条输出并发写入各自的管道。如果你写一个足够大的循环反复输出很容易观察到顺序确实是随机的——这与操作系统平台、Go 版本无关是两套独立输出流的固有行为。由此得到的工程启示调试代码时优先使用fmt.Println/标准库log它写向 stdout 且有明确、稳定的格式println写向 stderr适合快速打点排查但切勿依赖它与 stdout 输出的相对顺序在 IDE 中观察输出时stderr 往往被单独着色如红色看到乱序不要误以为是 Go 运行时或 IDE 的 bug。2 Go 终端输出彩色文字方法理解了输出流之后另一个被群友反复追问的话题是如何在 Go 终端输出带颜色的文字比如彩色日志答案依赖 ANSI 转义序列ANSI escape sequences下面给出完整方案。2.1 ANSI 转义序列终端颜色控制原理ANSI 转义序列是一种带内信号的转义序列标准用于控制视频文本终端上的光标位置、颜色以及其他选项。在文本中嵌入确定的字节序列大部分以 ESC 转义字符和[字符开始终端会把这些字节序列解释为相应指令而不是当作普通字符显示。控制字符颜色的控制符一般由ESC[开始中间包含若干个包括 0 个参数字节最后以一个最终字节结束。例如\x1b[37;44;4;1m hello go \x1b[0m表示hello go的格式为蓝色背景44、灰色字体37、带下划线4且加粗1。对应的 Go 代码如下package main import fmt func main() { fmt.Printf(\x1b[37;44;4;1m hello go \x1b[0m) }其中各部分的含义\x1b标志字符代表转义序列开始本质就是十六进制字节0x1B也是 ESC 键的 ASCII 码[转义序列的开始符以;分割的数字控制参数样式与颜色编码m结束控制字符序列SGR——Select Graphic Rendition 指令。2.2 常用文本样式控制符编码说明0重置/清除样式1加粗3斜体4下划线5闪烁8隐藏3037前景色参考下文『1 位颜色编码』38设置前景色后跟5;n代表使用 8 位 256 颜色码后跟2;r;g;b代表 24 位 RGB 颜色码40~47背景色参考下文『1 位颜色编码』48设置背景色后跟5;n代表使用 8 位 256 颜色码后跟2;r;g;b代表 24 位 RGB 颜色码9097亮色前景色参考下文『1 位颜色编码』100107亮色背景参考下文『1 位颜色编码』1 位颜色编码颜色前景色编码背景色编码黑色3040红色3141绿色3242黄色3343蓝色3444品红色3545青色3646白色灰3747亮黑色灰90100亮红色91101亮绿色92102亮黄色93103亮蓝色94104亮品红色95105亮青色96106亮白色971072.3 关于 ESC 的三种等价写法ESC控制字符在源码中有多种等价表示效果完全一致\e直接指代 Escape 字符\x1b或\x1B十六进制表示即字节0x1B\033八进制表示33 的八进制即 27是 ESC 的 ASCII 码。在 content/discuss/2018-08-09-log-color-in-go.md 中仓库还补充了一条简明的速查表\033[0m 重置为正常 \033[1m 设置高亮度或加粗 \033[4m 下划线 \033[5m 闪烁 \033[7m 反显 \033[8m 消隐 \033[30m -- \033[37m 设置前景色 \033[40m -- \033[47m 设置背景色其中\x1b[即 CSIControl Sequence Introducer控制序列引导符。例如黑色前景\x1b[30m红色前景\x1b[31m红色加粗\x1b[31;1m重置为缺省颜色\x1b[39;49m重置所有属性\x1b[0m。2.4 实战基于 ANSI 编码的彩色日志封装下面是在群讨论中沉淀下来的一个完整可运行的彩色日志示例。它用iota从 91 递增得到亮色前景编码为不同日志级别绑定不同颜色并自动拼接时间戳前缀package main import ( fmt time ) const ( color_red uint8(iota 91) color_green color_yellow color_blue color_magenta succ [succ] error [error] warn [warn] info [info] debug [debug] ) func red(s string) string { return fmt.Sprint(\x1b[%dm%s\x1b[0m, color_red, s) } func green(s string) string { return fmt.Sprintf(\x1b[%dm%s\x1b[0m, color_green, s) } func yellow(s string) string { return fmt.Sprintf(\x1b[%dm%s\x1b[0m, color_yellow, s) } func blue(s string) string { return fmt.Sprintf(\x1b[%dm%s\x1b[0m, color_blue, s) } func magenta(s string) string { return fmt.Sprintf(\x1b[%dm%s\x1b[0m, color_magenta, s) } func Success(format string, a ...interface{}) { prefix : green(succ) fmt.Println(formatLog(prefix), fmt.Sprintf(format, a...)) } func Error(format string, a ...interface{}) { prefix : red(error) fmt.Println(formatLog(prefix), fmt.Sprintf(format, a...)) } func Warning(format string, a ...interface{}) { prefix : magenta(warn) fmt.Println(formatLog(prefix), fmt.Sprintf(format, a...)) } func Info(format string, a ...interface{}) { prefix : blue(info) fmt.Println(formatLog(prefix), fmt.Sprintf(format, a...)) } func Debug(format string, a ...interface{}) { prefix : yellow(debug) fmt.Println(formatLog(prefix), fmt.Sprintf(format, a...)) } func formatLog(prefix string) string { return time.Now().Format(2006/01/02 15:04:05) prefix } func main() { Debug(%s, hello, go) }要点说明color_red uint8(iota 91)借助iota依次得到 91、92、93、94、95分别对应亮红、亮绿、亮黄、亮蓝、亮品红前景色与上文的 1 位颜色编码表一一对应每个颜色函数返回\x1b[%dm...\x1b[0m包裹的字符串\x1b[0m在末尾重置样式避免颜色泄漏到后续日志formatLog使用 Go 标准时间格式2006/01/02 15:04:05生成时间戳前缀这是 Go 对参考时间约定俗成的写法日志函数采用可变参数format string, a ...interface{}签名可像fmt.Printf一样传入格式化参数运行main中的Debug(%s, hello, go)即可在支持 ANSI 的终端看到黄色的[debug]前缀与时间戳。2.5 仓库中的配套验证代码该主题在『Go 夜读』仓库中并非孤例。早在 2018-08-09 的讨论中就沉淀了一个可直接运行的测试文件 content/discuss/log_color_test.go它用三层循环穷举所有前景色30~37、背景色40~47与样式组合0/1/4/5/7/8一次性打印出全部颜色效果for b : 40; b 47; b { for f : 30; f 37; f { for d : range []int{0, 1, 4, 5, 7, 8} { fmt.Printf( %c[%d;%d;%dm%s(f%d,b%d,d%d)%c[0m , 0x1B, d, b, f, , f, b, d, 0x1B) } fmt.Println() } fmt.Println() } fmt.Printf(%c[1;40;32m%s%c[0m, 0x1B, testPrintColor, 0x1B) fmt.Println() fmt.Printf(%s\n, testPrintColor)其中%c[1;40;32m%s%c[0m中的参数分别为1高亮、40黑色背景、32绿色前景、0恢复默认颜色。运行验证$ go test -v log_color_test.go即可在终端看到完整色卡以及一行高亮黑底绿字的testPrintColor效果。2.6 实战中的三点注意事项输出目标决定颜色是否生效ANSI 颜色只在支持转义序列的终端如 macOS Terminal、iTerm2、Linux 终端、VS Code 终端中生效重定向到文件或管道时转义字节会原样写入可能污染日志文件工程化时建议提供--color开关或自动检测isatty。务必以\x1b[0m收尾每次输出颜色后重置样式否则后续所有输出都会被上一次的颜色污染。stderr 与 stdout 的着色差异结合第 1 节的结论printlnstderr在 IDE 中显示为红色通道若再叠加 ANSI 颜色可能出现双重着色效果生产代码建议统一走fmt/log输出流把颜色仅当作终端渲染层的手段。3 小结本文围绕『Go 夜读』微信群的一次讨论梳理了两个相互关联的终端输出话题println与fmt.Print的乱序源于它们分别写入 stderr 与 stdout 两条互不同步的输出流IDE 对双通道的渲染放大了这一现象其证据可直接在 Go 内置函数与 fmt.Println 的源码注释中找到。彩色终端输出的底层是 ANSI 转义序列ESC[ 参数 m本文给出了完整的样式/颜色编码表、ESC 的三种等价写法、一个分级彩色日志封装示例以及仓库中 log_color_test.go 提供的全量色卡验证方法。相关的完整讨论还可在 2018-03-07 微信讨论 与 2018-08-09 Log 的颜色能设置吗 中继续查阅。【免费下载链接】nightWeekly Go Online Meetup via BilibiliGo 夜读通过 bilibili 在线直播的方式分享 Go 相关的技术话题每天大家在微信/telegram/Slack 上及时沟通交流编程技术话题。项目地址: https://gitcode.com/gh_mirrors/ni/night创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考