
本文详解如何在 Go 中正确实现支持 JSON 和 BSON 序列化的自定义 Timestamp 类型,解决指针接收器误用、零值处理及结构体字段初始化等常见错误,并提供可直接复用的生产级代码。
本文详解如何在 go 中正确实现支持 json 和 bson 序列化的自定义 `timestamp` 类型,解决指针接收器误用、零值处理及结构体字段初始化等常见错误,并提供可直接复用的生产级代码。
在 Go 应用开发中,尤其是对接 MongoDB(通过 mgo 或 mongo-go-driver)和 REST API 时,常需对时间字段进行统一格式化控制——例如将 time.Time 序列化为 Unix 时间戳整数(而非 ISO8601 字符串),同时保持数据库存储的兼容性。此时,自定义 Timestamp 类型是推荐方案,但实践中极易因接收器类型不一致、零值处理不当或指针转换错误引发编译失败或运行时异常。
以下是一个修正后、可直接投入生产使用的 Timestamp 实现(基于 time.Time 底层,兼容 mgo/bson v2 及标准 encoding/json):
// timestamp/timestamp.go
package timestamp
import (
"encoding/json"
"fmt"
"strconv"
"time"
"go.mongodb.org/mongo-driver/bson" // 推荐:使用官方 driver;若用 mgo,请替换为 "labix.org/v2/mgo/bson"
)
// Timestamp 是 time.Time 的别名,用于定制序列化行为
type Timestamp time.Time
// Now 返回当前时间的 Timestamp 实例(指针)
func Now() *Timestamp {
t := time.Now()
return (*Timestamp)(&t)
}
// New 从 time.Time 创建 *Timestamp(安全构造函数)
func New(t time.Time) *Timestamp {
return (*Timestamp)(&t)
}
// MarshalJSON 将 Timestamp 序列化为 Unix 秒级时间戳(int64 → string)
func (t *Timestamp) MarshalJSON() ([]byte, error) {
if t == nil || time.Time(*t).IsZero() {
return []byte("null"), nil
}
ts := time.Time(*t).Unix()
return []byte(strconv.FormatInt(ts, 10)), nil
}
// UnmarshalJSON 从 JSON 数字字符串反序列化为 Timestamp
func (t *Timestamp) UnmarshalJSON(data []byte) error {
if len(data) == 0 || string(data) == "null" {
*t = Timestamp(time.Time{})
return nil
}
// 去除引号(如 "1717023456" → 1717023456)
s := string(data)
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
s = s[1 : len(s)-1]
}
ts, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return fmt.Errorf("failed to parse timestamp JSON: %w", err)
}
*t = Timestamp(time.Unix(ts, 0))
return nil
}
// MarshalBSON / UnmarshalBSON(适用于 mongo-go-driver)
func (t *Timestamp) MarshalBSON() ([]byte, error) {
if t == nil || time.Time(*t).IsZero() {
return bson.Marshal(nil)
}
return bson.Marshal(time.Time(*t))
}
func (t *Timestamp) UnmarshalBSON(data []byte) error {
var tm time.Time
if err := bson.Unmarshal(data, &tm); err != nil {
return err
}
*t = Timestamp(tm)
return nil
}
// String 实现 fmt.Stringer,便于日志调试
func (t *Timestamp) String() string {
if t == nil {
return "<nil>"
}
return time.Time(*t).Format(time.RFC3339)
}✅ 关键修复与设计要点
- 指针接收器一致性:所有修改状态的方法(UnmarshalJSON、UnmarshalBSON)及需访问/修改 *t 的方法(如 MarshalJSON 中判空)*必须使用 `Timestamp接收器**。原错误invalid indirect of t (type Timestamp)正是因GetBSON使用了值接收器却尝试解引用*t`。
- 零值与 nil 安全:MarshalJSON 和 UnmarshalJSON 显式处理 nil 和零时间(IsZero()),避免 panic 或意外数据丢失。
- 构造便捷性:提供 Now() 和 New() 工厂函数,消除手动类型转换风险(如 (*Timestamp)(&now) 易出错且可读性差)。
-
结构体字段声明规范:
type User struct { Name string `json:"name"` CreatedAt *timestamp.Timestamp `bson:"created_at,omitempty" json:"created_at,omitempty"` }初始化时应使用工厂函数:
u := User{ Name: "Joe Bloggs", CreatedAt: timestamp.Now(), // ✅ 推荐 // 或 CreatedAt: timestamp.New(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), }
⚠️ 注意事项
- 若项目仍使用已归档的 mgo 库,请将 bson 导入路径替换为 "labix.org/v2/mgo/bson",并确保 GetBSON/SetBSON 方法签名与 mgo 要求一致(本文示例适配现代 mongo-go-driver)。
- JSON 时间戳默认为秒级整数;如需毫秒级,将 Unix() 替换为 UnixMilli(),并在 UnmarshalJSON 中调用 time.UnixMilli(ts)。
- 在 Gin/Echo 等框架中,*Timestamp 字段可直接参与 JSON 绑定(c.BindJSON()),无需额外配置。
通过此实现,你将获得一个健壮、符合 Go 惯例、跨序列化协议一致的时间戳抽象,彻底规避原始代码中的编译错误与逻辑缺陷。
立即学习“go语言免费学习笔记(深入)”;










