>

本文列举一些可以更好的提升 Go 程序性能的场景和方法。


#### string 与 []byte 相互转换

在写程序的过程中经常遇到string与[]byte的相互转换,但是这种转换是有代价的,string与[]byte并不共享底层内存空间,所以每次转换都伴随着内存的分配与底层字节的拷贝。
我们可以借助unsafe完成指针类型转换,避开内存分配与复制,从而提升性能。属于magic,尽量不要用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

// struct string{
// uint8 *str;
// int len;
// }
// struct []uint8{
// uint8 *array;
// int len;
// int cap;
// }
// uintptr 是 golang 的内置类型,是能存储指针的整型,uintptr 的底层类型是 int,它和 unsafe.Pointer 可相互转换。
// 但是转换后的 string 与 []byte 共享底层空间,如果修改了[]byte 那么 string 的值也会改变,就违背了string 应该是只读的规范了,可能会造成难以预期的影响。

func str2byte(s string) []byte {
x := (*[2]uintptr)unsafe.Pointer(&s)
h := [3]uintptr{x[0],x[1],x[1]}
return *(*[]byte)(unsafe.Pointer(&h))
}
func byte2str(b []byte) string{
return *(*string)(unsafe.Pointer(&b))
}

##### map使用注意事项 ---
###### (1) 预设容量 map可以动态扩容,所以我们可以不关心map的大小,但是每次动态扩容时需要付出数据拷贝和重新哈希成本,如果我们能预先知道一个map最终的容量,那么最好在初始化时就指定。 bigMap := make(map[int]int,100000)
###### (2) 直接存储小对象值而不是指针 对于小对象,直接将数据交由 map 保存,远比用指针高效。这不但减少了堆内存分配,关键还在于垃圾回收器不会扫描非指针类型 key/value 对象。 //存储值对象
1
2
3
4
5
6
7
8
9
10
11
m := make(map[int]int,1000)
for i := 0 ;i<10000;i++ {
m[i]=i;
}
// 存储指针对象
// 如果value是个小对象,直接存储值会比较好
m := make(map[int]*int,1000)
for i := 0 ;i<10000;i++ {
value := i
m[i]=&value;
}

###### (3) 手动删除没有元素的map map 可以动态扩容,我们可以不断的往map中添加新元素,但是map并不会自动收缩空间,即使一个map中的所有元素都被删除,map依然会保留所有已分配的空间。
1
2
3
4
5
6
7
8
9
10
11
var dict map[int]int = make(map[int]int)
for i := 0 ;i<100000;i++ {
dict[i] = i
}
for key := range dict {
delete(key) //即使删掉所有的元素,dict的容量仍然>=100000
}
// 如果不再使用dict那么手动设置为nil
// dict=nil
//也可以把dict指向一个新创建的小map,原有的map所占用的内存空间会被回收
//dict = make(map[int]int)

#### 正确使用 defer --- defer 是提高可读性和避免资源未释放的非常有用的关键字,是使用 golang 写出可靠、稳定程序的利器。 defer 后面的表达式会被放入一个类似于栈(stack)的结构,在当前方法返回的时候,列表中的表达式会按照后进先出的顺序执行。 defer 本身会有一定的性能损失,但是和它带来的好处相比根本不值得一提,我们需要注意的关键点是defer表达式会在函数返回时被调用,意味着有些资源只能在函数结束时才被释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func f(){
m.lock()
defer m.unlock()
//....业务处理逻辑
//这是很常见的上锁方式,但是m.unlock()只会在函数返回时调用,如果业务处理逻辑耗时很长,那么会一直占用着锁,在高并发情况下严重影响性能。
//解决办法是找到**最小临界区**,在处理完最小临界区后及时释放掉锁。
}

func f() {
m.lock()
//...最小临界区
m.unlock()
//...继续处理
}

#### 字符串拼接 --- 字符串的拼接大概有以下几种方式
  • fmt.Sprintf(“%s%s%d%s%s”, “hello”, “world”, 2018, “come”, “on”) // 这种方式效率最低,但是代码最简单,最优雅
  • 使用 “+” 拼接字符串 “hello” + “world” + strconv.FormatInt(2016,10) + “come” + “on” // 比fmt.Sprintf()高效一些,但是代码很难看
  • 使用 strings.Join() 将参数组装成 []string,然后再调用 strings.join 进行拼接 // 效率最高的一种方式,推荐使用
1
2
strs := []string{"hello","world",strconv.FormatInt(2016,10),"come","on"}
str = strings.Join(strs,"")

为什么使用string.Join效率最好呢,来看下 strings.Join 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func Join(a []string, sep string) string {
//计算最终字符串的长度,根据最终长度创建[]byte,避免拼接过程中内存重新分配
n := len(sep) * (len(a) - 1)
for i := 0; i < len(a); i++ {
n += len(a[i])
}
b := make([]byte, n)
//使用copy函数是最高效的
bp := copy(b, a[0])
for _, s := range a[1:] {
bp += copy(b[bp:], sep)
bp += copy(b[bp:], s)
}
return string(b)
}

但是有时候很难将参数拼接成[]string,这时我们可以使用 byte.Buffer

1
2
3
4
var buffer bytes.Buffer
for i := 0; i < 1000; i++ {
buffer.WriteString("a")
}

#### reflect 对性能的影响 --- refelct 包作为 Go 语言对反射机制提供的支持,其虽然带来了极大的方便,但是同时也有一定的性能损失。性能要求极高的模块中应该注意反射所带来的性能损失。 JSON 是一种常用的数据交换格式,但 Go 的 encoding/json 库依赖于反射来对json进行序列化和反序列化。开源库 ffjson,可以通过使用代码生成的方式来避免反射的使用,相比使用原生库可以提升2~3倍的性能。
#### channel 并不是很高效 --- 使用golang语言编程有一条很重要的思想是 Don't communicate by sharing memory, share memory by communicating. 而 channel 则是不同 goroutine 之间 communicate 的通道。但是channel的底层实现也不是无锁的,往channel中读写数据都是需要加锁的,而且锁的力度还很大。 在一些情况下可以使用利用atomic实现无锁的结构(ring buffer)来替代channel以提高程序的性能。 而在有些情况下使用sync.Mutex、atmoi不仅比使用channel效率更高代码还更简洁明了。Use whichever is most expressive and most simple


来源:
http://shanks.leanote.com/post/Go%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E6%8A%80%E5%B7%A7%E3%80%90%E8%BD%AC%E3%80%91