如何在Go+Gin+sqlc中实现JSON与模型的自动编解码?
解决sqlc生成模型与Gin BindJSON不兼容的方案
1. 自定义兼容JSON的NullTime类型,让sqlc生成时替换默认类型
既然sqlc生成的sql.NullTime不支持JSON编解码,我们可以自己实现一个兼容的类型,然后让sqlc在生成代码时用这个自定义类型替换标准库的sql.NullTime,这样生成的模型就能直接和Gin的BindJSON配合使用,不需要手动转换。
首先定义自定义的NullTime类型,实现JSON编解码接口:
// types/null_time.go package types import ( "database/sql" "encoding/json" "time" ) type NullTime struct { sql.NullTime } // UnmarshalJSON 实现json.Unmarshaler接口,处理字符串格式的时间 func (nt *NullTime) UnmarshalJSON(data []byte) error { var s *string if err := json.Unmarshal(data, &s); err != nil { return err } if s == nil { nt.Valid = false return nil } t, err := time.Parse(time.RFC3339, *s) if err != nil { // 可根据业务需求扩展支持更多时间格式 return err } nt.Time = t nt.Valid = true return nil } // MarshalJSON 实现json.Marshaler接口,有效时间转RFC3339字符串,无效则返回null func (nt NullTime) MarshalJSON() ([]byte, error) { if !nt.Valid { return json.Marshal(nil) } return json.Marshal(nt.Time.Format(time.RFC3339)) }
然后修改sqlc的配置文件(sqlc.yaml),添加类型替换规则:
version: "2" sql: - schema: "schema.sql" queries: "queries.sql" engine: "postgresql" # 根据你的数据库类型调整 gen: go: package: "data" out: "data" overrides: - db_type: "timestamp" nullable: true go_type: "your-module-path/types.NullTime" # 替换为你的自定义类型路径
这样sqlc生成的Author结构体就会自动使用自定义类型:
type Author struct { Name string `json:"name"` Bio types.NullTime `json:"bio"` }
之后直接用c.BindJSON(&authorParams)就能正常编解码,返回数据时也无需额外转换。
2. 自定义Gin的JSON绑定器,处理sql.NullTime类型
如果不想改动sqlc的生成配置,也可以自定义Gin的绑定逻辑,在绑定JSON时专门处理sql.NullTime类型。
首先实现自定义绑定器,基于Gin默认JSON绑定器扩展:
// binders/custom_json.go package binders import ( "database/sql" "encoding/json" "net/http" "reflect" "time" "github.com/gin-gonic/gin/binding" ) type CustomJSONBinding struct{} func (CustomJSONBinding) Name() string { return "json" } func (CustomJSONBinding) Bind(req *http.Request, obj any) error { decoder := json.NewDecoder(req.Body) // 注册sql.NullTime的自定义解码器 decoder.RegisterDecoder(reflect.TypeOf(sql.NullTime{}), func(data []byte, v any) error { nt := v.(*sql.NullTime) var s *string if err := json.Unmarshal(data, &s); err != nil { return err } if s == nil { nt.Valid = false return nil } t, err := time.Parse(time.RFC3339, *s) if err != nil { return err } nt.Time = t nt.Valid = true return nil }) return decoder.Decode(obj) }
然后在Gin初始化时替换默认的JSON绑定器:
// main.go import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "your-module-path/binders" ) func main() { r := gin.Default() // 替换默认JSON绑定器 binding.Binders["json"] = binders.CustomJSONBinding{} // 注册路由... r.Run() }
这样使用c.BindJSON时会自动处理sql.NullTime类型。如果需要处理响应编码,可通过类型别名给sql.NullTime添加MarshalJSON方法:
// types/null_time.go package types import ( "database/sql" "encoding/json" "time" ) type NullTime sql.NullTime func (nt NullTime) MarshalJSON() ([]byte, error) { if !nt.Valid { return json.Marshal(nil) } return json.Marshal(nt.Time.Format(time.RFC3339)) } // 返回数据时做类型转换即可 // respBio := types.NullTime(data.Author.Bio)
3. 用代码生成工具自动生成转换逻辑
如果以上方案都不想用,可借助mapstructure库自动处理结构体字段映射,减少手动编写转换代码的冗余:
首先安装依赖:
go get github.com/mitchellh/mapstructure
然后在代码中使用:
// controller.go import ( "database/sql" "reflect" "time" "github.com/mitchellh/mapstructure" ) // 绑定请求到临时结构 var apiAuthor map[string]any if err := c.BindJSON(&apiAuthor); err != nil { // 处理错误 } var authorParams data.InsertFileAuthorParams config := &mapstructure.DecoderConfig{ Result: &authorParams, DecodeHook: mapstructure.ComposeDecodeHookFunc( // 自定义字符串转sql.NullTime的逻辑 func(f reflect.Type, t reflect.Type, data any) (any, error) { if f.Kind() == reflect.String && t == reflect.TypeOf(sql.NullTime{}) { s := data.(string) tm, err := time.Parse(time.RFC3339, s) if err != nil { return nil, err } return sql.NullTime{Time: tm, Valid: true}, nil } return data, nil }, ), } decoder, err := mapstructure.NewDecoder(config) if err != nil { // 处理错误 } if err := decoder.Decode(apiAuthor); err != nil { // 处理错误 }
内容的提问来源于stack exchange,提问作者elixirfan2001
相关产品推荐
相关产品推荐

