Golang中如何使用db标签而非json标签实现Unmarshal扫描嵌套SQL结果
问题描述
我通过SQL查询获取嵌套结构的国家-省份-城市数据,SQL语句如下:
SELECT C.*, P.provinces FROM countries AS C LEFT JOIN ( SELECT P.country_id, json_agg(json_build_object( 'id', P.id, 'name', P.name, 'slug', P.slug, 'cities', Ci.cities )) AS provinces FROM provinces AS P LEFT JOIN ( SELECT Ci.province_id, json_agg(json_build_object( 'id', Ci.id, 'name', Ci.name, 'slug', Ci.slug )) AS cities FROM cities AS Ci GROUP BY Ci.province_id ) AS Ci ON Ci.province_id = P.id GROUP BY P.country_id ) AS P ON P.country_id = C.id
之后用Go结构体映射这些数据,结构体定义如下:
type Country struct { Id int64 `json:"id" db:"id"` ISOCode2 string `json:"isoCode2" db:"iso_code_2"` ISOCode3 string `json:"isoCode3" db:"iso_code_3"` ISONumCode string `json:"isoNumCode" db:"iso_num_code"` Name string `json:"name" db:"name"` Slug string `json:"slug" db:"slug"` Provinces SliceProvince `json:"provinces" db:"provinces"` } type SliceProvince []Province func (provinces *SliceProvince) Scan(src any) (err error) { if src == nil { return } var source []byte switch src := src.(type) { case []byte: source = src case string: source = []byte(src) default: return fmt.Errorf("unsupported type in scan:%v", src) } err = json.Unmarshal(source, provinces) return } type Province struct { Id int64 `json:"id" db:"id"` Name string `json:"name" db:"name"` Slug string `json:"slug" db:"slug"` Cities SliceCity `json:"cities" db:"cities"` } type SliceCity []City func (cities *SliceCity) Scan(src any) (err error) { if src == nil { return } var source []byte switch src := src.(type) { case []byte: source = src case string: source = []byte(src) default: return fmt.Errorf("unsupported type in scan") } err = json.Unmarshal(source, cities) return } type City struct { Id int64 `json:"id" db:"id"` Name string `json:"name" db:"name"` Slug string `json:"slug" db:"slug"` }
我的核心需求是:在Scan方法执行JSON反序列化时,使用结构体的db标签而非json标签进行字段映射,不想采用先转map替换键再重新序列化的复杂方案。
解决方案
方案1:自定义反射实现db标签映射
编写通用的反序列化函数,利用Go反射读取结构体的db标签,将JSON键与结构体字段对应,支持嵌套结构和切片:
import ( "encoding/json" "fmt" "reflect" ) // UnmarshalWithDBTag 按结构体db标签反序列化JSON数据 func UnmarshalWithDBTag(data []byte, v any) error { var raw map[string]any if err := json.Unmarshal(data, &raw); err != nil { return err } val := reflect.ValueOf(v).Elem() typ := val.Type() for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) dbTag := field.Tag.Get("db") if dbTag == "" || dbTag == "-" { continue } rawVal, ok := raw[dbTag] if !ok { continue } fieldVal := val.Field(i) switch fieldVal.Kind() { case reflect.Slice: // 处理切片类型 rawSlice, ok := rawVal.([]any) if !ok { return fmt.Errorf("field %s is not a slice", field.Name) } sliceVal := reflect.MakeSlice(fieldVal.Type(), 0, len(rawSlice)) elemType := fieldVal.Type().Elem() for _, item := range rawSlice { itemBytes, err := json.Marshal(item) if err != nil { return err } elem := reflect.New(elemType).Interface() if err := UnmarshalWithDBTag(itemBytes, elem); err != nil { return err } sliceVal = reflect.Append(sliceVal, reflect.ValueOf(elem).Elem()) } fieldVal.Set(sliceVal) case reflect.Struct: // 处理嵌套结构体 structBytes, err := json.Marshal(rawVal) if err != nil { return err } if err := UnmarshalWithDBTag(structBytes, fieldVal.Addr().Interface()); err != nil { return err } default: // 基础类型直接赋值 fieldVal.Set(reflect.ValueOf(rawVal).Convert(field.Type)) } } return nil }
修改SliceProvince和SliceCity的Scan方法,替换原有的json.Unmarshal:
func (provinces *SliceProvince) Scan(src any) (err error) { if src == nil { return } var source []byte switch src := src.(type) { case []byte: source = src case string: source = []byte(src) default: return fmt.Errorf("unsupported type in scan:%v", src) } return UnmarshalWithDBTag(source, provinces) } func (cities *SliceCity) Scan(src any) (err error) { if src == nil { return } var source []byte switch src := src.(type) { case []byte: source = src case string: source = []byte(src) default: return fmt.Errorf("unsupported type in scan") } return UnmarshalWithDBTag(source, cities) }
这个方案无需修改SQL,实现后可复用在所有需要db标签映射的结构体上。
方案2:调整SQL输出的JSON键为db标签值
直接修改SQL中json_build_object的键,使其与结构体的db标签完全匹配,这样原有的json.Unmarshal就能直接工作,无需改动Go代码。
比如,若Province的Name字段db标签是province_name,则调整SQL对应部分:
json_agg(json_build_object( 'id', P.id, 'province_name', P.name, 'slug', P.slug, 'cities', Ci.cities )) AS provinces
这个方案最简洁,没有额外代码或依赖,只需调整SQL输出格式。
方案3:使用第三方库支持自定义标签
使用支持自定义标签的JSON库(如json-iterator/go),配置其使用db标签进行反序列化:
import "github.com/json-iterator/go" // 初始化配置,指定使用db标签 var json = jsoniter.Config{ TagKey: "db", }.Froze() // 修改Scan方法 func (provinces *SliceProvince) Scan(src any) (err error) { if src == nil { return } var source []byte switch src := src.(type) { case []byte: source = src case string: source = []byte(src) default: return fmt.Errorf("unsupported type in scan:%v", src) } return json.Unmarshal(source, provinces) } func (cities *SliceCity) Scan(src any) (err error) { if src == nil { return } var source []byte switch src := src.(type) { case []byte: source = src case string: source = []byte(src) default: return fmt.Errorf("unsupported type in scan") } return json.Unmarshal(source, cities) }
该方案无需自己编写反射逻辑,利用成熟第三方库快速实现需求,但会引入外部依赖。
内容的提问来源于stack exchange,提问作者ankit729
相关产品推荐
相关产品推荐

