UTF-8 BOM 在消费端被读取时:shell、Go 源码、JSON 各自的报错与字节层确认
BOM 的问题分两侧。导出侧是 Excel 打开 UTF-8 CSV 乱码,需要主动加 BOM 才认(EF BB BF,即 U+FEFF)。消费侧相反:文件被脚本解释器、编译器、JSON 解析器读进去的那一刻,多出的三个字节让工具链直接拒绝。这篇只记录后者。
现象:三类报错
1. shell 脚本无法执行
$ bash myscript
myscript: line 1: #!/bin/sh: No such file or directory
$ dash myscript
myscript: 1: myscript: #!/bin/sh: not found
cat -v file 会显示 M-oM-;M-?#!/bin/bash。shell 不认 BOM,把它和 shebang 一起当成要执行的命令名。
和 CRLF 要分开看:#!/bin/sh\r 的报错是 exec failed: 2 No such file or directory,而 BOM 在前的报错是 exec failed: 8 Exec format error,errno 8 即 ENOEXEC。内核 execve 直接拒绝,shell 才回退到自己解释,于是 #!/bin/sh 被当作命令。
ShellCheck 对应规则 SC1082:
This file has a UTF-8 BOM. Remove it with: `LC_CTYPE=C sed '1s/^...//' 。
2. Go 源码
Go 规范允许编译器忽略作为文本第一个码点的 BOM;go/scanner 里 bom = 0xFEFF,注释是 "only permitted as very first character"。BOM 出现在 offset > 0 时报 illegal byte order mark;文件开头是 UTF-16 的 FF FE / FE FF 时报 illegal UTF-8 encoding (got UTF-16)(go.dev/issue/71950)。go/build 的 importReader 会 Peek(3) 后 Discard 开头 BOM。
历史 issue 里,文件中间出现 BOM 报 ./x.go:4: Unicode (UTF-8) BOM in middle of file;go test 生成的 _testmain.go 曾因导出数据里的 U+FEFF 报错(issue 5260)。
参考:。
3. JSON 与配置
RFC 8259 规定实现不得在网络传输的 JSON 文本前加 BOM,但解析实现可以忽略 BOM 而不是报错()。实际各家行为:
- V8:
SyntaxError: Unexpected token '', "{ ""a"": 1 }" is not valid JSON - Firefox:
JSON.parse: unexpected character at line 1 column 1 - Safari:
JSON Parse error: Unrecognized token '' - Python:
json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0),改用open(path, encoding="utf-8-sig") - Jackson 读
byte[]时自动跳过 BOM,readTree(byte[])正常;若 BOM 留在自己解码出的String里,报Unexpected character ('' (code 65279 / 0xfeff)): expected a valid value。Spring 的 converter 同样读字节,正常。 - .NET 的
Encoding.UTF8默认写 BOM,StreamWriter默认不写;NServiceBus + Json.NET 把 BOM 写进消息体后,Azure Functions 绑定报Unexpected character encountered while parsing value: ?. Path '', line 0, position 0.,修复是new StreamWriter(s, new UTF8Encoding(false))。
PHP 是另一条路径:被 include 的文件开头有 BOM,这三个字节先被输出,导致 headers already sent,页面顶部多一行空白或 。PHP 5.3.6 之后内置服务器等场景有修正,输出侧问题仍在。
字节层确认
先看前三字节:
$ head -c 3 file.json | xxd
00000000: efbb bf ...
$ od -c file | head -1
Python 里用字节模式,不要走文本模式:
from pathlib import Path
Path(f).read_bytes().split(b"\n", 1)[0]
# repr → b'\xef\xbb\xbf#!/bin/sh'
read_text().splitlines() 会改动换行,判断 BOM 时不要用。
各工具链行为对照
| 读取方 | 遇到 BOM 的行为 | 结果 |
|---|---|---|
内核 execve | 不识别 | ENOEXEC(8),shell 回退后报 shebang 找不到 |
Go go/scanner | 首个码点忽略,中间拒绝 | illegal byte order mark / illegal UTF-8 encoding (got UTF-16) |
Python 文本 open | utf-8 报错 | 用 utf-8-sig 才去 BOM |
JS JSON.parse | 不忽略 | Unexpected token |
Node fs.readFileSync(path,'utf8') | 保留 BOM(node issue #1918,按设计) | 后续解析失败 |
Node TextDecoder | 默认去 BOM | {ignoreBOM: true} 才保留 |
Jackson byte[] | 自动跳过 | 正常 |
Jackson String | 不跳过 | code 65279 / 0xfeff |
PHP include | 原样输出 | headers already sent |
Node 这一条最容易被误判:同一个 JSON 文件浏览器能解析,Node 脚本里却失败,差别就在 fs.readFileSync 和 TextDecoder 的默认值。
修复与预防
去掉脚本 BOM,按 SC1082 给出的写法:
$ LC_CTYPE=C sed '1s/^...//' 与 。