Go binary 怎样按大端序读写固定结构
来源:17golang原创
时间:2026-10-22 00:39:45 213浏览 收藏
我在处理固定格式的文件头和网络报文时,最容易踩的坑不是 API 不会用,而是写端和读端对字节序、字段宽度的约定没有落到同一份结构上。Go 的 encoding/binary 已经提供了明确的 binary.BigEndian,只要结构体只包含固定大小的字段,就可以用一套规则完成编码和解码。
官方文档:https://pkg.go.dev/encoding/binary
- 固定结构优先使用
binary.Write和binary.Read,两端传入同一个binary.BigEndian。 - 结构体不会按 C 语言那样自动写入内存填充,字段会按声明顺序编码;空白字段可以显式占位。
- 读取不完整数据时,读到一部分后遇到结尾通常应关注
io.ErrUnexpectedEOF。
一、先把协议字段写成固定结构
假设报文由一个 2 字节魔数、一个 1 字节版本、一个 4 字节负载长度和 4 字节固定负载组成。这里的重点是把协议宽度写成 Go 的定长类型,而不是使用 int、string 或不定长切片。
package main
import "encoding/binary"
// Packet 描述一个固定宽度的二进制报文头和负载。
type Packet struct {
Magic uint16 // 2 字节:用于识别协议
Version uint8 // 1 字节:用于判断格式版本
PayloadLen uint32 // 4 字节:声明负载长度
Payload [4]byte // 4 字节:示例中的固定负载
}
var order binary.ByteOrder = binary.BigEndian
这份结构一共占 11 个协议字节:uint16 占 2 字节,uint8 占 1 字节,uint32 占 4 字节,数组再占 4 字节。binary.Write 和 binary.Read 关注的是这些固定大小字段的顺序,不会把 Go 结构体在内存中的地址布局直接写进文件。

二、用 binary.Write 按大端序编码
编码时把同一个字节序对象传给 binary.Write。对于需要先在内存中组装、再交给网络层或文件层的场景,bytes.Buffer 足够直观;真正写文件时也可以把它替换成文件对象。
package main
import (
"bytes"
"encoding/binary"
"fmt"
)
func encodePacket(packet Packet) ([]byte, error) {
var buf bytes.Buffer
// 明确指定 BigEndian,避免依赖运行机器的本地字节序。
if err := binary.Write(&buf, binary.BigEndian, packet); err != nil {
return nil, fmt.Errorf("编码 Packet 失败: %w", err)
}
return buf.Bytes(), nil
}
func main() {
packet := Packet{
Magic: 0x1234,
Version: 1,
PayloadLen: 4,
Payload: [4]byte{'G', 'o', '!', '!'},
}
data, err := encodePacket(packet)
if err != nil {
fmt.Println(err)
return
}
// 输出只是示例打印,实际项目可交给文件或网络写入器。
fmt.Printf("% x\n", data)
}
在大端序下,0x1234 的两个字节会按 12 34 写出,PayloadLen 的 4 字节也从高位到低位排列。数组字段会原样写入。这个约定和机器是小端还是大端没有关系,协议规则已经由调用处固定。
三、用 binary.Read 按同一规则解码
解码时要保持三件事一致:读取器、字节序和目标结构体。目标必须是指向可写变量的指针;如果只传结构体值,读取函数没有地方保存结果。
package main
import (
"bytes"
"encoding/binary"
"fmt"
)
func decodePacket(data []byte) (Packet, error) {
var packet Packet
// Reader 会按字段顺序填充 packet,字节序必须与写入端一致。
if err := binary.Read(bytes.NewReader(data), binary.BigEndian, &packet); err != nil {
return Packet{}, fmt.Errorf("解码 Packet 失败: %w", err)
}
return packet, nil
}
func main() {
data := []byte{0x12, 0x34, 0x01, 0x00, 0x00, 0x00, 0x04, 'G', 'o', '!', '!'}
packet, err := decodePacket(data)
if err != nil {
fmt.Println(err)
return
}
// 读出后的字段已恢复为 Go 的定长类型。
fmt.Printf("magic=%#x version=%d length=%d payload=%q\n", packet.Magic, packet.Version, packet.PayloadLen, packet.Payload)
}
如果把读取端误写成 binary.LittleEndian,同一组字节中的多字节字段会被解释成另一个数值;单字节的 Version 和逐字节的 Payload 看起来却可能正常,这也是字节序问题容易被漏掉的原因。
四、在短数据和字段变更处设置边界
固定结构的大小可以用 binary.Size 计算。读取前先判断输入长度,能够把“报文被截断”和“报文内容非法”分开;即使不预检,也要保留 binary.Read 返回的错误。
package main
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
)
func decodeComplete(data []byte) (Packet, error) {
want := binary.Size(Packet{})
// 先判断长度,避免把明显的截断输入交给后续业务逻辑。
if len(data)
文档定义了一个有用的区别:如果一次字节都没有读到,可以得到 io.EOF;已经读到部分字段后才遇到输入结束,则应按 io.ErrUnexpectedEOF 处理。协议升级时新增字段也必须同步修改写端、读端和长度约定,否则旧版本可能把新字段误当成下一段数据。

五、按场景选择结构体还是手动切片
固定结构的优势是字段清楚、读写代码短,适合协议头、文件头和需要与其他语言约定字段顺序的格式。若结构中加入了字符串、指针、接口或变长切片,就不再是这个简单模型,应该把变长部分的长度和字节内容单独处理。
对于需要严格控制每一段长度的解析器,可以直接使用 binary.BigEndian.Uint16、Uint32 读取切片,并在每次读取前检查剩余长度:
func readHeader(data []byte) (magic uint16, version uint8, length uint32, err error) {
// 先检查每个字段需要的字节数,防止切片越界。
if len(data)
这个手动切片版本适合需要逐字段校验、兼容多个版本或只读取头部的场景,但切片下标更容易随着协议变更失步。我的取舍是:结构稳定时用结构体集中表达协议,字段经常演进或需要部分读取时再切换到显式偏移,并把偏移常量和长度检查放在同一个解析函数中。
六、发布前的固定清单
| 检查项 | 建议 | 常见问题 |
|---|---|---|
| 字节序 | 读写两端都传 binary.BigEndian | 一端默认使用 LittleEndian |
| 字段类型 | 使用 uint16、uint32 等定长类型 | 使用 int 导致协议宽度不明确 |
| 结构布局 | 按声明顺序定义字段,空白字段只用于明确占位 | 把 Go 内存对齐误当成协议填充 |
| 输入长度 | 用 binary.Size 或显式偏移检查长度 | 短数据进入业务层后才暴露 |
| 错误处理 | 保留并分类 io.EOF、io.ErrUnexpectedEOF | 忽略读取错误后继续使用零值 |
如果只是读写一个固定宽度结构,binary.Read 和 binary.Write 已经足够;如果格式有变长字段,则把“固定头部”和“变长负载”拆开,先按大端序读长度,再按长度读取负载,边界会更容易维护。
相关问题
binary.Read 能直接读取 string 吗?
固定长度的字节数组可以读取,之后再按约定的编码转换成字符串。不要把不定长 string 当作固定结构字段直接交给它。
为什么单字节字段看不出大小端错误?
大小端只会改变多字节数值的排列,uint8 或原样字节数组没有可交换的高低字节,所以要用多字节字段的已知值核对规则。
数据比固定结构更长时怎么办?
先读取固定结构,再根据其中的长度字段读取负载;不要为了“读完”而把未知尾部直接塞进结构体。
-
487 收藏
-
130 收藏
-
184 收藏
-
245 收藏
-
394 收藏
-
274 收藏
-
351 收藏
-
321 收藏
-
266 收藏
-
235 收藏
-
485 收藏
-
266 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习