English | 中文
一个高性能的 Go 二进制数据序列化库,采用 C 风格的结构体定义。
- 🚀 卓越性能:优化的二进制序列化,支持反射缓存
- 💡 简洁 API:基于结构体标签的直观配置,无需样板代码
- 🛡️ 类型安全:强类型检查和全面的错误处理
- 🔄 灵活编码:支持大端和小端字节序
- 📦 丰富类型支持:支持原始类型、数组、切片和自定义填充
- 🎯 零依赖:纯 Go 实现,无外部依赖
go get github.com/shengyanli1982/struc/v2
package main
import (
"bytes"
"github.com/shengyanli1982/struc/v2"
)
type Message struct {
Size int `struc:"int32,sizeof=Payload"` // 自动追踪负载大小
Payload []byte // 动态二进制数据
Flags uint16 `struc:"little"` // 小端编码
}
func main() {
var buf bytes.Buffer
// 打包数据
msg := &Message{
Payload: []byte("Hello, World!"),
Flags: 1234,
}
if err := struc.Pack(&buf, msg); err != nil {
panic(err)
}
// 解包数据
result := &Message{}
if err := struc.Unpack(&buf, result); err != nil {
panic(err)
}
}
- 原始类型:
bool
、int8
-int64
、uint8
-uint64
、float32
、float64
- 复合类型:字符串、字节切片、数组
- 特殊类型:用于对齐的填充字节
- 自动管理可变大小字段的长度
- 消除手动大小计算和追踪
- 减少二进制协议实现中的潜在错误
- 反射缓存以提高重复操作性能
- 高效的内存分配
- 优化的编码/解码路径
type Example struct {
Length int `struc:"int32,sizeof=Data"` // 大小追踪
Data []byte // 动态数据
Version uint16 `struc:"little"` // 字节序控制
Padding [4]byte `struc:"[4]pad"` // 显式填充
}
struc
标签支持多种格式和选项,用于精确控制二进制数据:
type BasicTypes struct {
Int8Val int `struc:"int8"` // 8位整数
Int16Val int `struc:"int16"` // 16位整数
Int32Val int `struc:"int32"` // 32位整数
Int64Val int `struc:"int64"` // 64位整数
UInt8Val int `struc:"uint8"` // 8位无符号整数
UInt16Val int `struc:"uint16"` // 16位无符号整数
UInt32Val int `struc:"uint32"` // 32位无符号整数
UInt64Val int `struc:"uint64"` // 64位无符号整数
BoolVal bool `struc:"bool"` // 布尔值
Float32Val float32 `struc:"float32"` // 32位浮点数
Float64Val float64 `struc:"float64"` // 64位浮点数
}
type ArrayTypes struct {
// 固定大小字节数组(4字节)
ByteArray []byte `struc:"[4]byte"`
// 固定大小整数数组(5个int32值)
IntArray []int32 `struc:"[5]int32"`
// 用于对齐的填充字节
Padding []byte `struc:"[3]pad"`
// 固定大小字符串(作为字节数组处理)
FixedString string `struc:"[8]byte"`
}
type DynamicTypes struct {
// 追踪 Data 长度的大小字段
Size int `struc:"int32,sizeof=Data"`
// 大小由 Size 追踪的动态字节切片
Data []byte
// 使用 uint8 追踪 AnotherData 的大小字段
Size2 int `struc:"uint8,sizeof=AnotherData"`
// 另一个动态数据字段
AnotherData []byte
// 带大小引用的动态字符串字段
StrSize int `struc:"uint16,sizeof=Text"`
Text string `struc:"[]byte"`
}
type ByteOrderTypes struct {
// 大端编码整数
BigInt int32 `struc:"big"`
// 小端编码整数
LittleInt int32 `struc:"little"`
// 未指定则默认为大端
DefaultInt int32
}
type SpecialTypes struct {
// 在打包/解包时跳过此字段(二进制中保留空间)
Ignored int `struc:"skip"`
// 完全忽略此字段(不包含在二进制中)
Private string `struc:"-"`
// 从其他字段获取大小引用
Data []byte `struc:"sizefrom=Size"`
// 自定义类型实现
YourCustomType CustomBinaryer
}
标签格式:struc:"type,option1,option2"
type
:二进制类型(如 int8、uint16、[4]byte)big
/little
:字节序指定sizeof=Field
:指定此字段追踪另一个字段的大小sizefrom=Field
:指定此字段的大小由另一个字段追踪skip
:在打包/解包时跳过此字段(二进制中保留空间)-
:完全忽略此字段(不包含在二进制中)[N]type
:长度为 N 的固定大小类型数组[]type
:动态大小的类型数组/切片
与 JSON 序列化可以选择性地省略字段不同,二进制序列化需要严格且固定的字节布局。以下是不支持 omitempty
的原因:
-
固定的二进制布局
- 二进制协议要求精确的字节定位
- 每个字段必须占据其预定义的位置和大小
- 省略字段会破坏字节对齐
-
解析依赖性
- 二进制数据是按字节顺序解析的
- 如果省略字段,字节流会错位
- 接收端无法正确重建数据结构
-
协议稳定性
- 二进制协议需要严格的版本控制
- 允许可选字段会破坏协议的稳定性
- 无法保证向后兼容性
-
调试复杂性
- 字段省略会导致二进制数据变得不可预测
- 极大增加了字节流调试的难度
- 提高了问题排查的复杂度
如果你需要标记某些字段为可选,可以考虑以下替代方案:
- 使用显式的标志字段来表示有效性
- 为可选字段使用默认值
- 使用
struc:"-"
标签完全排除字段不进行序列化
如果你需要完全控制类型的二进制序列化和反序列化,可以实现 CustomBinaryer
接口:
type CustomBinaryer interface {
// Pack 将数据序列化到字节切片
Pack(p []byte, opt *Options) (int, error)
// Unpack 从 Reader 中反序列化数据
Unpack(r io.Reader, length int, opt *Options) error
// Size 返回序列化后的字节大小
Size(opt *Options) int
// String 返回类型的字符串表示
String() string
}
例如,实现一个 3 字节整数类型:
// 使用示例
type Message struct {
Value CustomBinaryer // 使用自定义类型
}
// Int3 是一个自定义的 3 字节整数类型
type Int3 uint32
func (i *Int3) Pack(p []byte, opt *Options) (int, error) {
// 将 4 字节整数转换为 3 字节
var tmp [4]byte
binary.BigEndian.PutUint32(tmp[:], uint32(*i))
copy(p, tmp[1:]) // 只复制后 3 字节
return 3, nil
}
func (i *Int3) Unpack(r io.Reader, length int, opt *Options) error {
var tmp [4]byte
if _, err := r.Read(tmp[1:]); err != nil {
return err
}
*i = Int3(binary.BigEndian.Uint32(tmp[:]))
return nil
}
func (i *Int3) Size(opt *Options) int {
return 3 // 固定 3 字节大小
}
func (i *Int3) String() string {
return strconv.FormatUint(uint64(*i), 10)
}
自定义类型的优势:
- 完全控制二进制格式
- 支持特殊的数据布局
- 可以实现压缩或加密
- 适合处理遗留系统的特殊格式
-
使用适当的类型
- 将 Go 类型与其二进制协议对应物匹配
- 当大小已知时使用固定大小数组
- 对动态数据使用带
sizeof
的切片
-
错误处理
- 始终检查 Pack/Unpack 返回的错误
- 在处理之前验证数据大小
-
性能优化
- 尽可能重用结构体
- 考虑对频繁使用的结构使用对象池
-
内存管理
-
库在打包时,会根据数据大小预分配精确大小的缓冲区
bufferSize := packer.Sizeof(value, options) buffer := make([]byte, bufferSize)
-
解包时,库使用内部 4K 缓冲区来实现高效解包
-
解包时,结构体中的切片/字符串字段会直接引用这些内部缓冲区
-
只要你的结构体字段还在引用这些缓冲区,它们就会保留在内存中
type Message struct { Data []byte // 这个字段会引用内部缓冲区 } func processRetain() { messages := make([]*Message, 0) // >> 重要的是: // Field 结构体只是一个元数据描述对象 // 它的生命周期结束与否并不影响已经通过 unsafe 操作设置的用户结构体字段 // 因为 unsafe 操作已经直接修改了用户结构体字段的底层指针,指向了 4K buffer // >> 所以: // Field 结构体的释放并不会导致 4K buffer 上的切片引用消失 // 只有当使用这些切片的用户结构体被 GC 时,这些引用才会消失 // 4K buffer 的生命周期取决于所有引用它的用户结构体的生命周期 // 每个解包的消息的 Data 字段都引用内部缓冲区 for i := 0; i < 10; i++ { msg := &Message{} // 解包过程中: // 1. unpackBasicTypeSlicePool 提供 4K buffer // 2. Field 结构体处理元数据 // 3. unsafe 操作将 msg.Data 指向 4K buffer 的一部分 struc.Unpack(reader, msg) // 这时即使 Field 结构体被释放 // msg.Data 仍然指向 4K buffer // 只有当 msg 被 GC,这个引用才会消失 messages = append(messages, msg) // 内部缓冲区无法被 GC,因为 msg.Data 引用着它 // Field 结构体的生命周期与 4K buffer 的引用无关 // 4K buffer 的引用由用户结构体持有 // 只有当所有引用这个 4K buffer 的用户结构体都被 GC 时,这个 buffer 才可能被回收 } }
-
要释放对内部缓冲区的引用,你可以将字段设为 nil 或复制数据:
func processRelease() { msg := &Message{} struc.Unpack(reader, msg) // 方法1:如果不再需要数据,直接设为 nil msg.Data = nil // 现在 msg.Data 为 nil,不再引用内部缓冲区 // 方法2:如果需要保留数据,进行复制 if needData { dataCopy := make([]byte, len(msg.Data)) copy(dataCopy, msg.Data) msg.Data = dataCopy // 现在 msg.Data 引用我们的副本 } // 如果没有其他结构体引用,内部缓冲区现在可以被 GC 了 }
-
$ go.exe test -benchmem -run=^$ -bench . github.com/shengyanli1982/struc/v2
Starting pprof server on :6060
goos: windows
goarch: amd64
pkg: github.com/shengyanli1982/struc/v2
cpu: 12th Gen Intel(R) Core(TM) i5-12400F
BenchmarkArrayEncode-12 3288741 366.7 ns/op 137 B/op 4 allocs/op
BenchmarkSliceEncode-12 3110095 389.8 ns/op 137 B/op 4 allocs/op
BenchmarkArrayDecode-12 3410102 343.3 ns/op 73 B/op 2 allocs/op
BenchmarkSliceDecode-12 2904127 423.7 ns/op 113 B/op 4 allocs/op
BenchmarkEncode-12 3297550 364.5 ns/op 56 B/op 2 allocs/op
BenchmarkStdlibEncode-12 8496386 139.0 ns/op 24 B/op 1 allocs/op
BenchmarkManualEncode-12 48760538 24.66 ns/op 64 B/op 1 allocs/op
BenchmarkDecode-12 3493039 329.7 ns/op 55 B/op 1 allocs/op
BenchmarkStdlibDecode-12 6607056 176.8 ns/op 32 B/op 2 allocs/op
BenchmarkManualDecode-12 100000000 11.71 ns/op 8 B/op 1 allocs/op
BenchmarkFullEncode-12 1000000 1546 ns/op 216 B/op 2 allocs/op
BenchmarkFullDecode-12 1000000 1684 ns/op 279 B/op 4 allocs/op
BenchmarkFieldPool-12 7039993 162.4 ns/op 56 B/op 2 allocs/op
BenchmarkGetFormatString/Simple-12 4950135 238.6 ns/op 21 B/op 2 allocs/op
BenchmarkGetFormatString/Complex-12 2522713 465.0 ns/op 48 B/op 3 allocs/op
MIT 许可证 - 详见 LICENSE 文件