亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定
已解決430363個問題,去搜搜看,總會有你想問的

如何在 Go 中為通用類型展平 JSON

如何在 Go 中為通用類型展平 JSON

Go
白衣染霜花 2023-02-28 21:17:12
我正在嘗試在 Go 中實現 HAL,只是想看看我是否可以。這意味著我有一個對HAL有效負載通用的類型,并且還包含_links:type HAL[T any] struct {    Payload T    Links   Linkset `json:"_links,omitempty"`}在 HAL 規范中,有效負載實際上位于頂層,而不是嵌套在其中 - 就像 Siren 一樣。所以這意味著給出以下內容:type TestPayload struct {    Name   string `json:"name"`    Answer int    `json:"answer"`}    hal := HAL[TestPayload]{        Payload: TestPayload{            Name:   "Graham",            Answer: 42,        },        Links: Linkset{            "self": {                {Href: "/"},            },        },    }生成的 JSON 應該是:{    "name": "Graham",    "answer": 42,    "_links": {      "self": {"href": "/"}    }}但是我想不出一個好的方法來讓這個 JSON 編組工作。我已經看到將有效負載嵌入為匿名成員的建議,如果它不是通用的,它會很好用。不幸的是,您不能以這種方式嵌入泛型類型,所以這是行不通的。我可能可以編寫一個MarshalJSON方法來完成這項工作,但我想知道是否有任何標準方法來實現這一點?
查看完整描述

4 回答

?
慕的地10843

TA貢獻1785條經驗 獲得超8個贊

是的,不幸的是你不能嵌入類型參數T。我還將爭辯說,在一般情況下,您不應該嘗試展平輸出 JSON。通過約束Twith any,您幾乎可以接受任何類型,但并非所有類型都有可以提升到您的HAL結構中的字段。

這在語義上是不一致的。

如果您嘗試嵌入沒有字段的類型,輸出的 JSON 將不同。以解決方案為例reflect.StructOf,沒有什么能阻止我實例化HAL[[]int]{ Payload: []int{1,2,3}, Links: ... },在這種情況下,輸出將是:

{"X":[1,2,3],"Links":{"self":{"href":"/"}}}

這會使您的 JSON 序列化隨用于實例化的類型發生變化T,這對于閱讀您的代碼的人來說不容易發現。代碼的可預測性較低,并且您正在有效地對抗類型參數提供的泛化。

使用命名字段Payload T更好,因為:

  • 輸出 JSON 始終(對于大多數意圖和目的)與實際結構一致

  • 解組也保持可預測的行為

  • 代碼的可伸縮性不是問題,因為您不必重復HAL構建匿名結構的所有字段

OTOH,如果您的要求恰好是將結構編組為扁平化,而其他所有內容都帶有鍵(HAL 類型可能就是這種情況),至少通過檢查實現使其顯而易見,并為任何情況提供reflect.ValueOf(hal.Payload).Kind() == reflect.Struct默認MarshalJSON情況否則T可能。將不得不在JSONUnmarshal.

T這是一個帶有反射的解決方案,當您將更多字段添加到主結構時,它可以在不是結構時工作并縮放:

// necessary to marshal HAL without causing infinite loop

// can't declare inside the method due to a current limitation with Go generics

type tmp[T any] HAL[T]


func (h HAL[T]) MarshalJSON() ([]byte, error) {

    // examine Payload, if it isn't a struct, i.e. no embeddable fields, marshal normally

    v := reflect.ValueOf(h.Payload)

    if v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface {

        v = v.Elem()

    }

    if v.Kind() != reflect.Struct {

        return json.Marshal(tmp[T](h))

    }


    // flatten all fields into a map

    m := make(map[string]any)

    // flatten Payload first

    for i := 0; i < v.NumField(); i++ {

        key := jsonkey(v.Type().Field(i))

        m[key] = v.Field(i).Interface()

    }

    // flatten the other fields

    w := reflect.ValueOf(h)

    // start at 1 to skip the Payload field

    for i := 1; i < w.NumField(); i++ {

        key := jsonkey(w.Type().Field(i))

        m[key] = w.Field(i).Interface()

    }

    return json.Marshal(m)

}


func jsonkey(field reflect.StructField) string {

    // trickery to get the json tag without omitempty and whatnot

    tag := field.Tag.Get("json")

    tag, _, _ = strings.Cut(tag, ",")

    if tag == "" {

        tag = field.Name

    }

    return tag

}

HAL[TestPayload]orHAL[*TestPayload]它輸出:

{"answer":42,"name":"Graham","_links":{"self":{"href":"/"}}}

HAL[[]int]它輸出:

{"Payload":[1,2,3],"_links":{"self":{"href":"/"}}}

游樂場:https://go.dev/play/p/bWGXWj_rC5F


查看完整回答
反對 回復 2023-02-28
?
尚方寶劍之說

TA貢獻1788條經驗 獲得超4個贊

我會制作一個自定義 JSON 編解碼器,_links在為有效負載生成的 JSON 末尾插入字段。


編組器。



type Link struct {

    Href string `json:"href"`

}


type Linkset map[string]Link


type HAL[T any] struct {

    Payload T

    Links   Linkset `json:"_links,omitempty"`

}


func (h HAL[T]) MarshalJSON() ([]byte, error) {

    payloadJson, err := json.Marshal(h.Payload)

    if err != nil {

        return nil, err

    }

    if len(payloadJson) == 0 {

        return nil, fmt.Errorf("Empty payload")

    }

    if h.Links != nil {

        return appendField(payloadJson, "_links", h.Links)

    }

    return payloadJson, nil

}


func appendField[T any](raw []byte, fieldName string, v T) ([]byte, error) {

    // The JSON data must be braced in {}

    if raw[0] != '{' || raw[len(raw)-1] != '}' {

        return nil, fmt.Errorf("Not an object: %s", string(raw))

    }

    valJson, err := json.Marshal(v)

    if err != nil {

        return nil, err

    }

    // Add the field at the end of the json text

    result := bytes.NewBuffer(raw[:len(raw)-1])

    // Append `"<fieldName>":value`

    // Insert comma if the `raw` object is not empty

    if len(raw) > 2 {

        result.WriteByte(',')

    }

    // tag

    result.WriteByte('"')

    result.WriteString(fieldName)

    result.WriteByte('"')

    // colon

    result.WriteByte(':')

    // value

    result.Write(valJson)

    // closing brace

    result.WriteByte('}')

    return result.Bytes(), nil

}

Payload如果序列化為 JSON 對象以外的對象,編組器將返回錯誤。原因是編解碼器_links只能為對象添加字段。


解組器:


func (h *HAL[T]) UnmarshalJSON(raw []byte) error {

    // Unmarshal fields of the payload first.

    // Unmarshal the whole JSON into the payload, it is safe:

    // decorer ignores unknow fields and skips "_links".

    if err := json.Unmarshal(raw, &h.Payload); err != nil {

        return err

    }

    // Get "_links": scan trough JSON until "_links" field

    links := make(Linkset)

    exists, err := extractField(raw, "_links", &links)

    if err != nil {

        return err

    }

    if exists {

        h.Links = links

    }

    return nil

}


func extractField[T any](raw []byte, fieldName string, v *T) (bool, error) {

    // Scan through JSON until field is found

    decoder := json.NewDecoder(bytes.NewReader(raw))

    t := must(decoder.Token())

    // should be `{`

    if t != json.Delim('{') {

        return false, fmt.Errorf("Not an object: %s", string(raw))

    }

    t = must(decoder.Token())

    if t == json.Delim('}') {

        // Empty object

        return false, nil

    }

    for decoder.More() {

        name, ok := t.(string)

        if !ok {

            return false, fmt.Errorf("must never happen: expected string, got `%v`", t)

        }

        if name != fieldName {

            skipValue(decoder)

        } else {

            if err := decoder.Decode(v); err != nil {

                return false, err

            }

            return true, nil

        }

        if decoder.More() {

            t = must(decoder.Token())

        }

    }

    return false, nil

}


func skipValue(d *json.Decoder) {

    braceCnt := 0

    for d.More() {

        t := must(d.Token())

        if t == json.Delim('{') || t == json.Delim('[') {

            braceCnt++

        }

        if t == json.Delim('}') || t == json.Delim(']') {

            braceCnt--

        }

        if braceCnt == 0 {

            return

        }

    }

}

解組器在非對象上也會失敗。需要讀取_links字段。為此,輸入必須是一個對象。


完整示例:https://go.dev/play/p/E3NN2T7Fbnm


func main() {

    hal := HAL[TestPayload]{

        Payload: TestPayload{

            Name:   "Graham",

            Answer: 42,

        },

        Links: Linkset{

            "self": Link{Href: "/"},

        },

    }

    bz := must(json.Marshal(hal))

    println(string(bz))


    var halOut HAL[TestPayload]

    err := json.Unmarshal(bz, &halOut)

    if err != nil {

        println("Decode failed: ", err.Error())

    }

    fmt.Printf("%#v\n", halOut)

}

輸出:


{"name":"Graham","answer":42,"_links":{"self":{"href":"/"}}}

main.HAL[main.TestPayload]{Payload:main.TestPayload{Name:"Graham", Answer:42}, Links:main.Linkset{"self":main.Link{Href:"/"}}}



查看完整回答
反對 回復 2023-02-28
?
蝴蝶不菲

TA貢獻1810條經驗 獲得超4個贊

把事情簡單化。


是的,嵌入類型會很好 - 但由于目前不可能(從 開始go1.19)嵌入泛型類型 - 只需將其寫成內聯:


body, _ = json.Marshal(

    struct {

        TestPayload

        Links       Linkset `json:"_links,omitempty"`

    }{

        TestPayload: hal.Payload,

        Links:       hal.Links,

    },

)

https://go.dev/play/p/8yrB-MzUVK-


{

    "name": "Graham",

    "answer": 42,

    "_links": {

        "self": {

            "href": "/"

        }

    }

}

是的,約束類型需要被引用兩次——但所有自定義都是代碼本地化的,因此不需要自定義封送拆收器。


查看完整回答
反對 回復 2023-02-28
?
牧羊人nacy

TA貢獻1862條經驗 獲得超7個贊

是的,嵌入是最簡單的方法,正如您所寫,您目前無法嵌入類型參數。


但是,您可以構造一個使用反射嵌入類型參數的類型。我們可以實例化此類型并對其進行編組。


例如:


func (hal HAL[T]) MarshalJSON() ([]byte, error) {

    t := reflect.StructOf([]reflect.StructField{

        {

            Name:      "X",

            Anonymous: true,

            Type:      reflect.TypeOf(hal.Payload),

        },

        {

            Name: "Links",

            Type: reflect.TypeOf(hal.Links),

        },

    })


    v := reflect.New(t).Elem()

    v.Field(0).Set(reflect.ValueOf(hal.Payload))

    v.Field(1).Set(reflect.ValueOf(hal.Links))


    return json.Marshal(v.Interface())

}

這將輸出(在Go Playground上嘗試):


{"name":"Graham","answer":42,"Links":{"self":{"href":"/"}}}


查看完整回答
反對 回復 2023-02-28
  • 4 回答
  • 0 關注
  • 157 瀏覽
慕課專欄
更多

添加回答

舉報

0/150
提交
取消
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號