34 - Go 二进制处理(编码/解码)深度解析

文章目录


34 - Go 二进制处理(编码/解码)深度解析

在 Go 开发中,很多开发者第一次接触"二进制处理",往往是在:

  • TCP 网络协议
  • 文件格式解析
  • 数据序列化
  • 硬件通信
  • MySQL / Redis 协议分析
  • protobuf / msgpack / gob
  • 性能优化

等场景。

但很多人对它的理解,还停留在:

"把 int 转成 []byte"

实际上,Go 的二进制处理,本质上是在解决:

"如何以最紧凑、最稳定、跨平台的方式表示数据"。

这篇文章,我们就聊聊 Go 中的二进制编码与解码。


为什么需要二进制处理

先看一个问题:

假设我们要通过 TCP 发送一个用户 ID。

go 复制代码
userID := 10086

如果直接发送字符串:

text 复制代码
"10086"

那么实际发送的是:

text 复制代码
31 30 30 38 36

长度为 5 字节(ASCII 码表)。

但如果使用二进制:

text 复制代码
00 00 27 66

只需要 4 字节。

而且:

  • 更快
  • 更省空间
  • 更适合机器解析
  • 不依赖字符编码

这也是所有底层协议都大量使用二进制的原因。


核心概念

什么是二进制编码

本质上:

二进制编码,就是把"内存中的数据"转换成"字节流"。

例如:

数据类型 内存值 二进制表示
int32 10 00 00 00 0A
bool true 01
float32 3.14 40 48 F5 C3

而解码:

就是反向恢复。


二进制处理的核心问题

真正难的不是"转字节"。

而是:

  • 字节顺序(大端 / 小端)
  • 类型长度
  • 数据边界
  • 对齐问题
  • 跨平台一致性
  • 协议兼容性

这也是 Go encoding/binary 存在的核心意义。


Go 为什么提供 encoding/binary

Go 标准库没有选择"直接暴露内存"。

而是:

go 复制代码
encoding/binary

统一处理:

  • 整数编码
  • 浮点编码
  • struct 编码
  • 字节序转换

本质上:

Go 想保证不同机器上的数据一致性。

因为:

  • x86 是小端
  • 网络协议通常是大端
  • ARM 可能不同

如果不统一,跨平台通信会直接混乱。


Go 二进制处理核心模块

Go 中常见的二进制相关包:

作用
encoding/binary 基础二进制编码
bytes 字节缓冲
bufio 缓冲 IO
encoding/hex 十六进制
encoding/base64 Base64
math/bits 位运算
unsafe 底层内存操作

其中最核心的是:

go 复制代码
encoding/binary

最简单可运行示例

int 转二进制

go 复制代码
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

func main() {
	var number uint32 = 1024
	buffer := new(bytes.Buffer)                           // 创建一个新的缓冲器
	err := binary.Write(buffer, binary.BigEndian, number) // 编码
	if err != nil {
		panic(err)
	}
	fmt.Printf("编码结果: %v\n", buffer.Bytes()) // 输出编码结果

	var decoded uint32                                    // 创建一个变量用于解码
	err = binary.Read(buffer, binary.BigEndian, &decoded) // 解码
	if err != nil {
		panic(err)
	}
	fmt.Printf("解码结果: %v\n", decoded)
}

输出:

text 复制代码
编码结果: [0 0 4 0]
解码结果: 1024

这里发生了什么

go 复制代码
binary.Write()

实际上做了:

text 复制代码
uint32(1024)
↓
0x00000400
↓
[]byte{0x00,0x00,0x04,0x00}

小结

这里最关键的是:

go 复制代码
binary.BigEndian

它决定:

高位字节放前面还是后面。

这会直接影响:

  • 网络协议
  • 文件格式
  • 数据兼容性

大端序与小端序

这是二进制处理中最容易踩坑的知识点。


大端序(Big Endian)

高位在前。

例如:

text 复制代码
0x12345678

存储为:

text 复制代码
12 34 56 78

网络协议基本都使用它。

因此:

go 复制代码
binary.BigEndian

也叫:

text 复制代码
Network Byte Order

小端序(Little Endian)

低位在前:

text 复制代码
78 56 34 12

x86 CPU 默认就是小端。


Go 如何指定字节序

go 复制代码
binary.BigEndian
binary.LittleEndian

例如:

go 复制代码
binary.Write(buf, binary.LittleEndian, num)

思考点

为什么 CPU 喜欢小端?

text 复制代码
因为小端序在"低位扩展"时更高效。

例如一个 uint8:

0x11

在小端内存中:

11

如果扩展成 uint16:

0x0011

小端布局会变成:

11 00

原来的低位数据位置完全不用动,
CPU 只需要在高位补 00 即可。

但如果是大端序:

00 11

原来的 11 需要移动位置。

这意味着:

- 更多数据搬运
- 更复杂的硬件逻辑
- 更高的处理成本

因此:

CPU 在做整数扩展、加法、位运算时,
小端序会更自然、更高效。

所以大多数 CPU(如 x86)都采用小端序。

进阶示例:结构体编码

真实开发里,最常见的是 struct 编码。


示例:协议包编码

go 复制代码
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

type Message struct {
	ID     uint32
	Status uint16
}

func main() {
	msg := Message{
		ID:     1001,
		Status: 200,
	}

	buf := new(bytes.Buffer)

	err := binary.Write(buf, binary.BigEndian, msg)
	if err != nil {
		panic(err)
	}

	fmt.Printf("编码后: %x\n", buf.Bytes())

	var decoded Message

	err = binary.Read(buf, binary.BigEndian, &decoded)
	if err != nil {
		panic(err)
	}

	fmt.Printf("解码后: %+v\n", decoded)
}

输出:

text 复制代码
编码后: 000003e900c8
解码后: {ID:1001 Status:200}

小结

这里已经非常接近:

  • TCP 私有协议
  • 游戏协议
  • RPC 协议

底层实现。


进阶示例:自定义协议

真实场景:

TCP 粘包。

很多协议会这样设计:

text 复制代码
| 长度 | 数据 |

示例:长度头协议

go 复制代码
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

// 编码和解码协议包

// 编码协议包
func encode(message string) []byte {
	body := []byte(message)
	buf := new(bytes.Buffer)
	binary.Write(buf, binary.BigEndian, uint32(len(body))) // 写入长度
	buf.Write(body)                                        // 写入内容
	return buf.Bytes()
}

// 解码协议包
func decode(data []byte) string {
	buf := bytes.NewReader(data)
	var length uint32
	binary.Read(buf, binary.BigEndian, &length) // 读取长度
	body := make([]byte, length)                // 根据长度读取内容
	buf.Read(body)
	return string(body)
}

func main() {
	packet := encode("hello")
	fmt.Printf("协议包: %x\n", packet)

	msg := decode(packet)
	fmt.Println("解码:", msg)
}

输出:

text 复制代码
协议包: 0000000568656c6c6f
解码: hello

为什么协议都喜欢这样设计

因为 TCP 是:

text 复制代码
字节流

没有消息边界。

因此:

text 复制代码
长度 + 内容

是最稳定的协议设计。


点睛总结

很多人以为:

TCP 传输的是"消息"。

实际上:

TCP 只负责传输"字节流"。

消息边界,需要应用层自己维护。


进阶示例:文件二进制解析

假设读取一个简单文件头:

text 复制代码
| magic | version | data_length |

go 复制代码
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

type Header struct {
	Magic   [4]byte
	Version uint16
	Length  uint32
}

func main() {
	raw := []byte{
		'G', 'O', 'F', 'S', // Magic
		0x00, 0x01, // Version
		0x00, 0x00, 0x00, 0x64, // Length
	}

	var header Header

	err := binary.Read(
		bytes.NewReader(raw), // Reader
		binary.BigEndian,
		&header,
	)

	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", header)
}

输出:

text 复制代码
{Magic:[71 79 70 83] Version:1 Length:100}

常见错误与坑(重点)

坑一:struct 对齐问题

这是很多人最容易忽略的高危问题。


错误代码

go 复制代码
type Data struct {
	A uint8
	B uint32
}

很多人以为:

text 复制代码
1 + 4 = 5 字节

实际上:

go 复制代码
fmt.Println(binary.Size(Data{}))

结果:

text 复制代码
8

为什么会这样

因为 CPU 需要:

text 复制代码
内存对齐

Go 会自动填充 padding:

text 复制代码
A      -> 1 byte
padding-> 3 byte
B      -> 4 byte

总共:

text 复制代码
8 byte

正确写法

如果协议要求严格:

go 复制代码
type Data struct {
	B uint32
	A uint8
}

变成:

text 复制代码
4 + 1 + 3(padding)

或者:

手动编码字段。


小结

协议开发里:

不要迷信 struct 内存布局。

尤其:

  • 网络协议
  • 二进制文件
  • 跨语言通信

必须严格控制字节结构。


坑二:字节序不一致


错误代码

发送端:

go 复制代码
binary.Write(buf, binary.LittleEndian, num)

接收端:

go 复制代码
binary.Read(buf, binary.BigEndian, &num)

后果

数据直接错乱。

例如:

text 复制代码
0x12345678

可能变成:

text 复制代码
0x78563412

底层原因

因为:

text 复制代码
字节排列顺序不同

不是 Go 的问题。

而是:

二进制本身没有"方向感"。


正确做法

协议必须明确:

text 复制代码
统一字节序

一般推荐:

go 复制代码
binary.BigEndian

因为网络协议通用。


坑三:binary.Read 读不满


错误代码

go 复制代码
conn.Read(buf)
binary.Read(...)

为什么危险

TCP 不保证:

text 复制代码
一次读满

可能:

text 复制代码
只收到半包

正确写法

使用:

go 复制代码
io.ReadFull()

例如:

go 复制代码
_, err := io.ReadFull(conn, buf)

小结

网络开发里:

"读到数据" ≠ "读完整数据"。

这是很多线上 bug 根源。


底层原理解析(核心)

binary.Write 到底做了什么

核心流程:

text 复制代码
数据
↓
反射解析类型
↓
根据字节序拆分
↓
写入 io.Writer

源码核心思想:

go 复制代码
func Write(w io.Writer, order ByteOrder, data interface{})

为什么使用:

go 复制代码
io.Writer

因为 Go 希望:

二进制处理与 IO 解耦。

这样:

  • 文件
  • 网络
  • 内存
  • buffer

都能统一处理。

这是 Go 非常经典的设计。


为什么使用反射

因为:

go 复制代码
binary.Write()

支持:

  • int
  • float
  • array
  • struct

需要动态解析类型。

因此:

go 复制代码
reflect

不可避免。


为什么 binary 不支持 string

因为:

text 复制代码
string 没有边界定义

例如:

text 复制代码
hello

到底:

  • 固定长度?
  • UTF-8?
  • null 结尾?
  • 带长度头?

Go 无法猜测。

因此:

string 必须由协议自己定义。

这其实是非常合理的设计。


binary.Size 原理

go 复制代码
binary.Size(v)

本质:

text 复制代码
递归计算字段长度

但注意:

它会计算:

text 复制代码
padding

因此:

不一定等于协议长度。


对比与扩展

binary vs json

对比 binary json
可读性
性能 较低
空间占用
跨语言
调试 困难 简单

binary 适合

  • 游戏协议
  • RPC
  • 高性能服务
  • 文件格式
  • IoT

json 适合

  • Web API
  • 配置文件
  • 调试友好场景

binary vs protobuf

很多人会混淆。


binary

更底层:

text 复制代码
你自己定义协议

protobuf

属于:

text 复制代码
结构化序列化协议

自动:

  • 编码
  • 解码
  • 版本兼容

protobuf 的本质

其实也是:

text 复制代码
二进制编码

只是:

帮你自动管理协议结构。


最佳实践

协议一定明确字节序

推荐统一:

go 复制代码
binary.BigEndian // 网络协议通用

避免跨平台问题。


不要直接传 struct

尤其:

go 复制代码
unsafe.Pointer

直接转字节。

风险极高:

  • padding
  • 对齐
  • Go 版本差异
  • 架构差异

网络读取必须考虑半包

牢记:

go 复制代码
io.ReadFull() // 确保读满

协议必须有边界

推荐:

text 复制代码
长度 + 数据

不要依赖:

  • 特殊字符
  • EOF
  • 分隔符

高频场景避免反射

binary.Write() 很方便。

但高性能场景:

建议:

go 复制代码
手动编码

例如:

go 复制代码
buf[0] = byte(num >> 24)

性能更高。


思考与升华

为什么所有协议最后都会回归"字节流"

因为:

计算机最终只认识 bit。

无论:

  • JSON
  • protobuf
  • gRPC
  • MySQL
  • Redis

最后都会变成:

text 复制代码
0101010101

区别只是:

如何组织这些 bit。


一个极简协议实现思路

例如:

text 复制代码
| version | type | length | body |

编码:

go 复制代码
write(version)
write(type)
write(length)
write(body)

解码:

go 复制代码
read(version)
read(type)
read(length)
read(body)

你会发现:

所有协议,本质上都是"状态机 + 字节流解析"。


点睛总结

二进制处理看似只是:

text 复制代码
数据 ↔ 字节

但真正核心的是:

"如何让不同机器、不同语言、不同系统,对同一份数据达成一致理解"。

而这:

才是所有协议设计、序列化框架、网络通信的真正基础。


总结

Go 的二进制处理,本质上是:

用稳定、紧凑、可预测的方式组织数据。

掌握它之后:

你会真正理解:

  • TCP 协议
  • RPC 框架
  • protobuf
  • 数据序列化
  • 文件格式
  • 网络通信

底层到底在做什么。

很多时候:

高级框架,只是在帮你"自动管理字节流"。

相关推荐
RSTJ_16255 小时前
PYTHON+AI LLM DAY FIFITY-ONE
开发语言·人工智能·python
qingfeng154155 小时前
企业微信定时群发实战:API 如何实现批量消息自动发送?
java·开发语言·python·自动化·企业微信
丁劲犇5 小时前
QodeAssist:为msys2 ucrt64 Qt Creator 注入 AI 灵魂的开源插件
开发语言·人工智能·qt
qingfeng154155 小时前
企业微信 API 可以做什么?
java·开发语言·python·自动化·企业微信
梧桐和风5 小时前
2026 年 Java 趋势:AI 浪潮下,Java 会过时吗?
java·开发语言·人工智能
lsx2024065 小时前
React 组件详解
开发语言
嗨嗨的迷子5 小时前
JDK 17 远程调试连不上 5005:从 attach timeout 到 JDWP 监听地址变更
java·开发语言
Chase_______5 小时前
【Java杂项】为什么 long 可以自动转 float?宽化基本类型转换与精度丢失详解
java·开发语言·python
listhi5205 小时前
基于QT的串口心电波形实时显示系统
开发语言·qt