用encoding/binary写固定格式二进制数据最稳:需确保结构体字段全导出、类型定长、字节序严格对齐,变长部分须手动拆解长度与内容,并用io.ReadFull保证定长读取。

用 encoding/binary 写固定格式二进制数据最稳
Go 原生不支持像 Python 的 pickle 那种通用对象序列化,但如果你要写协议包、文件头、网络帧这类结构明确的二进制数据,encoding/binary 是首选。它不依赖反射,零分配(配合预分配 []byte),且字节序完全可控。
常见错误是直接对 struct 指针调用 binary.Write 却没确保字段可导出(首字母大写)——它只处理导出字段,小写字段会被静默跳过。
- 必须用
struct{ A uint32; B int16 }这类显式定义,不能嵌套未导出字段 - 写入前务必确认
io.Writer底层支持Write并返回准确字节数,比如bytes.Buffer安全,而某些自定义Writer可能截断 - 字节序选
binary.BigEndian还是binary.LittleEndian要和协议文档/接收方严格对齐,错一个字节就全乱
示例:写一个 8 字节头部(4 字节魔数 + 4 字节长度)
header := struct {
Magic uint32
Length uint32
}{0x474f4c41, uint32(len(payload))}
err := binary.Write(&buf, binary.BigEndian, header)
gob 适合 Go 到 Go 的进程间传递,但别碰跨语言场景
gob 是 Go 自带的二进制序列化包,能处理 map、slice、interface{} 甚至带方法的 struct,但它是 Go 专属协议,没有公开规范,其他语言基本无法解析。
立即学习“go语言免费学习笔记(深入)”;
典型误用是把 gob 输出当“通用二进制格式”存盘或发给 Java/Python 服务——结果就是对方根本读不了,还容易误以为是编码问题。
- 发送前必须先调用
enc.Encode()写入类型信息,接收端dec.Decode()才能匹配结构;如果中间有丢包或截断,EOF或unexpected EOF错误会很模糊 - struct 字段名变更(如
UserID→Uid)会导致反序列化失败,除非加gob:"uid"标签兼容 - 含
func、chan、unsafe.Pointer的值无法编码,运行时报gob: type not registered for interface
读二进制文件时,io.ReadFull 比 Read 更可靠
直接用 file.Read(buf) 读固定长度结构体时,常遇到只读了部分字节就返回(尤其在文件末尾或网络连接不稳定时),导致后续解析错位。这时候 io.ReadFull 是刚需。
它保证要么填满整个 buf,要么返回 io.ErrUnexpectedEOF 或其他真实错误,不会让你误以为“读成功了”。
- 不要对
*os.File直接用ReadFull后再接着用Read—— 文件偏移已变,容易漏字节或重复读 - 如果结构体含变长字段(如后面跟一个长度为 N 的 byte slice),先读定长头,再用
io.ReadFull读 N 字节,别用Read循环拼接 - 内存映射(
mmap)不是万能解法:Windows 上syscall.Mmap行为受限,且映射后仍需手动处理字节序和对齐
自定义二进制格式一定要预留版本字段和校验字段
哪怕只是内部工具链用,也建议在文件开头放 2 字节版本号 + 4 字节 CRC32。否则半年后你改了结构,旧程序读新文件直接 panic,连报错都难定位。
版本字段让反序列化逻辑可分支,校验字段帮你快速区分是文件损坏还是格式不匹配——前者该报 checksum mismatch,后者才是 unsupported version 2。
- 别用
time.Now().Unix()当版本号,它不可重现;用递增整数或语义化版本字符串(转成 uint16) - CRC32 计算范围要明确:通常从版本号之后开始,不含校验字段自身,否则循环依赖
- 如果未来要支持压缩,校验必须在解压后做,否则损坏可能被压缩算法掩盖
真正麻烦的从来不是怎么写进去,而是几个月后没人记得当初那个第 7 个字节到底是标志位还是保留位。留好版本和校验,等于给自己留了条退路。


















