oldme 博客

鸿雁在云鱼在水,惆怅此情难寄

GoFrame 规范路由巧妙设计数据返回

oldme create: 2023-07-30

GoFrame 是一款模块化、高性能、企业级的Go基础开发框架。用起来嘎嘎爽,关键作者也很帅,本文就自己的实际使用经验来探讨一个问题:GoFrame 规范路由数据该怎么样优雅的返回。

痛点的来源

我们来设想一个场景:一篇文章的查阅。一般来说,给用户看的文章一般要隐去某些字段,例如:文章是否发布、文章排序等,而给管理员看的文章则需要全部返回。在 GoFrame 中,我们每一个接口返回的数据是用结构体定义的。但文章标题、作者、简介、内容等这些都是共用的字段,这就势必带来一个问题:重复定义字段:

type ArticleAdmin struct {
	Id          uint        `json:"id"          description:""`
	Title       string      `json:"title"       description:"标题"`
	Author      string      `json:"author"      description:"作者"`
	Description string      `json:"description" description:"简介"`
	Content     string      `json:"content"     description:"内容"`
	Onshow      uint        `json:"onshow"      description:"是否显示/发布"`
	Order       int         `json:"order"       description:"排序,越大越靠前"`
}

type ArticleUser struct {
	Id          uint        `json:"id"          description:""`
	Title       string      `json:"title"       description:"标题"`
	Author      string      `json:"author"      description:"作者"`
	Description string      `json:"description" description:"简介"`
	Content     string      `json:"content"     description:"内容"`
}

这很不 amazing。能不能基于框架生成的 entity 数据模型,在此基础上进行一些"裁剪"操作,当 api 返回数据时屏蔽掉我们不想要的字段呢?请看以下的一个例子:

type ArticleAdmin struct {
	Id          uint   `json:"id"          description:""`
	Title       string `json:"title"       description:"标题"`
	Author      string `json:"author"      description:"作者"`
	Description string `json:"description" description:"简介"`
	Content     string `json:"content"     description:"内容"`
	Onshow      uint   `json:"onshow"      description:"是否显示/发布"`
	Order       int    `json:"order"       description:"排序,越大越靠前"`
}

type ArticleUser struct {
	ArticleAdmin
	Onshow    bool `json:"onshow,omitempty"`
	Order     bool `json:"order,omitempty"`
	DeletedAt bool `json:"deletedAt,omitempty"`
}

func TestStructJson(t *testing.T) {
	articleUser := ArticleUser{
		ArticleAdmin: ArticleAdmin{
			Id:          1,
			Title:       "title",
			Author:      "oldme",
			Description: "struct-json",
			Content:     "This is one about struct and json article.",
		},
	}
	j, _ := json.MarshalIndent(articleUser, "", "\t")
	fmt.Println(string(j))
}

// 结果
{
	"id": 1,
	"title": "title",
	"author": "oldme",
	"description": "struct-json",
	"content": "This is one about struct and json article."
}

这个代码示例中,关键点在于 ArticleUser 结构体,它包含了匿名结构体:ArticleAdmin 和三个带有 omitempty 标签的字段,omitempty 的意思是当字段的值为零值时,不会打印到 json 中。利用此点,我们就可以解决重复定义大量字段的问题。

amazing 的设计

依据 Gf 框架的分层设计思想,service 需要设计灵活,方便解耦,我们统一返回 entity 数据模型:

func Show(ctx context.Context, id model.Id) (info *entity.Article, err error)

设计 model 层数据结构:

type ArticleInput struct {
	...
}

type ArticleUser struct {
	// 直接使用框架生成的数据结构
	entity.Article
	Onshow    bool `json:"onshow,omitempty"`
	Order     bool `json:"order,omitempty"`
	DeletedAt bool `json:"deletedAt,omitempty"`
}

然后在 controller 层中使用 ArticleUser 结构体处理接口数据返回。具体的使用姿势,可以参考现成的例子:https://github.com/oldme-git/oldme-api

进阶

下文的内容不影响你 amazing ,涉及到了其他的知识,如果你感兴趣可以继续往下看。

omitempty 的坑

先来看一个例子:

type ArticleUser struct {
	ArticleAdmin struct{} `json:"articleAdmin,omitempty"`
	Order        bool     `json:"order"`
}

func TestArticleOmitAndStruct(t *testing.T) {
	aricle := ArticleUser{
		Order: false,
	}
	res, _ := json.MarshalIndent(aricle, "", "\t")
	fmt.Println(string(res))
}

// 结果
{
	"articleAdmin": {},
	"order": false
}

我们虽然给 ArticleAdmin 设置了 omitempty,但是 ArticleAdmin 并没有被忽略,这是因为 omitempty 只有字段是零值时才会被忽略,而结构体是一个复合结构,其零值取决内部结构,Go 不能准确知道,所以就会出现结构体设置 omitempty 失效的情况。所以如果需要忽略字段,最好使用有确切零值的数据类型,如 int、指针、bool、string等,那么,使用哪个最好呢?答案是占据字节越小的越好,如bool、int8,uint8这种占据1个字节的。这里在和群友探讨时,有一种特别的数据类型有争议:空结构体指针。有的人认为空结构体不占据内存空间,所以使用空结构体指针应该会更好,答案真的如此吗,让我们接着往下看。

内存对齐的影响

关于内存对齐,可以查看此文:https://oldme.net/article/22

看一个性能测试的例子:

type User struct {
	Pwd int8  `json:"pwd"`
	Age int8  `json:"age"`
	Sex int64 `json:"sex"`
}

type UserSafeStruct struct {
	User
	Pwd *struct{} `json:"pwd,omitempty"`
}

type UserSafeBool struct {
	User
	Pwd bool `json:"pwd,omitempty"`
}

var JsonCount = 5000000

func BenchmarkJsonStruct(t *testing.B) {
	for i := 0; i < JsonCount; i++ {
		user := UserSafeStruct{
			User: User{
				Pwd: 123,
				Age: 18,
			},
		}
		json.MarshalIndent(user, "", "\t")
	}
}

func BenchmarkJsonBool(t *testing.B) {
	for i := 0; i < JsonCount; i++ {
		user := UserSafeBool{
			User: User{
				Pwd: 123,
				Age: 18,
			},
		}
		json.MarshalIndent(user, "", "\t")
	}
}

// 测试结果
BenchmarkJsonStruct-24                 1        1340439100 ns/op        561034104 B/op  15001683 allocs/op
BenchmarkJsonBool-24                   1        1258765800 ns/op        561025376 B/op  15001637 allocs/op

在这个例子中,使用空结构体指针和布尔类型在内存使用上区别不大,但是空结构体指针更消耗时间,我们接着修改一下结构体,把 User 结构体的 sex 改成占用1个字节的字段,再重新测试:

type User struct {
	Pwd int8  `json:"pwd"`
	Age int8  `json:"age"`
	Sex bool  `json:"sex"`
}
...

// 测试结果
BenchmarkJsonStruct-24                 1        1403241000 ns/op        520956472 B/op  15001498 allocs/op
BenchmarkJsonBool-24                   1        1312572400 ns/op        460845712 B/op  15001301 allocs/op

可以看到,第二次测试,内存使用上出现了明显的差异。让我们来分析一下这种情况,第一次测试时,UserSafeStruct 和 UserSafeBool,其结构体占用大小都是 24,所以结果差距不大。第二次测试时,修改 sex 为占据1字节大小的 bool,受到内存对齐的影响,UserSafeStruct 占据 16 个字节,UserSafeBool 占据 4 个字节,运行 5000000 次,理论上应该省了 12 * 5000000 个字节,符合我们的测试结果:520,956,472 - 460,845,712 = 60,110,760。

结论:空结构体虽然不占据内存,但是空结构体指针在结构体上还是占据了 8 个字节,所以使用占据字节更小的 bool 更容易内存对齐,更容易获取更高的性能。

评论

欢迎您的回复 取消回复

您的邮箱不会显示出来,*必填

本文目录