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
我知道这并非理想的类型安全方案,但如果宠物的类型是无限制的,并且由于我们不清楚你的代码需要对宠物进行何种操作,我倾向于将宠物建模为 type Pet map[string]any,并为 Pet 添加 Name 和 Type 的方法。如果预期所有宠物都像猫和狗一样拥有一个自定义的布尔属性,则再添加一个对应的方法。
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:使用代码生成工具
对于大型项目,可以使用代码生成工具如 easyjson 或 ffjson 配合自定义模板:
// 使用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确实需要更多样板代码,但通过合理的设计模式可以保持代码的可维护性。

