1. 官方文档
2. 对 Go time.Time的支持
msgp原生支持 Go 的time.Time类型,如下是官方文档截图:
如下是代码生成器生成的处理time.Time类型字段的序列化和反序列化代码:
- 序列化:
// AppendTime appends a time.Time to the slice as a MessagePack extension
func AppendTime(b []byte, t time.Time) []byte {
o, n := ensure(b, TimeSize)
t = t.UTC()
o[n] = mext8
o[n+1] = 12
o[n+2] = TimeExtension
// 序列化成时间戳,时间戳本质是UTC的绝对时间
putUnix(o[n+3:], t.Unix(), int32(t.Nanosecond()))
return o
}
- 反序列化:
func ReadTimeBytes(b []byte) (t time.Time, o []byte, err error) {
if len(b) < 6 {
err = ErrShortBytes
return
}
typ, o, b, err := readExt(b)
if err != nil {
return
}
switch typ {
case TimeExtension:
if len(b) != 12 {
err = ErrShortBytes
return
}
sec, nsec := getUnix(b)
// 反序列化成UTC时间,再转成本地时区时间
t = time.Unix(sec, int64(nsec)).Local()
return
case MsgTimeExtension:
switch len(b) {
case 4:
t = time.Unix(int64(binary.BigEndian.Uint32(b)), 0).Local()
return
case 8:
v := binary.BigEndian.Uint64(b)
nanos := int64(v >> 34)
if nanos > 999999999 {
// In timestamp 64 and timestamp 96 formats, nanoseconds must not be larger than 999999999.
err = InvalidTimestamp{Nanos: nanos}
return
}
t = time.Unix(int64(v&(1<<34-1)), nanos).Local()
return
case 12:
nanos := int64(binary.BigEndian.Uint32(b))
if nanos > 999999999 {
// In timestamp 64 and timestamp 96 formats, nanoseconds must not be larger than 999999999.
err = InvalidTimestamp{Nanos: nanos}
return
}
ux := int64(binary.BigEndian.Uint64(b[4:]))
t = time.Unix(ux, nanos).Local()
return
default:
err = InvalidTimestamp{FieldLength: len(b)}
return
}
default:
err = errExt(int8(b[2]), TimeExtension)
return
}
}
注意时区问题
当 msgp 将 time.Time 序列化为整数时间戳(如 Unix 时间戳)时,会丢失时区信息(因为时间戳本质是 UTC 时间的绝对值),这可能导致以下影响:
1. 核心问题:时区信息丢失
- 序列化行为:msgp 默认将
time.Time转换为 UTC 时间的 Unix 时间戳(例如1696147200对应 UTC 时间的2023-10-01 00:00:00)。 - 反序列化行为:从时间戳还原为
time.Time时,生成的time.Time对象默认使用 UTC 时区,而非原始时区。
2. 具体影响场景
场景 1:跨时区系统的数据同步
-
问题:
如果序列化端和反序列化端处于不同时区,时间戳会被统一解析为 UTC,可能导致本地时间显示错误。
示例:- 序列化端(时区
UTC+8)的时间2023-10-01 08:00:00转换为时间戳1696118400(对应 UTC2023-10-01 00:00:00)。 - 反序列化端(时区
UTC-5)将时间戳解析为本地时间2023-09-30 19:00:00,而非原始时间08:00。
- 序列化端(时区
场景 2:依赖时区的业务逻辑
-
问题:
若业务逻辑依赖具体时区(例如“每天北京时间 9:00 执行任务”),丢失时区会导致逻辑错误。
示例:- 存储的时间戳
1696147200(UTC2023-10-01 00:00:00)在北京时间(UTC+8)应为08:00,但反序列化后可能被误判为其他时区的00:00。
- 存储的时间戳
场景 3:数据持久化与恢复
-
问题:
存储的时间戳无法还原原始时区,导致历史数据的时间表示与实际记录不符。
示例:- 用户记录的日志时间
2023-10-01 08:00:00 CST,序列化为时间戳后存储。 - 反序列化时还原为
2023-10-01 00:00:00 UTC,若未明确标注时区,可能被误认为是 UTC 时间而非 CST。
- 用户记录的日志时间
场景 4:与其他系统交互
- 问题:
若第三方系统预期时间包含时区信息(如 ISO 8601 格式2023-10-01T08:00:00+08:00),仅传递时间戳会导致解析错误或歧义。
3. 解决方案
方案 1:统一使用 UTC 时间
- 操作:
在序列化和反序列化时,强制所有时间转换为 UTC,业务逻辑统一基于 UTC 处理。 - 优点:
避免时区差异,简化时间管理。 - 缺点:
前端显示需额外转换本地时间。
// 序列化前转换为 UTC
func (t *MyTime) MarshalMsg(b []byte) ([]byte, error) {
utcTime := t.Time.UTC()
return msgp.AppendInt64(b, utcTime.Unix()), nil
}
// 反序列化后默认为 UTC
func (t *MyTime) UnmarshalMsg(b []byte) ([]byte, error) {
ts, b, err := msgp.ReadInt64Bytes(b)
if err != nil {
return nil, err
}
t.Time = time.Unix(ts, 0).UTC()
return b, nil
}
方案 2:显式存储时区偏移量
- 操作:
在数据结构中额外存储时区偏移(如+08:00)或时区名称(如Asia/Shanghai)。 - 优点:
保留完整时区信息,精准还原时间。 - 缺点:
增加数据存储和传输开销。
type Event struct {
Timestamp int64 `msgp:"timestamp"` // Unix 时间戳(UTC)
Timezone string `msgp:"timezone"` // 时区偏移 "+08:00" 或名称 "Asia/Shanghai"
}
// 反序列化时还原带时区的时间
func (e *Event) LocalTime() (time.Time, error) {
loc, err := time.LoadLocation(e.Timezone)
if err != nil {
return time.Time{}, err
}
return time.Unix(e.Timestamp, 0).In(loc), nil
}
方案 3:使用字符串格式(ISO 8601)
- 操作:
自定义序列化逻辑,将time.Time转换为带时区的字符串(如2023-10-01T08:00:00+08:00)。 - 优点:
兼容性强,直观可读。 - 缺点:
序列化后数据体积较大,解析性能略低于时间戳。
//msgp:marshal time.Time
func (t time.Time) MarshalMsg(b []byte) ([]byte, error) {
return msgp.AppendString(b, t.Format(time.RFC3339)), nil
}
//msgp:unmarshal time.Time
func (t *time.Time) UnmarshalMsg(b []byte) ([]byte, error) {
str, b, err := msgp.ReadStringBytes(b)
if err != nil {
return nil, err
}
parsedTime, err := time.Parse(time.RFC3339, str)
if err != nil {
return nil, err
}
*t = parsedTime
return b, nil
}
4. 选择建议
| 场景 | 推荐方案 |
|---|---|
| 系统内部处理,统一 UTC | 方案 1(UTC 时间) |
| 需精确还原原始时区 | 方案 2(显式存储时区) |
| 与其他系统交互,强调可读性 | 方案 3(ISO 8601 字符串) |
5. 总结
- 默认时间戳会丢失时区:msgp 的默认行为可能导致跨时区系统的时间误解,需根据场景选择处理策略。
- 业务需求决定方案:若时区无关紧要,用 UTC;若需精准还原,显式存储时区;若需兼容性,用字符串。
- 性能权衡:时间戳体积小、性能高,字符串可读性强但效率略低。根据数据量和调用频率选择。
6. 经验收获
Unix时间戳是绝对时间,是自1970年1月1日 00:00:00 UTC以来经过的秒数。- 时间带有「时区」属性,存在依赖时间的业务逻辑必须明确时区。
- 同一个时间(绝对时间),在不同时区表达的本地时间不同,需要注意同一份时间数据被跨时区系统消费的问题。
3. omitempty支持
官方文档:Zero Values; Omitempty and Allownil
msgp支持omitempty标签,序列化时省略零值字段,可节省序列化结果的体积,和JSON行为类似,看如下例子:
type Example struct {
A string `msg:"a,omitempty"`
B int `msg:"b,omitempty"`
C string `msg:"c"`
}
Example序列化时,A不为空字符串、B不为0时才会被写入序列化结果,C一定会被写入。
不同类型的零值不同:如:false, 0, "", complex(0,0), nil, time.Time{} or YourStruct{}。
需要注意的:
- 使用
omitempty存在性能权衡。因为需要生成额外处理代码,对于常态下不是零值的字段加该标签性能影响比较严重,但对应常态下是零值的字段加该标签性能可能更好。 - 解码(或反序列化)不受该tag影响。在编码(或序列化)时被
omitempty省略的字段在解码期间不会涉及它,意味着解码后结构体该字段会保留其原来的值(而不会强制设为零值)。
msgp:clearomitted指令
一句话解释该指令的作用:可以使用//msgp:clearomitted指令在解码时强制将省略的字段设为其类型的零值。
//msgp:clearomitted 是 msgp 库中的一个指令,用于控制在反序列化时如何处理被标记为 omitempty 的字段。其核心作用如下:
- 默认行为:当结构体字段使用
omitempty标签时,序列化会忽略该字段的零值。但反序列化时,如果输入数据中缺少该字段,目标结构体的对应字段会保留原有值(而不是自动设置为零值)。 - 启用
//msgp:clearomitted后:在反序列化过程中,若输入数据中缺少被标记为omitempty的字段,msgp 会将这些字段显式重置为其类型的零值,覆盖结构体原有值。
这是官方文档的解释:
使用场景
假设你需要确保反序列化后的结构体严格反映输入数据,未被包含的字段必须清零(而非保留旧值)。例如:
//msgp:clearomitted
type User struct {
ID int `msgp:"id,omitempty"`
Name string `msgp:"name,omitempty"`
}
- 序列化:若
ID为 0 或Name为空字符串,则忽略该字段。 - 反序列化:若输入数据无
id字段,反序列化后User.ID会被强制设为0(而非保持之前的值)。
3. 对自定义结构体的支持
结构体中存在其他结构体类型的字段时,也要对依赖的其他结构体生成代码,如下例子:
package main
import (
"fmt"
"log"
"github.com/tinylib/msgp/msgp"
)
//go:generate go install github.com/tinylib/msgp@latest
//go:generate msgp
// Person 是一个包含个人信息的结构体
type Person struct {
Name string `msg:"name"`
Age int `msg:"age"`
Address *Address `msg:"address"`
}
// Address 是一个包含地址信息的结构体
type Address struct {
City string `msg:"city"`
Country string `msg:"country"`
}
除了要对Person生成代码,还要对Address结构体生成代码。
序列化和反序列化示例:
func main() {
// 创建一个 Person 实例,其中包含 Address 字段
person := Person{
Name: "Alice",
Age: 30,
Address: nil,
}
// 序列化
m1, err := person.MarshalMsg(nil)
if err != nil {
log.Fatalf("Failed to marshal: %v", err)
}
fmt.Printf("m1: %X\n", m1)
// 反序列化
var decoded = &Person{}
if _, err := decoded.UnmarshalMsg(m1); err != nil {
log.Fatalf("Failed to unmarshal m1: %v", err)
}
fmt.Printf("decoded: %+v\n", decoded)
fmt.Printf("decoded.Address struct: %+v\n", decoded.Address)
person.Name = "John"
person.Age = 18
person.Address = &Address{
City: "Beijing",
Country: "China",
}
m2, err := person.MarshalMsg(nil)
if err != nil {
log.Fatalf("Failed to marshal: %v", err)
}
fmt.Printf("m2: %X\n", m2)
var decoded2 = &Person{}
if _, err := decoded2.UnmarshalMsg(m2); err != nil {
log.Fatalf("Failed to unmarshal m2: %v", err)
}
fmt.Printf("decoded2: %+v\n", decoded2)
fmt.Printf("decoded2.Address struct: %+v\n", decoded2.Address)
}
运行结果:
m1: 83A46E616D65A5416C696365A36167651EA761646472657373C0
decoded: &{Name:Alice Age:30 Address:<nil>}
decoded.Address struct: <nil>
m2: 83A46E616D65A44A6F686EA361676512A76164647265737382A463697479A74265696A696E67A7636F756E747279A54368696E61
decoded2: &{Name:John Age:18 Address:0xc000090080}
decoded2.Address struct: &{City:Beijing Country:China}
4. 对Go 切片类型的支持
msgp完全支持 Go 的切片(slice)类型,具体说明:
- 基础类型切片:msgp 支持所有 Go 基础类型的切片(如
[]int、[]string、[]byte等)。 - 自定义类型切片:如果切片元素是自定义结构体或其他复合类型,需确保元素类型本身支持 msgp 的编解码(例如通过生成代码或实现接口)。
示例:
//go:generate go install github.com/tinylib/msgp@latest
//go:generate msgp
type Data struct {
IDs []int `msgp:"ids"` // int 切片
Names []string `msgp:"names"` // string 切片
Scores []float64 `msgp:"scores"` // float64 切片
}
type User struct {
ID int `msgp:"id"`
Name string `msgp:"name"`
}
type Group struct {
Members []*User `msgp:"members"` // User 结构体切片
}
func main() {
testData()
fmt.Printf("\n=====testGroup=====\n\n")
testGroup()
}
func testData() {
d1 := Data{
IDs: nil,
Names: make([]string, 0),
Scores: nil,
}
m1, err := d1.MarshalMsg(nil)
if err != nil {
panic(fmt.Sprintf("d1.MarshalMsg err: %v", err))
}
fmt.Printf("m1: %X\n", m1)
decodeD1 := &Data{}
_, err = decodeD1.UnmarshalMsg(m1)
if err != nil {
panic(fmt.Sprintf("decodeD1.UnmarshalMsg err: %v", err))
}
fmt.Printf("decodeD1:%+v\n", decodeD1)
fmt.Printf("decodeD1.IDs == nil:%+v\n", decodeD1.IDs == nil)
fmt.Printf("decodeD1.Names == nil:%+v\n", decodeD1.Names == nil)
fmt.Printf("decodeD1.Scores == nil:%+v\n", decodeD1.Scores == nil)
fmt.Println("==================")
d2 := Data{
IDs: []int{1, 10, 100},
Names: []string{"a", "b", "c"},
Scores: []float64{1.1, 2.22, 3.333, 4.4444, 5.55555, 6.66666, 7.7777777, 8.88888888},
}
m2, err := d2.MarshalMsg(nil)
if err != nil {
panic(fmt.Sprintf("d2.MarshalMsg err: %v", err))
}
fmt.Printf("m2: %X\n", m2)
decodeD2 := &Data{}
_, err = decodeD2.UnmarshalMsg(m2)
if err != nil {
panic(fmt.Sprintf("decodeD2.UnmarshalMsg err: %v", err))
}
fmt.Printf("decodeD2:%+v\n", decodeD2)
}
func testGroup() {
g1 := Group{}
m1, err := g1.MarshalMsg(nil)
if err != nil {
panic(fmt.Sprintf("g1.MarshalMsg err: %v", err))
}
fmt.Printf("m1: %X\n", m1)
decodeG1 := &Group{}
_, err = decodeG1.UnmarshalMsg(m1)
if err != nil {
panic(fmt.Sprintf("decodeG1.UnmarshalMsg err: %v", err))
}
fmt.Printf("decodeG1:%+v\n", decodeG1)
fmt.Printf("decodeG1.Members:%+v\n", decodeG1.Members == nil)
fmt.Println("==================")
g2 := Group{
Members: []*User{
&User{
ID: 1,
Name: "ceshi1",
},
nil,
&User{
ID: 3,
Name: "ceshi3",
},
},
}
m2, err := g2.MarshalMsg(nil)
if err != nil {
panic(fmt.Sprintf("g2.MarshalMsg err: %v", err))
}
fmt.Printf("m2: %X\n", m2)
decodeG2 := &Group{}
_, err = decodeG2.UnmarshalMsg(m2)
if err != nil {
panic(fmt.Sprintf("decodeG2.UnmarshalMsg err: %v", err))
}
fmt.Printf("decodeG2:%+v\n", decodeG2)
fmt.Printf("decodeG2.Members[0].:%+v\n", decodeG2.Members[0])
fmt.Printf("decodeG2.Members[1]:%+v\n", decodeG2.Members[1])
fmt.Printf("decodeG2.Members[2]:%+v\n", decodeG2.Members[2])
}
运行结果:
m1: 83A349447390A54E616D657390A653636F72657390
decodeD1:&{IDs:[] Names:[] Scores:[]}
decodeD1.IDs == nil:true
decodeD1.Names == nil:true
decodeD1.Scores == nil:true
==================
m2: 83A349447393010A64A54E616D657393A161A162A163A653636F72657398CB3FF199999999999ACB4001C28F5C28F5C3CB400AA9FBE76C8B44CB4011C710CB295E9ECB401638E219652BD4CB401AAAA8EB463498CB401F1C71C1E43B7ECB4021C71C717AC192
decodeD2:&{IDs:[1 10 100] Names:[a b c] Scores:[1.1 2.22 3.333 4.4444 5.55555 6.66666 7.7777777 8.88888888]}
=====testGroup=====
m1: 81A74D656D6265727390
decodeG1:&{Members:[]}
decodeG1.Members:true
==================
m2: 81A74D656D626572739382A2494401A44E616D65A6636573686931C082A2494403A44E616D65A6636573686933
decodeG2:&{Members:[0xc000010180 <nil> 0xc000010198]}
decodeG2.Members[0].:&{ID:1 Name:ceshi1}
decodeG2.Members[1]:<nil>
decodeG2.Members[2]:&{ID:3 Name:ceshi3}