何为结构体内嵌
golang 中允许定义不带名称的结构体成员,只需要指定类型即可,这种结构体成员称作匿名成员。将一个命名结构体当做另一个结构体类型的匿名成员使用,即为结构体内嵌。如下示例中,Circle 和 Wheel都拥有一个匿名成员,Point 被内嵌到 Circle 中,Circle 被内嵌到 Wheel 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
func main() {
var w Wheel
w.X = 8 //等价于w.Circle.Point.X = 8
w.Y = 8 //等价于w.Circle.Point.Y = 8
w.Radius = 5 //等价于w.Circle.Radius = 5
w.Spokes = 20
}
|
问题现象
我们的后端项目在版本迭代过程中,内存消耗越来越大,编译时间越来越长,编译后的文件也越来越大。在某个版本时,编译服务器的内存已经耗光,编译的同时,整个服务器卡死,任何任务都被中断,我们甚至已经觉得服务器该换了。这还是我们熟悉的 golang 吗?golang 不是应该以编译速度闻名的吗?
问题原因
几经周折,我们发现如果把Redis相关操作的代码注释掉,编译速度立马恢复,最后定位在这段代码之中:
1
2
3
4
5
6
7
8
9
10
|
package redisop
type InBaseRedisCommand struct {
}
func (this InBaseRedisCommand) GetUser(strKeySuffix string, ptRedis *redis.Client) (*in_base.User, error) {
//...
}
//...
|
这段代码是由 protobuf 文件通过工具自动生成的,目的是把Redis的读写操作封装起来,供业务开发使用。因为是自动生成的代码,我们没有刻意控制其代码量,目前 InBaseRedisCommand 结构体的代码量接近8万行,方法数达到2000多个。相信很多项目中,这个量级的结构体并非没有可能,而且极有可能很常见。InBaseRedisCommand 结构体本身并没有问题,问题在于它被内嵌太多了。
1
2
3
4
5
6
7
8
9
|
type SS2RS_LoginCommand struct {
redisop.InBaseRedisCommand
}
func (this SS2RS_LoginCommand) Process(gRecord *record.Record, msg *zebra.Message) bool {
//...
user, uErr := this.GetUser(strUserId, gRecord.RedisClient)
//...
}
|
这是一段处理通信协议的逻辑,为了业务开发方便,我们把 InBaseRedisCommand 结构体内嵌到各类 Command 结构体中,这样的内嵌大概有300多处。
解决方法
知道问题了,修改很简单,将所有内嵌删除了,内部调用改成外部调用:
1
2
3
4
5
6
7
8
9
10
|
type SS2RS_LoginCommand struct {
//redisop.InBaseRedisCommand
}
func (this SS2RS_LoginCommand) Process(gRecord *record.Record, msg *zebra.Message) bool {
//...
//user, uErr := this.GetUser(strUserId, gRecord.RedisClient)
user, uErr := redisop.GetM().GetUser(strUserId, gRecord.RedisClient)
//...
}
|
改完之后,效果立竿见影:
问题总结
golang 的结构体内嵌是一个能够实现类似继承的实现,在面向对象编程能力偏弱的 golang 中,内嵌应用非常广泛,但如果被内嵌的结构体非常复杂,内嵌次数也没有限制,对程序的编译将会造成相当高的资源浪费,编译时的内存和时间消耗都成倍增加,编译后的程序也非常大。