登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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 结构体在内存中的地址布局直接写进文件。

Go binary 大端序固定结构中的协议约定和字段关系说明图
图1:固定结构说明图,展示 BigEndian 规则与 Packet 字段的静态归属关系,不是截图或运行证据。

二、用 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.Write binary.Read 与 Packet 和短数据错误边界的静态关系说明图
图2:读写边界说明图,展示 binary.Write、binary.Read、Packet 和 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 或原样字节数组没有可交换的高低字节,所以要用多字节字段的已知值核对规则。

数据比固定结构更长时怎么办?

先读取固定结构,再根据其中的长度字段读取负载;不要为了“读完”而把未知尾部直接塞进结构体。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>