【踩坑记录】UTF-8 和 GBK 编码冲突导致代码全变?Git 为什么没有提示冲突?
前言
最近在做 Memoria 项目的 Flutter / Dart 代码时,遇到了一个非常经典但又很隐蔽的问题:
一个原本正常的 UTF-8 文件,被某个环境用 GBK 打开并保存后,文件内容看起来变成了大量乱码、转义字符,代码 diff 也变得非常恐怖。
更坑的是:
> Git 并不会认为这是“编码冲突”,它只会认为这是一次普通的文件修改。
也就是说,如果一个人把 UTF-8 文件用 GBK 保存了,然后再提交,另一个人 pull 下来之后,Git 很可能不会爆冲突,而是直接把“变了编码后的文件”合并下来。
这篇文章记录一下这个问题的原因、排查方式、修复方式和后续项目规范。
一、问题现象
原本 Dart 文件里有一些中文 prompt、中文注释、语义搜索规则等内容。
结果某次打开后,文件里出现了大量类似下面的内容:
'\u8bf7\u6839\u636e\u7528\u6237\u8f93\u5165...'或者直接出现乱码:
璇锋牴鎹甤...在 IDE 里看起来就是:
中文变成乱码;
字符串被转义成一堆
\uXXXX;Git diff 显示整个文件大面积变化;
代码本身逻辑没怎么改,但几百行甚至上千行都变了。
这种情况在包含中文 prompt、中文配置、中文注释的项目里尤其容易出现。
二、根本原因:Git 只比较字节流,不理解文本编码
这个问题的核心原因是:
Git 本质上管理的是文件内容的字节流,而不是“人眼看到的文本”。
同一个中文字符,在 UTF-8 和 GBK 里的字节表示是不一样的。
例如中文字符:
中在 UTF-8 中是:
E4 B8 AD在 GBK 中可能是:
D6 D0人看到的都是“中”,但底层字节完全不同。
所以当一个 UTF-8 文件被 GBK 打开、编辑、保存后,文件的底层字节流可能会发生大面积变化。
Git 看到的是:
旧字节流 != 新字节流于是它会认为:
这个文件被正常修改了而不是:
这个文件发生了编码冲突三、为什么 Git 不会自动报冲突?
很多人会以为:
既然文件被弄乱了,Git 应该提示 conflict 吧?
但实际上,Git 的冲突检测主要发生在三方合并时:
base 版本 你的版本 对方版本只有当两边修改了同一片内容,并且 Git 无法自动判断该怎么合并时,才会出现:
<<<<<<< HEAD ... ======= ... >>>>>>> branch但是编码变化通常有两种情况:
情况 1:只有一个人改了编码
比如:
A 提交:正常 UTF-8 文件 B 提交:把文件用 GBK 保存了 C pull:直接拿到 B 的结果这时候 Git 认为 B 只是正常修改文件,不会产生冲突。
情况 2:两个人改了不同区域
比如:
A 改了上半部分代码 B 用 GBK 保存导致全文件变化Git 可能仍然会尝试自动合并,不一定冲突。
情况 3:两个人改了同一行
这种情况下有可能冲突,但冲突原因仍然是“同一行被修改”,不是“UTF-8 和 GBK 冲突”。
所以结论是:
Git 没有“编码冲突”这个概念。
它只知道字节变了,不知道你是 UTF-8、GBK、Shift-JIS 还是别的编码。
四、这个问题为什么很危险?
这个坑危险在于,它不像语法错误那样立刻爆炸。
它可能表现为:
代码还能编译;
逻辑似乎没变;
Git 没有冲突提示;
review 的时候 diff 太大,别人不一定仔细看;
最后乱码文件被正常 merge 进主分支。
尤其是有中文 prompt 的 AI 项目,这个问题会更严重。
例如:
final prompt = ''' 你是一个语义查询解析器,请把用户输入解析成 JSON。 ''';如果 prompt 编码坏了,模型收到的提示词可能就变成乱码,后续效果会非常诡异:
搜索结果不稳定;
LLM 输出格式异常;
中文查询解析失败;
原本能识别“上个月”“济南”“照片”的逻辑突然失效。
这类 bug 很难第一时间想到是编码问题。
五、如何判断是不是编码问题?
1. 看 Git diff
如果你发现某个文件明明只改了一点点,但 diff 显示几百行都变了,就要警惕:
git diff path/to/file.dart典型现象:
- 正常中文 + \u6b63\u5e38\u4e2d\u6587或者:
- 请根据用户输入解析 + 璇锋牴鎹甤...2. 看 IDE 右下角编码
在 VS Code、Android Studio、JetBrains 系 IDE 里,一般右下角会显示当前文件编码:
UTF-8 GBK GB2312 UTF-8 with BOM如果 Dart / Markdown / JSON 文件不是 UTF-8,就要注意。
3. 用命令检查编码
Linux / macOS 可以用:
file -bi path/to/file.dart可能输出:
text/plain; charset=utf-8或者:
text/plain; charset=iso-8859-1Windows 下也可以用 Python 简单检测:
from pathlib import Path path = Path("lib/service/semantic_query_parser_lm.dart") data = path.read_bytes() try: data.decode("utf-8") print("UTF-8 OK") except UnicodeDecodeError as e: print("Not valid UTF-8:", e)六、如何修复已经被 GBK 保存的文件?
情况 1:你不需要保留当前修改
这是最简单的情况,直接恢复:
git restore path/to/file.dart或者恢复整个工作区:
git restore .注意:这会丢弃本地未提交修改。
情况 2:你需要保留代码逻辑修改
如果里面确实有一些有效代码改动,但是编码坏了,可以这样处理:
先备份当前文件;
从 Git 恢复一份正常 UTF-8 版本;
手动把有效代码逻辑迁移过去;
不要直接整文件覆盖;
最后确认 diff 只包含真正的逻辑修改。
建议流程:
copy lib/service/semantic_query_parser_lm.dart lib/service/semantic_query_parser_lm.dart.bak git restore lib/service/semantic_query_parser_lm.dart然后打开.bak,只复制真正需要的代码片段。
情况 3:只是编码错了,内容还可逆
如果确定文件是 GBK 编码,可以尝试转回 UTF-8:
iconv -f gbk -t utf-8 old.dart > new.dart或者 Python:
from pathlib import Path src = Path("old.dart") dst = Path("new.dart") text = src.read_text(encoding="gbk") dst.write_text(text, encoding="utf-8")但注意:如果文件已经经历过多次错误打开和保存,不一定能完全恢复。
七、如何防止再次发生?
1. 统一项目编码:全部使用 UTF-8
在项目根目录加.editorconfig:
root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.dart] charset = utf-8 end_of_line = lf [*.md] charset = utf-8 end_of_line = lf [*.json] charset = utf-8 end_of_line = lf这样支持 EditorConfig 的 IDE 会自动使用 UTF-8。
2. VS Code 设置
项目里可以加:
{ "files.encoding": "utf8", "files.autoGuessEncoding": false, "files.eol": "\n" }建议关闭自动猜编码,因为自动猜有时反而会把 UTF-8 文件识别成 GBK。
3. JetBrains / Android Studio 设置
路径一般是:
Settings / Preferences -> Editor -> File Encodings建议设置:
Global Encoding: UTF-8 Project Encoding: UTF-8 Default encoding for properties files: UTF-8如果 IDE 提示:
Reload in another encoding Convert to UTF-8要优先选择:
Convert to UTF-8而不是随便用 GBK 重新保存。
4. 添加 Git 提交前检查
可以写一个简单脚本,阻止非 UTF-8 文件提交。
例如scripts/check_utf8.py:
from pathlib import Path import sys TEXT_EXTS = { ".dart", ".md", ".json", ".yaml", ".yml", ".txt", ".py", ".js", ".ts", ".html", ".css" } failed = [] for path in Path(".").rglob("*"): if not path.is_file(): continue if ".git" in path.parts: continue if path.suffix.lower() not in TEXT_EXTS: continue try: path.read_text(encoding="utf-8") except UnicodeDecodeError: failed.append(str(path)) if failed: print("以下文件不是合法 UTF-8:") for f in failed: print(" -", f) sys.exit(1) print("UTF-8 check passed.")运行:
python scripts/check_utf8.py也可以接到 pre-commit 里。
5..gitattributes统一换行
.gitattributes不一定能完全解决编码问题,但可以规范文本文件和换行:
* text=auto *.dart text eol=lf *.md text eol=lf *.json text eol=lf *.yaml text eol=lf *.yml text eol=lf *.py text eol=lf注意:.gitattributes主要解决文本归一化和换行问题,不能指望它自动帮你识别所有编码错误。
八、团队协作时怎么说清楚这个问题?
这类问题最好不要只说:
你代码乱码了。而是应该明确说明:
这个文件原本是 UTF-8,但被 GBK 或其他编码方式打开并保存了。 Git 只比较字节流,不理解文本编码,所以它会把这当成普通修改,不一定产生冲突。 建议恢复原文件后,只迁移真正的逻辑修改,并统一 IDE 编码为 UTF-8。这样对方更容易理解问题本质。
九、这次给我的教训
这次问题给我的最大提醒是:
对包含中文 prompt、中文注释、中文配置的 AI 项目来说,编码规范不是小事。
尤其是像 Memoria 这种项目,里面会有很多中文语义解析 prompt,例如:
时间解析;
地点解析;
相册事件生成;
语义检索;
LLM 输出 JSON 模板;
中文标签和规则。
这些内容一旦被错误编码污染,表面看起来可能只是“中文显示不正常”,但实际上可能影响整个模型调用链路。
所以后续我会把 UTF-8 编码检查作为项目工程规范的一部分。
十、总结
这次问题可以总结成一句话:
Git 只管理字节流,不理解文本编码。UTF-8 文件被 GBK 打开并保存后,底层字节完全变化,Git 只会认为这是普通修改,不会自动触发所谓“编码冲突”。
解决方案:
项目统一使用 UTF-8;
IDE 全局设置 UTF-8;
不要用 GBK 保存代码文件;
发现 diff 大面积异常时,优先怀疑编码问题;
对已经污染的文件,优先从 Git 恢复,再手动迁移有效逻辑;
添加
.editorconfig、.gitattributes和 UTF-8 检查脚本;团队协作时明确说明:这是编码问题,不是普通代码风格问题。
这类 bug 不难修,但如果不重视,很容易悄悄污染主分支。
所以,尤其是中文 AI 项目,一定要记住:
代码文件统一 UTF-8。 Prompt 文件统一 UTF-8。 配置文件统一 UTF-8。 不要让 GBK 混进来。