Golang中动态类型切片的Unmarshal/Marshal可维护方案

Golang中动态类型切片的Unmarshal/Marshal可维护方案 我在思考,每次处理类似以下示例的JSON时,如何以最合理的方式避免编写大量代码。这是一个我无法控制的REST API的潜在响应。它包含一个宠物列表,其中每个宠物可能是1+种类型中的一种。

{
    "pets": [
        {
            "type": "Cat",
            "name": "Mittens",
            "is_angry": true
        },
        {
            "type": "Dog",
            "name": "Spot",
            "has_ball": false
        }
    ]
}

我已经设法编写了以下代码来处理它。但这感觉有点取巧,特别是如果这种模式在与REST API交互时可能出现多次。

package main

import (
	"encoding/json"
	"fmt"
)

type Pet struct {
	Type string `json:"type"`
	Name string `json:"name"`
}

type DynamicPet interface {
	isPet()
}

func (p Pet) isPet() {}

type Cat struct {
	Pet
	IsAngry bool `json:"is_angry"`
}

type Dog struct {
	Pet
	HasBall bool `json:"has_ball"`
}

var petTypeMap = map[string]func() DynamicPet{
	"Cat": func() DynamicPet { return &Cat{} },
	"Dog": func() DynamicPet { return &Dog{} },
}

const (
	CatType    = "Cat"
	DogType    = "Dog"
	PingusType = "Pingus"
)

type DynamicPetWrapper struct {
	Pet DynamicPet `json:"-"`
}

func (p *DynamicPetWrapper) UnmarshalJSON(data []byte) error {
	var typeData struct {
		Type string `json:"type"`
	}
	if err := json.Unmarshal(data, &typeData); err != nil {
		return err
	}

	petType, ok := petTypeMap[typeData.Type]
	if !ok {
		return fmt.Errorf("unknown pet type: %s", typeData.Type)
	}
	p.Pet = petType()

	if err := json.Unmarshal(data, p.Pet); err != nil {
		return err
	}

	return nil
}

func (p DynamicPetWrapper) MarshalJSON() ([]byte, error) {
	return json.Marshal(p.Pet)
}

type PetList struct {
	Pets []DynamicPetWrapper `json:"pets"`
}

func main() {
	// json example PetList
	jsonData := []byte(`
	{
		"pets": [
			{
				"type": "Cat",
				"name": "Mittens",
				"is_angry": true
			},
			{
				"type": "Dog",
				"name": "Spot",
				"has_ball": false
			}
		]
	}
	`)

	// deserialize json into PetList dynamically unmarshalling into the correct types
	var petList PetList
	if err := json.Unmarshal(jsonData, &petList); err != nil {
		fmt.Println("Error:", err)
		return
	}

	// iterate over the list of pets and do logic based on pet type
	for _, petWrapper := range petList.Pets {
		switch pet := petWrapper.Pet.(type) {
		case *Cat:
			fmt.Printf("Cat: is_angry %v\n", pet.IsAngry)
		case *Dog:
			fmt.Printf("Dog: has_ball %v\n", pet.HasBall)
		}
	}

	// serialize back to JSON to make sure it worked both ways
	jsonData2, err := json.Marshal(petList)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println(string(jsonData2))
}

让我夜不能寐的一件事是,在Rust中,我不需要包装类型,也不需要编写特殊的编组逻辑,它就能正常工作,像魔法一样,而且代码量只有一半。由于匹配分支的工作方式,如果我还没有处理任何可能实现的新类型,编译器会对我发出警告。我可以期望在我的代码中的任何地方轻松使用相同的类型,而无需额外的层次或复杂性。

use serde::{Deserialize, Serialize};
use serde_json;

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
enum Pet {
    Cat(Cat),
    Dog(Dog),
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Cat {
    name: String,
    is_angry: bool,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Dog {
    name: String,
    has_ball: bool,
}

#[derive(Serialize, Deserialize, Debug)]
struct PetList {
    pets: Vec<Pet>,
}

fn main() {
    // json example PetList
    let json = r#"
        {
            "pets": [
                {
                    "type": "Cat",
                    "name": "Mittens",
                    "is_angry": true
                },
                {
                    "type": "Dog",
                    "name": "Spot",
                    "has_ball": false
                }
            ]
        }
    "#;

    // deserialize json into PetList dynamically unmarshalling into the correct types
    let pet_list: PetList = serde_json::from_str(json).unwrap();

    // can easily iterate over the list of pets and do logic based on pet type
    for pet in pet_list.pets.iter() {
        match pet {
            Pet::Cat(cat) => println!("Cat: is_angry {}", cat.is_angry),
            Pet::Dog(dog) => println!("Dog: has_ball {}", dog.has_ball),
        }
    }

    // serialize back to JSON to make sure it worked both ways
    let json = serde_json::to_string(&pet_list).unwrap();
    println!("{}", json);

}

我只是觉得,每次在Go中必须与复杂的JSON交互时,我的代码库都会失控。感谢大家对这个话题的任何讨论!


更多关于Golang中动态类型切片的Unmarshal/Marshal可维护方案的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

我知道这并非理想的类型安全方案,但如果宠物的类型是无限制的,并且由于我们不清楚你的代码需要对宠物进行何种操作,我倾向于将宠物建模为 type Pet map[string]any,并为 Pet 添加 NameType 的方法。如果预期所有宠物都像猫和狗一样拥有一个自定义的布尔属性,则再添加一个对应的方法。

type Pet map[string]any

更多关于Golang中动态类型切片的Unmarshal/Marshal可维护方案的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


以下是我的实现方式,虽然去掉基础结构体可能会更简单,但……

package main

import (
	"encoding/json"
	"fmt"
)

type Pet interface {
	GetType() string
	GetName() string
	IsAngry() bool
	HasBall() bool
}
type PetBase struct {
	Name  string `json:"name"`
	Ptype string `json:"type"`
	Angry *bool  `json:"is_angry,omitempty"`
	Ball  *bool  `json:"has_ball,omitempty"`
}

func (p *PetBase) GetName() string {
	return p.Name
}
func (p *PetBase) GetType() string {
	return p.Ptype
}
func (p *PetBase) IsAngry() bool {
	if p.Angry != nil {
		return *p.Angry
	}
	return false
}
func (p *PetBase) HasBall() bool {
	if p.Ball != nil {
		return *p.Ball
	}
	return false
}

type Cat struct {
	PetBase
}
type Dog struct {
	PetBase
}

type PetList struct {
	Pets []PetBase `json:"pets"`
}

func main() {
	// json 示例 PetList
	jsonData := []byte(`
	{
		"pets": [
			{
				"type": "Cat",
				"name": "Mittens",
				"is_angry": true
			},
			{
				"type": "Dog",
				"name": "Spot",
				"has_ball": false
			}
		]
	}
	`)

	// 将 json 反序列化为 PetList,动态解组到正确的类型
	var data PetList
	if err := json.Unmarshal(jsonData, &data); err != nil {
		fmt.Println("Unmarshall Error:", err)
		return
	}
	fmt.Printf("PetList: %v\n", data)
	// 遍历宠物列表并根据宠物类型执行逻辑
	for _, petb := range data.Pets {
		var mypet Pet
		switch petb.GetType() {
		case "Cat":
			mypet = &Cat{PetBase: petb}
		case "Dog":
			mypet = &Dog{PetBase: petb}
		}
		fmt.Printf("%s: is_angry %v\n", mypet.GetType(), mypet.IsAngry())
		fmt.Printf("%s: has_ball %v\n", mypet.GetType(), mypet.HasBall())
	}

	// 序列化回 JSON 以确保双向转换都有效
	jsonData2, err := json.Marshal(data)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println(string(jsonData2))
}

在Go中处理动态类型JSON确实需要更多样板代码,但可以通过一些模式来改善可维护性。以下是几种专业方案:

方案1:使用json.RawMessage延迟解析

package main

import (
    "encoding/json"
    "fmt"
)

type PetType string

const (
    CatType PetType = "Cat"
    DogType PetType = "Dog"
)

type Pet struct {
    Type PetType          `json:"type"`
    Data json.RawMessage  `json:"-"`
}

type Cat struct {
    Name    string `json:"name"`
    IsAngry bool   `json:"is_angry"`
}

type Dog struct {
    Name    string `json:"name"`
    HasBall bool   `json:"has_ball"`
}

type PetList struct {
    Pets []Pet `json:"pets"`
}

func (p *Pet) UnmarshalJSON(data []byte) error {
    var raw map[string]json.RawMessage
    if err := json.Unmarshal(data, &raw); err != nil {
        return err
    }
    
    if err := json.Unmarshal(raw["type"], &p.Type); err != nil {
        return err
    }
    
    p.Data = data
    return nil
}

func (p Pet) MarshalJSON() ([]byte, error) {
    return p.Data, nil
}

func (p Pet) Parse() (interface{}, error) {
    switch p.Type {
    case CatType:
        var cat Cat
        if err := json.Unmarshal(p.Data, &cat); err != nil {
            return nil, err
        }
        return cat, nil
    case DogType:
        var dog Dog
        if err := json.Unmarshal(p.Data, &dog); err != nil {
            return nil, err
        }
        return dog, nil
    default:
        return nil, fmt.Errorf("unknown pet type: %s", p.Type)
    }
}

func main() {
    jsonData := []byte(`
    {
        "pets": [
            {
                "type": "Cat",
                "name": "Mittens",
                "is_angry": true
            },
            {
                "type": "Dog",
                "name": "Spot",
                "has_ball": false
            }
        ]
    }
    `)

    var petList PetList
    if err := json.Unmarshal(jsonData, &petList); err != nil {
        panic(err)
    }

    for _, pet := range petList.Pets {
        parsed, err := pet.Parse()
        if err != nil {
            fmt.Printf("Error parsing pet: %v\n", err)
            continue
        }
        
        switch p := parsed.(type) {
        case Cat:
            fmt.Printf("Cat: %s, angry: %v\n", p.Name, p.IsAngry)
        case Dog:
            fmt.Printf("Dog: %s, has ball: %v\n", p.Name, p.HasBall)
        }
    }
}

方案2:使用工厂模式与反射

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

type PetFactory struct {
    types map[string]reflect.Type
}

func NewPetFactory() *PetFactory {
    pf := &PetFactory{
        types: make(map[string]reflect.Type),
    }
    pf.Register("Cat", reflect.TypeOf(Cat{}))
    pf.Register("Dog", reflect.TypeOf(Dog{}))
    return pf
}

func (pf *PetFactory) Register(typeName string, typ reflect.Type) {
    pf.types[typeName] = typ
}

func (pf *PetFactory) Create(typeName string) (interface{}, error) {
    typ, exists := pf.types[typeName]
    if !exists {
        return nil, fmt.Errorf("unknown type: %s", typeName)
    }
    return reflect.New(typ).Interface(), nil
}

type DynamicPetList struct {
    Pets []json.RawMessage `json:"pets"`
}

func (pf *PetFactory) UnmarshalPets(data []byte) ([]interface{}, error) {
    var list DynamicPetList
    if err := json.Unmarshal(data, &list); err != nil {
        return nil, err
    }

    pets := make([]interface{}, 0, len(list.Pets))
    for _, raw := range list.Pets {
        var base struct {
            Type string `json:"type"`
        }
        if err := json.Unmarshal(raw, &base); err != nil {
            return nil, err
        }

        pet, err := pf.Create(base.Type)
        if err != nil {
            return nil, err
        }

        if err := json.Unmarshal(raw, pet); err != nil {
            return nil, err
        }
        pets = append(pets, pet)
    }
    return pets, nil
}

func main() {
    jsonData := []byte(`
    {
        "pets": [
            {
                "type": "Cat",
                "name": "Mittens",
                "is_angry": true
            },
            {
                "type": "Dog",
                "name": "Spot",
                "has_ball": false
            }
        ]
    }
    `)

    factory := NewPetFactory()
    pets, err := factory.UnmarshalPets(jsonData)
    if err != nil {
        panic(err)
    }

    for _, pet := range pets {
        switch p := pet.(type) {
        case *Cat:
            fmt.Printf("Cat: %s, angry: %v\n", p.Name, p.IsAngry)
        case *Dog:
            fmt.Printf("Dog: %s, has ball: %v\n", p.Name, p.HasBall)
        }
    }
}

方案3:使用代码生成工具

对于大型项目,可以使用代码生成工具如 easyjsonffjson 配合自定义模板:

// 使用go:generate指令生成代码
//go:generate go run generate_pets.go

package main

import (
    "encoding/json"
    "fmt"
)

// 生成的代码会包含这些接口的实现
type PetUnmarshaler interface {
    UnmarshalPet(data []byte) error
    PetType() string
}

type GeneratedPetList struct {
    Pets []json.RawMessage `json:"pets"`
}

func (l *GeneratedPetList) UnmarshalTo(petType string, unmarshaler PetUnmarshaler) ([]interface{}, error) {
    result := make([]interface{}, 0)
    
    for _, raw := range l.Pets {
        var base struct {
            Type string `json:"type"`
        }
        if err := json.Unmarshal(raw, &base); err != nil {
            return nil, err
        }
        
        if base.Type == petType {
            if err := unmarshaler.UnmarshalPet(raw); err != nil {
                return nil, err
            }
            result = append(result, unmarshaler)
        }
    }
    return result, nil
}

方案4:使用第三方库

使用 mapstructure 库简化动态解析:

package main

import (
    "encoding/json"
    "fmt"
    "github.com/mitchellh/mapstructure"
)

type PetConfig struct {
    Type string                 `json:"type"`
    Data map[string]interface{} `json:"-"`
}

func (p *PetConfig) UnmarshalJSON(data []byte) error {
    var raw map[string]interface{}
    if err := json.Unmarshal(data, &raw); err != nil {
        return err
    }
    
    p.Type = raw["type"].(string)
    p.Data = raw
    return nil
}

func (p *PetConfig) Decode() (interface{}, error) {
    switch p.Type {
    case "Cat":
        var cat Cat
        if err := mapstructure.Decode(p.Data, &cat); err != nil {
            return nil, err
        }
        return cat, nil
    case "Dog":
        var dog Dog
        if err := mapstructure.Decode(p.Data, &dog); err != nil {
            return nil, err
        }
        return dog, nil
    default:
        return nil, fmt.Errorf("unknown type: %s", p.Type)
    }
}

func main() {
    jsonData := []byte(`
    {
        "pets": [
            {
                "type": "Cat",
                "name": "Mittens",
                "is_angry": true
            },
            {
                "type": "Dog",
                "name": "Spot",
                "has_ball": false
            }
        ]
    }
    `)

    var list struct {
        Pets []PetConfig `json:"pets"`
    }
    
    if err := json.Unmarshal(jsonData, &list); err != nil {
        panic(err)
    }

    for _, pet := range list.Pets {
        decoded, err := pet.Decode()
        if err != nil {
            fmt.Printf("Error: %v\n", err)
            continue
        }
        
        switch p := decoded.(type) {
        case Cat:
            fmt.Printf("Cat: %s\n", p.Name)
        case Dog:
            fmt.Printf("Dog: %s\n", p.Name)
        }
    }
}

这些方案各有优劣。方案1最适合简单场景,方案2适合需要动态注册类型的场景,方案3适合大型项目需要性能优化,方案4适合快速原型开发。Go确实需要更多样板代码,但通过合理的设计模式可以保持代码的可维护性。

回到顶部