Go binary 怎样按大端序读写固定结构
我在处理固定格式的文件头和网络报文时,最容易踩的坑不是 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 或原样字节数组没有可交换的高低字节,所以要用多字节字段的已知值核对规则。
数据比固定结构更长时怎么办?
先读取固定结构,再根据其中的长度字段读取负载;不要为了“读完”而把未知尾部直接塞进结构体。
Go gzip 怎样给 HTTP 流增加压缩
- 上一篇
- Go gzip 怎样给 HTTP 流增加压缩
- 下一篇
- Go filepath 怎样跨平台拼接路径
-
- Golang · Go教程 | 31分钟前 | go · Go 交叉编译 GOARCH GOOS CGO_ENABLED
- Go 交叉编译怎样关闭 CGO
- 487浏览 收藏
-
- Golang · Go教程 | 37分钟前 | go · Go教程 · go.work Go Modules go work 多模块联调
- Go work 怎样管理多个模块联调
- 245浏览 收藏
-
- Golang · Go教程 | 39分钟前 | Go教程 · Go 模块依赖 Go Modules go mod why 依赖路径
- Go mod why 怎样解释模块引入路径
- 394浏览 收藏
-
- Golang · Go教程 | 42分钟前 |
- Go net/url 怎样只修改查询参数而保留路径
- 274浏览 收藏
-
- Golang · Go教程 | 45分钟前 | goroutine · go ·
- Go trace 怎样观察 goroutine 调度
- 351浏览 收藏
-
- Golang · Go教程 | 46分钟前 |
- Go pprof 怎样定位 CPU 热点函数
- 321浏览 收藏
-
- Golang · Go教程 | 51分钟前 |
- Go benchmark 怎样排除初始化耗时
- 266浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 582次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 620次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 654次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 618次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 435次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览
-
- Golang中Interface接口的三个特性
- 2023-01-07 394浏览
-
- go语言中函数与方法介绍
- 2023-01-07 297浏览
-
- go语言数据类型之字符串string
- 2022-12-30 321浏览

