使用泛型将JSON解析为结构体的Golang实践指南

使用泛型将JSON解析为结构体的Golang实践指南 我正在开发一个用于练习的评分软件。我将成绩保存到JSON文件中,并希望尝试使用泛型来减少代码量。我对Go语言以及整体编程还比较陌生。

以下是JSON内容:

{
    "homework": [
        {
            "Name": "Homework1",
            "Grade": 45
        },
        {
            "Name": "Homework2",
            "Grade": 45
        }
    ],
    "lab": [
        {
            "Name": "Lab1",
            "Grade": 12
        },
        {
            "Name": "Lab2",
            "Grade": 45
        }
    ]
}

我的想法类似于下面的代码,但我知道Go不支持宏替换。 以下是结构体定义:

type Subject struct {
    Homework []Homework `json:"homework"`
    Lab      []Lab      `json:"lab"`
    //Weight   []Weight
}

type Homework struct {
    Name  string
    Grade int
}

type Lab struct {
    Name  string
    Grade int
}

函数如下: // 我希望使用类似 category2.result 的写法,而不是 category2.Homework // 这样我就可以对所有类似的结构体使用相同的代码

func display<T Grades> {
    var result []T
    //var cat map[string]any
    content, err := ioutil.ReadFile("grades.json")

    if err != nil {
        log.Fatal(err)
    }
    category2 := Subject{}

    err = json.Unmarshal(content, &category2)
    fmt.Println(category2)
    res := category2
    fmt.Println(res)

    for key, value := range category2.Homework {

        fmt.Println(key, value)
    }
    if err != nil {
        log.Fatal(err)
    }
}

更多关于使用泛型将JSON解析为结构体的Golang实践指南的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

感谢您的回复。我是Go语言的新手,如果工作需要,我会断断续续地进行编程。我的背景是SQL。

更多关于使用泛型将JSON解析为结构体的Golang实践指南的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


谢谢Dean,是的,我同意你的观点,这个问题可以很容易地不使用泛型来解决,我只是对这个错误信息感到好奇。另外,我只是遵循了Hunter的原帖。 非常感谢!!!

为什么在这里使用泛型?从技术上讲,作业和实验都是成绩记录行,对吗?为什么不将它们表示为相同的类型呢?类似这样:

type GradeLine struct {
	Name  string `json:"Name"`
	Grade int    `json:"Grade"`
}

func main() {
	// 将结果声明为一个字符串映射,其值是GradeLine数组。
	var result map[string][]GradeLine
	json.Unmarshal([]byte(myData), &result)
	// 遍历每个键,打印键和该键下的所有成绩。
	for key, value := range result {
		fmt.Println("Grades in", key)
		for _, grade := range value {
			fmt.Println(grade.Name, ":", grade.Grade)
		}
	}
}

假设 myData 是一个JSON字符串。根据你上面发布的JSON,这将打印:

Grades in homework
Homework1 : 45
Homework2 : 45
Grades in lab
Lab1 : 12
Lab2 : 45

我很可能误解了你在这里想要实现的目标。你可以通过此链接在Go Playground中运行这段代码:

Go Playground

你也可以这样表示:

type GradeLine struct {
	Name  string `json:"Name"`
	Grade int    `json:"Grade"`
}
type Grades struct {
	Homework []GradeLine `json:"homework"`
	Lab      []GradeLine `json:"lab"`
}

我仍然对你试图使用泛型来实现的目标感到有点困惑,因为就我所见,被表示的类型是相同的。我认为你可能正在寻找一种更通用/灵活的方式来解析JSON(就像我上面的例子那样)。

非常好的解释!!! 由于我正在学习 Go 中的泛型(虽然我在其他语言中使用过),我发现了一些需要澄清的地方。

  1. 定义 Subject 结构体时,我们有

    type Subject struct {
        Results []T `json:"results"`
    }
    

    这种 []T 语法看起来像是“Java风格”的,并且 Go 不支持(我尝试过),所以需要改为:

    type Subject[T any] struct {
        Results []T `json:"results"`
    }
    

    同时,根据这个定义,也需要对 JSON 文件做一些修改:

    "results" : {
        "homework": [
            {
                ...
            }
        ],
        "lab":[
            {
                ...
            }
        ],
    }
    
  2. displayGrades 函数中,我们发现了 []T 的写法

    func displayGrades(filename string) ([]T, error) ...
    var subject Subject
    

    改为

    func displayGrades[T any](filename string) ([]T, error) ...
    var subject Subject[T]
    

    到目前为止一切正常,但当我运行这个程序,并且我有以下代码时:

    jsonFile := ".../grades.json"
    homeworks, err := displayGrades[Homework](jsonFile)
    if err != nil {
        log.Fatal("Reading Homework : ", err)
    }
    ...
    

    Go 报告了这个错误: 2023/05/07 19:10:09 Reading Homework : json: cannot unmarshal object into Go struct field Subject[main.Homework]results of type []main.Homework exit status 1

    在我看来,我需要编写自己的反序列化器,但不太确定。

    有什么提示吗?

是的,我可以帮助你!

首先,在你的 Subject 结构体中,可以使用泛型类型参数来表示不同的类别(如作业、实验等)。以下是一个示例:

type Subject<T> struct {
    Results []T `json:"results"`
}

在这个版本的 Subject 结构体中,我们用通用的 Results 字段替换了 HomeworkLab 元素,该字段可以承载任何类型的结果。Results 字段使用了 json:"results" 标签进行注解,这指示 Go 的 JSON 包使用 JSON 文件中的 "results" 键来填充此字段。

然后,像之前一样定义 HomeworkLab 结构体:

type Homework struct {
    Name  string `json:"name"`
    Grade int    `json:"grade"`
}

type Lab struct {
    Name  string `json:"name"`
    Grade int    `json:"grade"`
}

我们还为这些结构体的字段添加了 json 标签。这是确保 Go 的 JSON 包能够正确解析 JSON 文件所必需的。

最后,你可以让 displayGrades 函数接受一个泛型类型参数:

func displayGrades<T>(filename string) ([]T, error) {
    content, err := ioutil.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    var subject Subject<T>
    err = json.Unmarshal(content, &subject)
    if err != nil {
        return nil, err
    }
    return subject.Results, nil
}

这个函数接收一个 filename 参数,并返回一个泛型类型 T 的结果切片。它读取文件,将内容解组到一个名为 Subject 的结构体中,并返回 Results 字段。

要调用此方法并显示结果,请使用以下语法:

func main() {
    homework, err := displayGrades<Homework>("grades.json")
    if err != nil {
        log.Fatal(err)
    }
    for _, hw := range homework {
        fmt.Printf("Name: %s, Grade: %d\n", hw.Name, hw.Grade)
    }
    lab, err := displayGrades<Lab>("grades.json")
    if err != nil {
        log.Fatal(err)
    }
    for _, l := range lab {
        fmt.Printf("Name: %s, Grade: %d\n", l.Name, l.Grade)
    }
}

在这个例子中,我们调用了两次 displayGrades,一次使用 Homework 作为类型参数,一次使用 Lab。然后我们遍历结果并打印出每个结果的名称和成绩。

希望这对你有帮助!如果你还有其他问题,请告诉我。

祝你今天愉快。

以下是使用Go泛型解析JSON到结构体的实现方案:

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
)

// 基础成绩接口
type GradeItem interface {
	Homework | Lab
	GetName() string
	GetGrade() int
}

// 泛型成绩结构体
type GenericGrade[T GradeItem] struct {
	Items []T `json:"items"`
}

// 具体类型实现
type Homework struct {
	Name  string `json:"Name"`
	Grade int    `json:"Grade"`
}

func (h Homework) GetName() string {
	return h.Name
}

func (h Homework) GetGrade() int {
	return h.Grade
}

type Lab struct {
	Name  string `json:"Name"`
	Grade int    `json:"Grade"`
}

func (l Lab) GetName() string {
	return l.Name
}

func (l Lab) GetGrade() int {
	return l.Grade
}

// 主结构体
type Subject struct {
	Homework []Homework `json:"homework"`
	Lab      []Lab      `json:"lab"`
}

// 泛型显示函数
func displayGrades[T GradeItem](items []T) {
	for i, item := range items {
		fmt.Printf("%d: %s - %d\n", i, item.GetName(), item.GetGrade())
	}
}

// 泛型解析函数
func parseCategory[T GradeItem](jsonData []byte, fieldName string) ([]T, error) {
	var subject Subject
	if err := json.Unmarshal(jsonData, &subject); err != nil {
		return nil, err
	}

	// 使用类型断言获取对应字段
	switch fieldName {
	case "homework":
		if items, ok := any(subject.Homework).([]T); ok {
			return items, nil
		}
	case "lab":
		if items, ok := any(subject.Lab).([]T); ok {
			return items, nil
		}
	}
	return nil, fmt.Errorf("field not found or type mismatch")
}

// 更简洁的泛型版本
func loadAndDisplay[T GradeItem](filename, fieldName string) error {
	content, err := ioutil.ReadFile(filename)
	if err != nil {
		return err
	}

	items, err := parseCategory[T](content, fieldName)
	if err != nil {
		return err
	}

	displayGrades(items)
	return nil
}

func main() {
	// 示例1:解析homework
	fmt.Println("Homework Grades:")
	if err := loadAndDisplay[Homework]("grades.json", "homework"); err != nil {
		log.Fatal(err)
	}

	// 示例2:解析lab
	fmt.Println("\nLab Grades:")
	if err := loadAndDisplay[Lab]("grades.json", "lab"); err != nil {
		log.Fatal(err)
	}

	// 示例3:直接使用泛型函数
	content, _ := ioutil.ReadFile("grades.json")
	
	homeworks, _ := parseCategory[Homework](content, "homework")
	fmt.Println("\nDirect Homework Access:")
	displayGrades(homeworks)

	labs, _ := parseCategory[Lab](content, "lab")
	fmt.Println("\nDirect Lab Access:")
	displayGrades(labs)
}

更进一步的简化方案,使用单一泛型结构体:

// 统一成绩结构体
type Grade struct {
	Name  string `json:"Name"`
	Grade int    `json:"Grade"`
}

// 泛型容器
type GradeContainer[T any] struct {
	Data []T `json:"data"`
}

// 泛型解析器
func parseJSONField[T any](jsonData []byte, fieldName string) ([]T, error) {
	var raw map[string]json.RawMessage
	if err := json.Unmarshal(jsonData, &raw); err != nil {
		return nil, err
	}

	if fieldData, exists := raw[fieldName]; exists {
		var items []T
		if err := json.Unmarshal(fieldData, &items); err != nil {
			return nil, err
		}
		return items, nil
	}
	return nil, fmt.Errorf("field %s not found", fieldName)
}

// 使用示例
func main() {
	content, _ := ioutil.ReadFile("grades.json")
	
	// 解析为Grade结构体
	homeworks, _ := parseJSONField[Grade](content, "homework")
	fmt.Println("Homeworks:", homeworks)
	
	labs, _ := parseJSONField[Grade](content, "lab")
	fmt.Println("Labs:", labs)
	
	// 计算总成绩的泛型函数
	total := calculateTotal[Grade](homeworks)
	fmt.Printf("Homework Total: %d\n", total)
}

// 泛型计算函数
func calculateTotal[T interface{ GetGrade() int }](items []T) int {
	total := 0
	for _, item := range items {
		total += item.GetGrade()
	}
	return total
}

这个实现通过Go 1.18+的泛型特性,减少了重复代码,同时保持了类型安全。

回到顶部