Go程序在Termux中的日志与时区踩坑
引子
古人说"察其所安",意思是观察一个人安于什么处境,便知其品性。程序也是一样——它安于沉默,你就只能在黑暗中摸索。
把一个 Go 编写的 ADB UI 自动化工具部署到 Android Termux 环境之后,我遇到了两个不声不响的故障。它们不抛异常,不给堆栈,只是安静地产出错误的结果。一个让我失去了定位问题的能力,另一个让我失去了对时间的感知。这两样东西恰好是调试的全部依靠。
坑一:没有分级日志,UI 自动化故障只能猜
现象
程序输出只有简单的 fmt.Println,报错信息像一封被删去了所有细节的电报:
警告: 睡眠详情截取失败: 未找到睡眠卡片
[运动] 从运动tab页截取到 0 条记录摘要
"未找到"三个字背后,是至少五种可能的原因:App 没启动?UI 节点属性变了?屏幕还锁着?控件的 clickable 从 true 变成了 false?还是 content-desc 的文案被产品经理改了?
全靠猜。像是在考古现场,只看到一片碎片,却要还原整个器型。
根因
Go 标准库没有像 Python logging 那样开箱即用的分级日志。fmt.Println 缺少时间戳、调用位置、级别控制,线上问题只能盲猜。
解决方案
基于 runtime.Caller 实现轻量日志模块,输出格式:
2026-02-11 16:05:24 [INFO ] sleep.go:80 | [睡眠] 候选节点: desc="无睡眠数据", clickable=true
核心代码:
| |
通过环境变量 MIHEALTH_LOG_LEVEL 控制级别(DEBUG/INFO/WARN/ERROR),默认 INFO。
关键经验
DEBUG 级别必须记录足够细的信息。对 UI 自动化场景来说,"足够细"意味着:
- ADB 命令:完整参数 + 返回值 + 错误信息
- UI 节点属性:
text、resource-id、content-desc、clickable、bounds全部打印 - 查找过程:搜了什么条件、找到几个候选、为什么没匹配上
有了这些,一次 MIHEALTH_LOG_LEVEL=DEBUG 运行就能精确定位问题。碎片拼成了完整的器型,原来睡眠卡片确实在——只不过 clickable 属性从 true 变成了 false,一眼便知。
不再需要猜了。
坑二:Go 在 Termux 中 time.Now() 静默回退 UTC
现象
Termux 中 date 命令显示北京时间,一切正常。但 Go 程序的日志时间比实际早了 8 小时。仿佛程序身在上海,心在伦敦。
根因
Go 的 time.LoadLocation 依赖 /usr/share/zoneinfo/ 路径下的时区数据库文件。Termux 的文件系统是 /data/data/com.termux/files/usr/share/zoneinfo/,Go 在标准路径找不到时区文件,不报错,直接将 time.Local 设为 UTC。
所有 time.Now() 调用都返回 UTC 时间,差 8 小时。一个沉默的、系统性的偏移。
解决方案
在最早被 import 的包(logger)的 init() 中强制设置时区:
| |
为什么放在 logger 的 init()
Go 的 init() 按 import 依赖顺序执行。logger 是最底层的包,被所有其他模块 import,它的 init() 最先执行。在这里设置 time.Local,后续所有模块的 time.Now() 都能拿到正确的时区。
延伸
这个问题不只出现在 Termux。Docker alpine 镜像、嵌入式系统、交叉编译的目标环境——任何缺少 zoneinfo 文件的地方,Go 都会安静地退回格林尼治。
余记
两个坑的共同特征是沉默。不报错,不 panic,只是给你一个看起来合理但实际上错误的结果。这大概是所有故障中最难缠的一种——它不是墙,而是一面画着通路的墙。
《庄子》有言:"吾生也有涯,而知也无涯。"在 Termux 的沙盒里调试 ADB 自动化,确有此感。不过话说回来,给程序装上一双能说话的眼睛,让它把看到的一切如实报告,至少下次不必在黑暗中摸索了。