如果你在使用golang的plugin模式进行代码热更新时会发现protobuf编译生成的xxxxx.pb.go文件中会生成2个init方法。这2个init方法会向"github.com/golang/protobuf/proto"包的全局变量进行注册,原本这种注册在常规的golang程序中是没有任何问题的,因为一个package的init方法只会调用一次。但是在使用plugin模式时,如果xxxxx.pb.go文件是在plugin插件代码中,那么每次新插件加载的时候都会调用对应包的init方法,并且因为需要热更新plugin,所以就会进行多次调用了。因为在"github.com/golang/protobuf/proto"包中多次进行注册同名的类型名会触发panic,因此起初我要解决的问题是如何可以避免这个panic? 我们先看看触发panic的代码片段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// error.pb.go func init() { proto.RegisterEnum("command.ErrorCode", ErrorCode_name, ErrorCode_value) proto.RegisterType((*SCErrorNotify)(nil), "command.SCErrorNotify") } func init() { proto.RegisterFile("error.proto", fileDescriptor_0579b252106fcf4a) } // "github.com/golang/protobuf/proto" properties.go func RegisterEnum(typeName string, unusedNameMap map[int32]string, valueMap map[string]int32) { if _, ok := enumValueMaps[typeName]; ok { panic("proto: duplicate enum registered: " + typeName) } enumValueMaps[typeName] = valueMap } |
每次golang宿主程序加载新的插件hotswap_myplugin.so(该插件包含error.pb.go代码文件)都会调用上面的init方法中的RegisterEnum进行枚举类型的注册触发panic。为了解决这个问题我想到2个思路:
1. 每次新的插件变更error.pb.go中的变量的命名
2. 去除init方法中的注册代码
根据实际应用场景的分析,第一种方法不太适用,因为每次都变更error.pb.go的命名的话意味着插件中的其他代码引用error.pb.go中的类型或者变量的代码都需要全部修改,这对于需要经常热更的代码场景来说显得比较麻烦,一个原因是类似error.pb.go的文件会非常多,每次所有的proto文件都需要修改,另一个原因是所有引用代码也需要全盘修改。这个方式显得非常鸡肋,因为大多数时候如果我只想修改一行代码去修复某个bug,却要花费大量的时间代价去重命名proto定义文件去避免重名注册panic。
那么如此只好使用第二种方式了,第二种方式是希望能够不要执行init方法中的注册方法那么就不会触发重复注册panic了。好家伙,这意思是要从源头解决问题了,no code no bug(panic) !!!! 大多数人看到这里应该都会质疑error.pb.go是由protobuf编译工具protoc自动生成的代码,那么如果去除其中的代码是否会引起其他问题呢? 分析了一波"github.com/golang/protobuf/proto"包的源代码发现这个只是proto这个包想为外部提供一个全局的枚举类型的获取接口返回enumValueMaps = make(map[string]map[string]int32)数据,那么我么的应用场景中并没有使用这个全局的枚举类型数据,因为到这里我们就可以大胆的去除这部分代码了。
当方法确定了剩下的就是执行了,不用多想执行的过程中必定会产生新的问题! 类似error.pb.go的文件有很多,而且可能还会逐渐增加,因此手动修改代码变得很不现实!那只能考虑用脚本或者程序进行修改了,起初的想法是通过正则表达式匹配到func init() {} 代码段,然后进行文本替换。说干就干,然而写一个匹配一个代码段的正则表达式我想正则表达式水平一般的人应该已经开始望而却步或者已经开始疯狂使用google尝试搜索答案了。我选择去请教我一个正则表达式学的不错的同学,有意思的是他给我提供了另外一个思路。 把init方面改了!!! 这想法真是惊为天人啊!!! 匹配一个代码段的正则非常难写,但是匹配init方法名然后替换方法名却变得格外轻松。然而正当我觉得难题迎刃而解并且进行了一次尝试之后发现。不用多想执行的过程中必定又产生新的问题! 每个xxxx.pb.go文件中init方法不止一个,替换后会出现方法重定义的编译问题(golang允许重定义多个init方法)。
100米赛跑跑出去50米告诉我抢跑了要重新开始。又回到最开始的方法了,正则表达式匹配代码段,然后进行文本替换。经过千辛万苦(这里省略了大约16000字吧)终于google出一个匹配javascript代码段的正则表达式,只要稍加改造就可以匹配golang代码了。正则表达式:/func init() \s*([A-z0-9]+)?\s*\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)\s*\{(?:[^}{]+|\{(?:[^}{]+|\{[^}{]*\})*\})*\}/g
利用这个正则表达式我们写一个python脚本来对所有xxxx.pb.pb扫描并且进修修改,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
#!/usr/bin/env python # encoding=utf8 import re,os,io def searchFile(pathname,filename):#参数1要搜索的路径,参数2要搜索的文件名,可以是正则表代式 matchedFile =[] for root,dirs,files in os.walk(pathname): for file in files: if re.match(filename,file): fname = os.path.abspath(os.path.join(root,file)) matchedFile.append(fname) return matchedFile regex = r"func init\s*([A-z0-9]+)?\s*\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)\s*\{(?:[^}{]+|\{(?:[^}{]+|\{[^}{]*\})*\})*\}" def alter(file,old_str,new_str): with io.open(file, "r", encoding="utf-8") as f1,io.open("%s.bak" % file, "w", encoding="utf-8") as f2: srcText = f1.read() #printMatch(srcText) f2.write(re.sub(old_str,new_str,srcText)) os.remove(file) os.rename("%s.bak" % file, file) print("\033[32mfile:{file} remove func init()\033[0m".format(file=file)) def printMatch(test_str): matches = re.finditer(regex, test_str) for matchNum, match in enumerate(matches, start=1): print("Match {matchNum} was found at {start}-{end}: {match}".format(matchNum = matchNum, start = match.start(), end = match.end(), match = match.group())) pbFiles = searchFile(".",r'.+\.pb.go') for file in pbFiles: alter(file, regex, "// remove func init()") // 执行上述脚本代码后发现pb.bo文件替换成功: func (x LobbyCmd) String() string { return proto.EnumName(LobbyCmd_name, int32(x)) } func (LobbyCmd) EnumDescriptor() ([]byte, []int) { return fileDescriptor_333ba92dd49f6abc, []int{0} } // remove func init() // remove func init() var fileDescriptor_333ba92dd49f6abc = []byte{ // 166 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xcb, 0xc9, 0x4f, 0x4a, 0xaa, 0x4c, 0xce, 0x4d, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0xca, 0xc8, 0x2f, 0x29, 0x2e, 0x4f, 0x2c, 0x48, 0xce, 0x4d, 0xd1, 0x5a, 0xce, 0xc8, 0xc5, 0xe1, 0x03, 0x92, 0x76, 0xce, 0x4d, 0x11, 0x12, 0xe5, 0x12, 0x04, 0xb3, 0x5d, 0x4a, 0x73, 0x73, 0x2b, 0x9d, 0xf3, 0x73, 0x73, 0x13, 0xf3, 0x52, 0x04, 0x18, 0x84, 0xc4, 0xb9, 0x84, 0x02, 0x72, 0x12, 0x2b, 0x53, 0x8b, 0xa0, 0x42, 0xc1, 0x25, 0x89, 0x45, 0x25, 0x02, 0x2f, 0xd9, 0x85, 0x84, 0xb9, 0xf8, 0x9c, 0x83, 0xdd, 0x32, 0x8b, 0x8a, 0x4b, 0x42, 0x52, 0x8b, 0x4b, 0x9c, 0x73, 0x53, 0x04, 0x5e, 0x81, 0x05, 0x83, 0x9d, 0x51, 0x04, 0x5f, 0xb3, 0x0b, 0x89, 0x70, 0xf1, 0xa3, 0xaa, 0x34, 0x12, 0x78, 0x03, 0x16, 0x45, 0x55, 0x6a, 0x24, 0xf0, 0x96, 0x5d, 0x48, 0x94, 0x4b, 0x00, 0xc5, 0x3a, 0xd7, 0xbc, 0x14, 0x81, 0x7e, 0xbf, 0x24, 0x36, 0xb0, 0xe3, 0x8d, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xde, 0xa0, 0xb6, 0xac, 0xce, 0x00, 0x00, 0x00, } |
到这里便大功告成了,将pb.go文件嵌入插件hotswap_myplugin.so中,可以随时方便热增减proto协议了。