GoFrame 规范路由巧妙设计数据返回
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 更容易内存对齐,更容易获取更高的性能。
本文目录