4 回答

TA貢獻1785條經驗 獲得超8個贊
是的,不幸的是你不能嵌入類型參數T
。我還將爭辯說,在一般情況下,您不應該嘗試展平輸出 JSON。通過約束T
with 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

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:"/"}}}

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": "/"
}
}
}
是的,約束類型需要被引用兩次——但所有自定義都是代碼本地化的,因此不需要自定義封送拆收器。

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":"/"}}}
- 4 回答
- 0 關注
- 157 瀏覽
添加回答
舉報