第一章:Go语言显示颜色不一样的根本原因
Go语言本身并不内置终端颜色支持,所谓“显示颜色不一样”完全取决于运行时环境对ANSI转义序列的解析能力,而非Go编译器或标准库的主动着色行为。根本原因在于:Go程序输出的彩色文本实际是向标准输出(stdout)写入了符合ECMA-48标准的ANSI控制码,而最终是否呈现为颜色,由终端模拟器(如iTerm2、Windows Terminal、GNOME Terminal)决定——它是否启用并正确渲染这些转义序列。
终端能力差异导致颜色表现不一致
不同终端对ANSI序列的支持程度存在显著差异:
- 现代终端(如Windows Terminal v1.15+)默认支持256色及真彩色(16M色)
- 旧版CMD或某些SSH会话仅识别基础16色(
\033[31m红,\033[32m绿等) - 部分IDE内嵌终端(如VS Code早期版本)可能禁用颜色渲染以提升性能
Go代码中实现颜色的典型方式
使用fmt.Printf直接输出ANSI序列是最轻量级方案:
package main
import "fmt"
func main() {
// 基础红字:\033[31m 开启红色,\033[0m 重置样式
fmt.Printf("\033[31m错误:连接超时\033[0m\n")
// 真彩色示例(需终端支持):RGB(255, 105, 180) 粉红
fmt.Printf("\033[38;2;255;105;180m高亮提示\033[0m\n")
}
注意:
fmt.Print系列函数不进行任何转义处理,因此\033会被原样传递给终端;若在不支持ANSI的环境(如记事本打开log文件)中查看,将显示乱码字符。
验证当前终端颜色支持能力
可通过以下命令快速检测:
# 检查TERM环境变量(常见值:xterm-256color, screen-256color)
echo $TERM
# 测试256色表(执行后观察是否显示完整渐变色块)
awk 'BEGIN{for(i=0;i<256;i++) printf "\033[48;5;%dm%3d\033[0m", i, i; print ""}'
| 检测项 | 推荐值 | 不满足时的表现 |
|---|---|---|
TERM变量 |
xterm-256color |
dumb 或 unknown → 无色 |
COLORTERM |
truecolor 或 24bit |
缺失 → 可能降级为16色 |
stdout是否为终端 |
isatty(1) 返回真 |
重定向到文件时颜色失效 |
第二章:终端能力检测机制的底层原理与Go实现
2.1 ANSI转义序列在不同终端环境中的解析差异
ANSI转义序列的渲染行为高度依赖终端实现,同一序列在不同环境中可能被忽略、截断或误解析。
常见兼容性断层点
ESC[?25h(显示光标)在 Windows Terminal v1.11+ 中生效,但旧版 ConHost 仅支持ESC[?25lESC[38;2;R;G;Bm(24位真彩色)在 iTerm2 和 VS Code 终端中完整支持,在 GNOME Terminal 3.36+ 中需启用enable_true_color- 部分嵌入式串口终端(如 minicom)仅识别 7-bit ASCII 控制字符,会丢弃高位字节
兼容性检测示例
# 检测终端是否支持真彩色
if [[ $COLORTERM = "truecolor" ]] || [[ $TERM_PROGRAM = "iTerm.app" ]]; then
echo -e "\e[38;2;255;0;128m真彩支持✅\e[0m"
else
echo -e "\e[31m降级为256色\e[0m"
fi
该脚本通过环境变量组合判断渲染能力:$COLORTERM 是终端明确声明的能力标识,$TERM_PROGRAM 提供客户端上下文;echo -e 启用转义解析,\e 等价于 \033(ESC 字符)。
| 终端环境 | CSI m 支持度 |
SGR 重置行为 | 备注 |
|---|---|---|---|
| Windows ConHost | ✅ 基础16色 | ✅ 完全重置 | 不支持 RGB/RGB24 |
| Alacritty 0.12 | ✅ 全集 | ⚠️ 部分缓存 | ESC[0m 可能不重置背景 |
| tmux 3.3a | ✅(经转义代理) | ✅ | 实际由底层终端决定最终效果 |
graph TD
A[应用输出 ESC[38;2;255;0;0m] --> B{终端解析层}
B -->|支持RGB| C[渲染纯红文字]
B -->|仅支持256色| D[映射至最近色号 196]
B -->|忽略参数| E[回退至默认前景色]
2.2 Go标准库中os.Stdout.Fd()与isatty判断的实践陷阱
终端检测的常见误用
os.Stdout.Fd() 返回文件描述符,但不保证其指向终端;直接传给 isatty.IsTerminal() 可能因重定向失效:
// ❌ 危险:未检查 Stdout 是否为真实终端输出
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Println("\033[1;32mHello\033[0m") // 彩色输出
}
逻辑分析:
os.Stdout.Fd()在管道(cmd | grep)或重定向(go run main.go > out.txt)时仍返回有效 fd(如 1),但isatty.IsTerminal(1)返回false—— 此处需显式判空或结合os.Stdin.Stat().Mode() & os.ModeCharDevice辅证。
多场景兼容性对照
| 场景 | IsTerminal(Fd()) |
Stdout.Stat().Mode() & os.ModeCharDevice |
|---|---|---|
| 直接终端运行 | true |
true |
> file.log |
false |
false |
| cat(管道) |
false |
false |
安全检测流程
graph TD
A[调用 os.Stdout.Fd()] --> B{fd >= 0?}
B -->|否| C[视为非终端]
B -->|是| D[调用 isatty.IsTerminal(fd)]
D -->|true| E[启用ANSI转义]
D -->|false| F[降级为纯文本]
2.3 Docker默认TTY配置与pty分配机制对颜色输出的影响
Docker 容器默认是否分配伪终端(PTY),直接决定 ANSI 颜色序列能否被正确解析与渲染。
TTY 分配行为差异
docker run -t:强制分配 TTY,启用isatty(1),多数 CLI 工具(如ls --color=auto、grep --color=auto)自动启用颜色;docker run(无-t):标准输出为管道,isatty()返回 false,颜色被禁用;docker run -t -i:交互式 TTY,支持信号传递与行缓冲优化。
颜色输出控制对比表
| 运行方式 | isatty(stdout) |
ls --color=auto |
grep --color=auto |
|---|---|---|---|
docker run alpine ls |
❌ | 无色 | 无色 |
docker run -t alpine ls |
✅ | 彩色 | 彩色 |
# 检查容器内 stdout 是否为 TTY
docker run alpine sh -c 'if [ -t 1 ]; then echo "TTY allocated"; else echo "No TTY"; fi'
# 输出:No TTY → 因未指定 -t,/dev/stdout 指向 pipe 而非 /dev/pts/N
该判断逻辑由 libc 的 fstat() 检查文件描述符关联的设备类型触发,是 color auto-detection 的底层依据。
2.4 runtime.GOOS与runtime.GOARCH对终端能力推断的干扰验证
runtime.GOOS 和 runtime.GOARCH 仅反映构建时目标平台,而非运行时真实环境,易导致终端能力误判。
常见误用场景
- 将
GOOS == "linux"直接等价于“支持epoll”; - 认为
GOARCH == "amd64"意味着“具备 AVX2 指令集”。
干扰验证代码
package main
import (
"fmt"
"runtime"
"runtime/debug"
)
func main() {
fmt.Printf("GOOS=%s, GOARCH=%s\n", runtime.GOOS, runtime.GOARCH)
if info, ok := debug.ReadBuildInfo(); ok {
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" {
fmt.Printf("Built from commit: %s\n", setting.Value)
}
}
}
}
该代码输出构建元信息,但不包含 CPU 特性、内核版本或容器运行时标识。GOOS/GOARCH 是静态常量,无法反映容器中运行在 linux/amd64 但内核为 5.4(无 io_uring)或 CPU 不支持 RDRAND 的真实约束。
典型干扰组合对比
| 构建环境 | 运行环境 | 是否支持 io_uring |
推断是否可靠 |
|---|---|---|---|
| linux/amd64 | WSL2 (kernel 5.15) | ✅ | 可靠 |
| linux/amd64 | Alpine Linux 3.18 (kernel 5.10) | ❌(未启用) | 不可靠 |
graph TD
A[编译时 GOOS/GOARCH] --> B[静态目标平台标识]
B --> C{是否等于运行时能力?}
C -->|否| D[需运行时探测:uname -r, cpuid, /proc/sys/fs/aio-max-nr]
C -->|是| E[偶然一致,不可依赖]
2.5 通过strace和gdb动态追踪fmt.Print颜色失效的系统调用链
当 fmt.Print("\033[32mOK\033[0m") 在终端显示为纯文本(无绿色),问题常源于 ANSI 转义序列被截断或写入失败。
追踪 write 系统调用行为
使用 strace -e write,writev,ioctl go run main.go 2>&1 | grep -A2 write 可捕获实际写出的字节数:
write(1, "\33[32mOK\33[0m", 12) = 12 # ✅ 正常写出全部12字节
# 若返回值 < 12(如 =8),说明部分转义序列被丢弃
write() 返回值小于请求长度,表明内核未完整接受数据——常见于管道、重定向或非TTY环境。
关键环境判定
ioctl(fd, TIOCGWINSZ, ...) 和 isatty(fd) 决定是否启用颜色: |
环境 | isatty(1) | 输出颜色 | 原因 |
|---|---|---|---|---|
| 终端直连 | true | ✅ | stdlib 启用 ANSI | |
go run ... \| cat |
false | ❌ | os.Stdout 自动禁用 |
gdb 断点验证逻辑流
(gdb) b runtime.write
(gdb) r
(gdb) p $rdi # 检查 fd 是否为 1(stdout)
(gdb) x/12xb $rsi # 查看缓冲区原始字节:0x1b 0x5b 0x33 0x32 0x6d...
若 $rsi 中 0x1b 0x5b(ESC [)存在但 write 返回值异常,则问题在内核层或终端驱动。
graph TD A[fmt.Print] –> B[os.Stdout.Write] B –> C{isatty(STDOUT_FILENO)?} C –>|true| D[保留ANSI序列] C –>|false| E[strip color codes] D –> F[syscall write(1, buf, len)] F –> G[内核TTY层处理]
第三章:Docker容器内颜色渲染失效的三大典型场景
3.1 alpine镜像中缺少terminfo数据库导致的颜色支持缺失
Alpine Linux 因其极简设计,默认不包含 ncurses 的 terminfo 数据库(/usr/share/terminfo),导致 ls --color、grep --color、tput 等依赖终端能力查询的命令无法识别 xterm-256color 等类型,进而禁用颜色输出。
根本原因定位
# 检查 terminfo 是否存在
ls /usr/share/terminfo/x/xterm-256color 2>/dev/null || echo "missing"
# 输出:missing → 表明数据库未安装
该命令验证 terminfo 条目缺失;2>/dev/null 抑制错误输出,仅保留逻辑结果。xterm-256color 是多数 SSH 终端(如 iTerm2、VS Code Terminal)上报的 $TERM 值,缺失则 tput colors 返回 0。
解决方案对比
| 方案 | 命令 | 镜像体积增量 | 是否持久生效 |
|---|---|---|---|
| 安装完整 ncurses | apk add ncurses-terminfo-base |
~1.2 MB | ✅ |
| 轻量替代 | apk add ncurses-terminfo |
~2.8 MB | ✅ |
| 运行时注入 | infocmp xterm-256color > /tmp/xterm.ti && tic /tmp/xterm.ti |
0 MB(需 root) | ❌(容器重启丢失) |
推荐修复流程
FROM alpine:3.20
RUN apk add --no-cache ncurses-terminfo-base
ENV TERM=xterm-256color
ncurses-terminfo-base 提供常用终端定义(含 xterm*, screen*, linux),体积最小且满足 CI/CD 和交互式 shell 双场景需求。
3.2 docker run -t 与 -it 参数对TERM环境变量注入的差异实测
TERM 变量注入机制
Docker 在容器启动时,仅当分配伪终端(PTY)时才自动注入 TERM 环境变量。-t 强制分配 TTY,-i 保持 STDIN 打开,二者组合 -it 才触发完整交互式终端行为。
实测对比
# 仅 -t:分配 TTY,但不保证 STDIN 可读 → TERM 注入成功
docker run -t --rm alpine sh -c 'echo $TERM'
# 输出:xterm
# 仅 -i:无 TTY 分配 → TERM 为空
docker run -i --rm alpine sh -c 'echo "[$TERM]"'
# 输出:[]
-t触发libcontainer的setupConsole流程,调用os.Setenv("TERM", "xterm");-i单独使用不触发该逻辑。
关键差异总结
| 参数组合 | 分配 TTY | 注入 TERM | 交互式 Shell |
|---|---|---|---|
-t |
✅ | ✅ | ❌(STDIN 关闭) |
-i |
❌ | ❌ | ❌(无 TTY) |
-it |
✅ | ✅ | ✅ |
graph TD
A[docker run] --> B{含 -t?}
B -->|是| C[调用 setupConsole → 设置 TERM=xterm]
B -->|否| D[跳过 TERM 注入]
C --> E[TERM 可见于容器环境]
3.3 多阶段构建中build-stage与run-stage终端能力继承断裂分析
Docker 多阶段构建中,build-stage 与 run-stage 是隔离的执行环境,终端能力(如 TTY 分配、信号转发、ANSI 控制序列支持)无法自动继承。
终端能力断裂根源
- 构建阶段使用
docker build --no-cache启动时默认分配伪 TTY(-t),但RUN指令在后台容器中执行,无交互式终端上下文; FROM alpine:latest AS run-stage启动的最终镜像默认不启用stdin_open和tty,导致docker run -it仍无法还原构建时的终端语义。
典型复现代码块
# 构建阶段:可正常输出彩色日志(依赖 TTY)
FROM node:18 AS build-stage
RUN echo -e "\033[32m[INFO]\033[0m Building..." && \
echo "BUILD_TTY=$TERM" # 输出:xterm 或 dumb
# 运行阶段:TTY 环境变量丢失,ANSI 被忽略
FROM alpine:3.19 AS run-stage
COPY --from=build-stage /app/dist /app
CMD ["sh", "-c", "echo -e '\033[31m[ERROR]\033[0m No TTY'; echo \"RUN_TTY=$TERM\""]
逻辑分析:
build-stage中TERM由构建守护进程注入(如dockerd设置为xterm),但run-stage的CMD在无-t的容器中启动,默认TERM=dumb,且stdout为非终端文件描述符(isatty(1) == false),导致所有 ANSI 序列被静默丢弃。关键参数:--tty(-t)仅影响docker run,不传递至FROM镜像内部。
能力继承对比表
| 能力项 | build-stage | run-stage(默认) | 是否可显式恢复 |
|---|---|---|---|
TERM 变量 |
xterm |
dumb |
✅ docker run -e TERM=xterm |
| ANSI 渲染 | ✅ | ❌ | ✅ 需 -t + TERM |
SIGWINCH 传递 |
✅(构建器内) | ❌(无 TTY) | ✅ docker run -t |
graph TD
A[build-stage RUN] -->|创建产物| B[中间镜像层]
B --> C[run-stage CMD]
C --> D{容器启动模式}
D -->|docker run -it| E[分配 TTY → ANSI 生效]
D -->|docker run| F[无 TTY → TERM=dumb → ANSI 丢弃]
第四章:生产级Go应用颜色输出的健壮性解决方案
4.1 使用github.com/mattn/go-isatty进行运行时TTY智能检测
为什么需要 TTY 检测?
命令行工具需区分终端直连(支持颜色、光标控制)与管道/重定向(应禁用 ANSI 转义)。硬编码 os.Stdout.Fd() 判断不可靠,go-isatty 提供跨平台、内核级的准确检测。
快速集成示例
package main
import (
"fmt"
"os"
"github.com/mattn/go-isatty"
)
func main() {
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
fmt.Print("\033[32mHello, TTY!\033[0m\n") // 彩色输出
} else {
fmt.Println("Hello, pipe/file!")
}
}
逻辑分析:
IsTerminal()调用ioctl(TIOCGWINSZ)(Unix)或GetConsoleMode()(Windows)验证句柄是否关联交互式终端;IsCygwinTerminal()额外兼容 Cygwin/msys2 环境。两者组合覆盖主流场景。
检测能力对比
| 环境 | IsTerminal() |
IsCygwinTerminal() |
推荐组合调用 |
|---|---|---|---|
| Linux/macOS 终端 | ✅ | ❌ | || |
| Windows CMD | ✅ | ❌ | || |
| Git Bash | ❌ | ✅ | || |
cmd | less |
❌ | ❌ | 安全降级 |
graph TD
A[获取 os.Stdout.Fd()] --> B{IsTerminal?}
B -->|Yes| C[启用 ANSI]
B -->|No| D{IsCygwinTerminal?}
D -->|Yes| C
D -->|No| E[纯文本模式]
4.2 封装color.Writer适配器,自动降级为灰度文本的工程实践
在终端能力不确定的CI/CD环境或老旧TTY中,彩色输出常导致乱码或截断。我们通过封装 color.Writer 实现运行时智能降级。
核心适配器设计
type GrayFallbackWriter struct {
w io.Writer
colorer *color.Color
enabled bool // 由 isTerminal() + TERM 检测动态决定
}
func (g *GrayFallbackWriter) Write(p []byte) (n int, err error) {
if !g.enabled {
// 移除ANSI转义序列,保留纯文本内容
clean := ansi.Strip(p)
return g.w.Write(clean)
}
return g.colorer.Write(p)
}
逻辑分析:enabled 标志由 os.Getenv("TERM") 和 isatty.IsTerminal() 共同决策;ansi.Strip() 使用正则 \x1b\[[0-9;]*m 安全剥离所有SGR控制码,不依赖外部库。
降级策略对比
| 场景 | 彩色输出 | 灰度输出 | 体验保障 |
|---|---|---|---|
| macOS iTerm2 | ✅ | — | 高亮语义 |
| Linux systemd-journald | ❌(乱码) | ✅ | 可读性优先 |
| Windows GitHub Actions | ❌(部分支持差) | ✅ | 兼容性兜底 |
自动检测流程
graph TD
A[Write调用] --> B{g.enabled?}
B -->|true| C[原样调用colorer.Write]
B -->|false| D[ansi.Strip → 写入底层Writer]
4.3 在CI/CD流水线中注入TERM=xterm-256color并验证颜色支持
在容器化构建环境中,多数CI运行器(如GitHub Actions、GitLab Runner)默认使用哑终端(TERM=dumb),导致ANSI颜色被抑制。需显式注入环境变量以启用真彩色支持。
为什么需要 xterm-256color?
xterm-256color告知应用终端支持256色调色板;- 工具如
ls --color=auto、grep --color=always、tput依赖此变量决定是否输出ANSI转义序列。
注入方式示例(GitLab CI)
job:
script:
- export TERM=xterm-256color
- echo "Testing color: $(tput setaf 2)GREEN$(tput sgr0)"
✅
export TERM=...作用于当前shell会话;
✅tput setaf 2输出绿色前景色控制序列;
❌ 若未设置TERM,tput可能静默失败或回退为无色。
验证颜色是否生效
| 检查项 | 命令 | 期望输出 |
|---|---|---|
| TERM值 | echo $TERM |
xterm-256color |
| 色彩能力检测 | tput colors |
256 |
| 实际ANSI渲染测试 | printf '\033[38;5;42mHello\033[0m\n' |
绿色”Hello” |
graph TD
A[CI作业启动] --> B[注入 TERM=xterm-256color]
B --> C[运行带颜色的CLI工具]
C --> D{tput colors == 256?}
D -->|是| E[保留ANSI序列输出]
D -->|否| F[降级为单色]
4.4 基于go test -v输出的彩色日志分级控制策略(debug/info/warn/error)
Go 标准测试框架本身不支持彩色日志或多级日志,需借助 testing.T.Log / testing.T.Logf 结合终端 ANSI 转义序列与环境感知实现分级着色。
日志级别映射与 ANSI 编码
| 级别 | ANSI 前景色 | 示例用途 |
|---|---|---|
| debug | \033[36m(青) |
详细变量快照、路径追踪 |
| info | \033[32m(绿) |
测试流程关键节点 |
| warn | \033[33m(黄) |
非失败但需关注的边界行为 |
| error | \033[31m(红) |
断言失败前的上下文诊断信息 |
彩色日志封装示例
func Logf(t *testing.T, level string, format string, args ...any) {
prefix := map[string]string{
"debug": "\033[36m[DEBUG]\033[0m",
"info": "\033[32m[INFO]\033[0m",
"warn": "\033[33m[WARN]\033[0m",
"error": "\033[31m[ERROR]\033[0m",
}[level]
t.Logf("%s "+format, append([]any{prefix}, args...)...)
}
该函数将 level 映射为带颜色前缀的字符串,并透传至 t.Logf;"\033[0m" 重置样式,避免污染后续输出;所有输出仍受 -v 控制,非 verbose 模式下被静默丢弃。
执行时序逻辑
graph TD
A[go test -v] --> B{是否启用 -v?}
B -->|是| C[调用 t.Logf]
B -->|否| D[跳过所有 Logf 输出]
C --> E[ANSI 渲染到终端]
第五章:未来演进与跨平台一致性保障
随着 WebAssembly(Wasm)运行时在边缘设备、Serverless 环境及桌面客户端中的深度集成,跨平台一致性已从“兼容性目标”升级为“交付基线”。某头部金融级低代码平台在 2023 年 Q4 启动的“OneEngine”项目即为此类演进的典型实践:其核心表达式引擎原依赖 JavaScript + WebView 桥接,在 iOS/Android/macOS/Windows 四端出现浮点精度偏差(如 0.1 + 0.2 !== 0.3 在 Safari WebKit 与 Chromium V8 中表现不一),导致风控规则在移动端误触发率高达 0.7%。
构建可验证的字节码契约
该平台将表达式求值逻辑编译为 Wasm 字节码(通过 Rust + wasm-pack),并定义二进制接口规范(.wit 文件):
default world calculator {
export eval: func(
expr: string,
context: list<u8>
) -> result<f64, string>;
}
所有平台均通过 WASI SDK 加载同一 .wasm 文件,彻底消除 JS 引擎差异。实测显示:iOS 17.4、Android 14、Windows 11 (ARM64) 与 macOS Sonoma 下的 eval("1e-16 + 1") 返回值标准差为 0.0。
自动化一致性巡检流水线
团队在 CI 中嵌入跨平台比对任务,每日执行 12,843 个测试用例(覆盖 IEEE 754 边界值、Unicode 标识符、时区敏感函数等),结果以表格形式归档:
| 平台 | 测试总数 | 失败数 | 主要失败类型 | 最后通过时间 |
|---|---|---|---|---|
| iOS Simulator | 12843 | 0 | — | 2024-06-12T03:14Z |
| Android Emulator | 12843 | 0 | — | 2024-06-12T03:15Z |
| Windows WSL2 | 12843 | 2 | Intl.DateTimeFormat 时区解析差异 |
2024-06-12T03:16Z |
运行时沙箱策略协同
针对平台特有的“用户自定义函数注入”场景,采用双层隔离:Wasm 模块运行于 WASI-capable runtime(如 Wasmtime),而外部 JS 函数调用则经由预注册的 host function 表白名单控制。以下 Mermaid 流程图描述了函数调用链路安全校验逻辑:
flowchart LR
A[JS 调用 eval] --> B{Wasm 导出函数入口}
B --> C[检查 context 参数结构]
C --> D[验证函数名是否在 allowlist.json 中]
D --> E[调用 Host Function Wrapper]
E --> F[执行 sandboxed JS 函数]
F --> G[返回结果至 Wasm 内存]
静态资源哈希锚定机制
为防止 CDN 缓存导致的 JS/Wasm 版本错配,构建系统生成 manifest.json 并嵌入 HTML 的 <meta> 标签:
<meta name="wasm-integrity" content="sha256-9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08">
加载器强制校验 Wasm 文件 SHA256 值,校验失败则触发降级至本地 fallback bundle。
动态 ABI 兼容性迁移
当平台需升级 Rust std 库版本时,通过 wasm-bindgen 的 --no-typescript 模式生成 TypeScript 类型声明,并利用 tsc --noEmit --watch 实时检测 ABI 变更。一次 std::collections::HashMap 迭代器 API 调整导致 17 个前端组件编译报错,CI 系统自动标记受影响模块并推送修复建议 PR。
长期演进路线图
团队已启动 WASI-NN(WebAssembly System Interface for Neural Networks)适配,计划将模型推理能力下沉至 Wasm 层;同时与 Bytecode Alliance 合作推进 wasi-http 标准落地,目标在 2025 年实现 HTTP 客户端行为在所有支持 WASI 的运行时中完全一致。
