Go程序在Termux中的日志与时区踩坑

引子

古人说"察其所安",意思是观察一个人安于什么处境,便知其品性。程序也是一样——它安于沉默,你就只能在黑暗中摸索。

把一个 Go 编写的 ADB UI 自动化工具部署到 Android Termux 环境之后,我遇到了两个不声不响的故障。它们不抛异常,不给堆栈,只是安静地产出错误的结果。一个让我失去了定位问题的能力,另一个让我失去了对时间的感知。这两样东西恰好是调试的全部依靠。

坑一:没有分级日志,UI 自动化故障只能猜

现象

程序输出只有简单的 fmt.Println,报错信息像一封被删去了所有细节的电报:

警告: 睡眠详情截取失败: 未找到睡眠卡片
[运动] 从运动tab页截取到 0 条记录摘要

"未找到"三个字背后,是至少五种可能的原因:App 没启动?UI 节点属性变了?屏幕还锁着?控件的 clickabletrue 变成了 false?还是 content-desc 的文案被产品经理改了?

全靠猜。像是在考古现场,只看到一片碎片,却要还原整个器型。

根因

Go 标准库没有像 Python logging 那样开箱即用的分级日志。fmt.Println 缺少时间戳、调用位置、级别控制,线上问题只能盲猜。

解决方案

基于 runtime.Caller 实现轻量日志模块,输出格式:

2026-02-11 16:05:24 [INFO ] sleep.go:80          | [睡眠] 候选节点: desc="无睡眠数据", clickable=true

核心代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func log(level Level, callerSkip int, format string, args ...any) {
    if level < currentLevel {
        return
    }
    now := time.Now().Format("2006-01-02 15:04:05")
    _, file, line, ok := runtime.Caller(callerSkip)
    loc := "???:0"
    if ok {
        loc = fmt.Sprintf("%s:%d", filepath.Base(file), line)
    }
    msg := fmt.Sprintf(format, args...)
    fmt.Fprintf(os.Stderr, "%s [%-5s] %-20s | %s\n", now, levelNames[level], loc, msg)
}

通过环境变量 MIHEALTH_LOG_LEVEL 控制级别(DEBUG/INFO/WARN/ERROR),默认 INFO。

关键经验

DEBUG 级别必须记录足够细的信息。对 UI 自动化场景来说,"足够细"意味着:

  • ADB 命令:完整参数 + 返回值 + 错误信息
  • UI 节点属性textresource-idcontent-descclickablebounds 全部打印
  • 查找过程:搜了什么条件、找到几个候选、为什么没匹配上

有了这些,一次 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() 中强制设置时区:

1
2
3
4
5
6
7
8
9
func init() {
    // Termux 中 Go 可能找不到 zoneinfo,回退 UTC
    if loc, err := time.LoadLocation("Asia/Shanghai"); err == nil {
        time.Local = loc
    } else {
        // LoadLocation 失败,硬编码 UTC+8
        time.Local = time.FixedZone("CST", 8*3600)
    }
}

为什么放在 logger 的 init()

Go 的 init() 按 import 依赖顺序执行。logger 是最底层的包,被所有其他模块 import,它的 init() 最先执行。在这里设置 time.Local,后续所有模块的 time.Now() 都能拿到正确的时区。

延伸

这个问题不只出现在 Termux。Docker alpine 镜像、嵌入式系统、交叉编译的目标环境——任何缺少 zoneinfo 文件的地方,Go 都会安静地退回格林尼治。

余记

两个坑的共同特征是沉默。不报错,不 panic,只是给你一个看起来合理但实际上错误的结果。这大概是所有故障中最难缠的一种——它不是墙,而是一面画着通路的墙。

《庄子》有言:"吾生也有涯,而知也无涯。"在 Termux 的沙盒里调试 ADB 自动化,确有此感。不过话说回来,给程序装上一双能说话的眼睛,让它把看到的一切如实报告,至少下次不必在黑暗中摸索了。