何为结构体内嵌

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 不是应该以编译速度闻名的吗?

结构体内嵌1

结构体内嵌2

结构体内嵌3

问题原因

几经周折,我们发现如果把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)
    //...
}

改完之后,效果立竿见影:

结构体内嵌1

结构体内嵌2

结构体内嵌3

问题总结

golang 的结构体内嵌是一个能够实现类似继承的实现,在面向对象编程能力偏弱的 golang 中,内嵌应用非常广泛,但如果被内嵌的结构体非常复杂,内嵌次数也没有限制,对程序的编译将会造成相当高的资源浪费,编译时的内存和时间消耗都成倍增加,编译后的程序也非常大。

微信公众号