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 关注
- 132 浏览
添加回答
举报