You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 21:48:30