msgpack之tinylib/msgp的使用

253 阅读12分钟

1. 官方文档

github

2. 对 Go time.Time的支持

msgp原生支持 Go 的time.Time类型,如下是官方文档截图:

image.png

如下是代码生成器生成的处理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(对应 UTC 2023-10-01 00:00:00)。
    • 反序列化端(时区 UTC-5)将时间戳解析为本地时间 2023-09-30 19:00:00,而非原始时间 08:00
场景 2:依赖时区的业务逻辑
  • 问题
    若业务逻辑依赖具体时区(例如“每天北京时间 9:00 执行任务”),丢失时区会导致逻辑错误。
    示例

    • 存储的时间戳 1696147200(UTC 2023-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. 经验收获

  1. Unix时间戳绝对时间,是自1970年1月1日 00:00:00 UTC以来经过的秒数。
  2. 时间带有「时区」属性,存在依赖时间的业务逻辑必须明确时区。
  3. 同一个时间(绝对时间),在不同时区表达的本地时间不同,需要注意同一份时间数据被跨时区系统消费的问题。

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一定会被写入。
不同类型的零值不同:如:false0""complex(0,0)niltime.Time{} or YourStruct{}


需要注意的:

  1. 使用omitempty存在性能权衡。因为需要生成额外处理代码,对于常态下不是零值的字段加该标签性能影响比较严重,但对应常态下是零值的字段加该标签性能可能更好。
  2. 解码(或反序列化)不受该tag影响。在编码(或序列化)时被omitempty省略的字段在解码期间不会涉及它,意味着解码后结构体该字段会保留其原来的值(而不会强制设为零值)。

msgp:clearomitted指令

一句话解释该指令的作用:可以使用//msgp:clearomitted指令在解码时强制将省略的字段设为其类型的零值。

//msgp:clearomitted 是 msgp 库中的一个指令,用于控制在反序列化时如何处理被标记为 omitempty 的字段。其核心作用如下:

  • 默认行为:当结构体字段使用 omitempty 标签时,序列化会忽略该字段的零值。但反序列化时,如果输入数据中缺少该字段,目标结构体的对应字段会保留原有值(而不是自动设置为零值)。
  • 启用 //msgp:clearomitted 后:在反序列化过程中,若输入数据中缺少被标记为 omitempty 的字段,msgp 会将这些字段显式重置为其类型的零值,覆盖结构体原有值。

这是官方文档的解释:

image.png


使用场景

假设你需要确保反序列化后的结构体严格反映输入数据未被包含的字段必须清零(而非保留旧值)。例如:

//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}