golang编程规范4

16. // 每个对象有多条tableB记录
17. type tableB struct {
18.     _is_nil bool 
19.     city    string
20.     code    int
21.     next *tableB // 指向下一条记录
22. }
23.
24. // 每个对象只有一条tableC记录
25. type tableC struct {
26.     _is_nil bool
27.     id      int
28.     value   int64
29. }


5.3 其它优化建议
【建议5.8】减少[]byte和string之间的转换,尽量使用[]byte来处理字符。
说明:Go里面string类型是immutable类型,而[]byte是切片类型,是可以修改的,所以Go为了保证语法上面没有二义性,在string和[]byte之间进行转换的时候是一个实实在在的值copy,所以我们要尽量的减少不必要的这个转变。


下面这个例子展示了传递slice但是进行了string的转化,
1. func PrefixForBytes(b []byte) string {
2.         return "Hello" + string(b)
3. }


所以我们可以有两种方式,一种是保持全部的都是slice的操作,如下:
1. func PrefixForBytes(b []byte) []byte {
2.     return append([]byte(“Hello”,b…))
3. }


还有一种就是全部是string的操作方式
1. func PrefixForBytes(str string) string {
2.         return "Hello" + str
3. }


推荐阅读:https://blog.golang.org/strings


【建议5.9】make申请slice/map时,根据预估大小来申请合适内存。
说明:map和数组不同,可以根据新增的<key,value>对动态的伸缩,因此它不存在固定长度或者最大限制。


map的空间扩展是一个相对复杂的过程,每次扩容会增加到上次大小的两倍。它的结构体中有一个buckets和oldbuckets,用来实现增量扩容,正常情况下直接使用buckets,oldbuckets为空,如果当前哈希表正在扩容,则oldbuckets不为空,且buckets大小是oldbuckets大小的两倍。对于大的map或者会快速扩张的map,即便只是大概知道容量,也最好先标明。


slice是一个C语言动态数组的实现,在对slice进行append等操作时,可能会造成slice的自动扩容,其扩容规则:
 如果新的大小是当前大小2倍以上,则大小增长为新大小
 否则循环以下操作:如果当前大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长,直到增长的大小超过或者等于新大小


推荐做法:在初始化map时指明map的容量。
1. map := make(map[string]float, 100)


【建议5.10】字符串拼接优先考虑bytes.Buffer。
Golang字符串拼接常见有如下方式:
 fmt.Sprintf 
 strings.Join 
 string + 
 bytes.Buffer
    
fmt.Sprintf会动态解析参数,效率通常是最差的,而string是只读的,string+会导致多次对象分配与值拷贝,而bytes.Buffer在预设大小情况下,通常只会有一次拷贝和分配,不会重复拷贝和复制,故效率是最佳的。


推荐做法:优先使用bytes.Buffer,非关键路径,若考虑简洁,可考虑其它方式,比如错误日志拼接使用fmt.Sprintf,但接口日志使用就不合适。


【建议5.11】避免使用CGO或者减少跨CGO调用次数。
说明:Go可以调用C库函数,但是Go带有垃圾收集器且Go的栈是可变长,跟C实际是不能直接对接的,Go的环境转入C代码执行前,必须为C新创建一个新的调用栈,把栈变量赋值给C调用栈,调用结束后再拷贝回来,这个调用开销非常大,相比直接GO语言调用,单纯的调用开销,可能有2个甚至3个数量级以上,且Go目前还存在版本兼容性问题。


推荐做法:尽量避免使用CGO,无法避免时,要减少跨CGO调用次数。


【建议5.12】避免高并发调用同步系统接口。
说明:编程世界同步场景更普遍,GO提供了轻量级的routine,用同步来模拟异步操作,故在高并发下的,相比线程,同步模拟代价比较小,可以轻易创建数万个并发调用。然而有些API是系统函数,而这些系统函数未提供异步实现,程序中最常见的posix规范的文件读写都是同步,epoll异步可解决网络IO,而对regular file是无法工作的。Go的运行时环境不可能提供超越操作系统API的能力,它依赖于系统syscall文件中暴露的api能力,而1.6版本还是多线程模拟,线程创建切换的代价也非常巨大,开源库中有filepoller来模拟异步其实也基于这两种思路,效率上也会大打折扣。


推荐做法:把诸如写文件这样的同步系统调用,要隔离到可控的routine中,而不是直接高并发调用。


【建议5.13】高并发时避免共享对象互斥。
说明:在Go中,可以轻易创建10000个routine而对系统资源通常就是100M的内存要求,但是并发数多了,在多线程中,当并发冲突在4个到8个线程间时,性能可能就开始出现拐点,急剧下降,这同样适应于Go,Go可以轻易创建routine,但对并发冲突的风险必须要做实现的处理。


推荐做法:routine需要是独立的,无冲突的执行,若routine间有并发冲突,则必须控制可能发生冲突的并发routine个数,避免出现性能恶化拐点。


【建议5.14】长调用链或在函数中避免申明较多较大临时变量。
routine的调用栈默认大小1.7版本已修改为2K,当栈大小不够时,Go运行时环境会做扩栈处理,创建10000个routine占用空间才20M,所以routine非常轻量级,可以创建大量的并发执行逻辑。而线程栈默认大小是1M,当然也可以设置到8K(有些系统可以设置4K),一般不会这么做,因为线程栈大小是固定的,不能随需而变大,不过实际CPU核一般都在100以内,线程数是足够的。


routine是怎么实现可变长栈呢?当栈大小不够时,它会新创建一个栈,通常是2倍大小增长,然后把栈赋值过来,而栈中的指针变量需要搜索出来重新指向新的栈地址,好处不是随便有的,这里就明显有性能开销,而且这个开销不小。


说明:频繁创建的routine,要注意栈生长带来的性能风险,比如栈最终是2M大小,极端情况下就会有数10次扩栈操作,从而让性能急剧下降。所以必须控制调用栈和函数的复杂度,routine就意味着轻量级。


对于比较稳定的routine,也要注意它的栈生长后会导致内存飙升。


【建议5.15】为高并发的轻量级任务处理创建routine池。
说明:Routine是轻量级的,但对于高并发的轻量级任务处理,频繁创建routine来执行,执行效率也是非常低效率的。


推荐做法:高并发的轻量级任务处理,需要使用routine池,避免对调度和GC带来冲击。


【建议5.16】建议版本提供性能/内存监控的功能,并动态开启关闭,但不要长期开启pprof提供的CPU与MEM profile功能。


Go提供了pprof工具包,可以运行时开启CPU与内存的profile信息,便于定位热点函数的性能问题,而MEM的profile可以定位内存分配和泄漏相关问题。开启相关统计,跟GC一样,也会严重干扰性能,因而不要长期开启。


推荐做法:做测试和问题定位时短暂开启,现网运行,可以开启短暂时间收集相关信息,同时要确保能够自动关闭掉,避免长期打开。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值