来自「Go语言圣经」:
- func shadow() (err error) {
- if y, err := check2(x); err != nil { // y和if语句中err被创建
- return // if语句中的err覆盖外面的err,所以错误的返回nil!
- } else {
- fmt.Println(y)
- }
- return
- }
-
十分推荐:Go 语言高性能编程(link:https://geektutu.com/post/high-performance-go.html),聚焦性能优化
频繁操作字符串时,不推荐使用 a += b 或者 fmt.Sprintf("%s%s", a, b)的形式,因为golang中字符串类型的变量是不可变的,所以每次go都会新创建一个临时变量。推荐的做法是:
- var b bytes.Buffer
- ...
- for condition {
- b.WriteString(str) // 将字符串str写入缓存buffer
- }
- return b.String()
-
还可以用 strings.Builder 或者 []byte:
- var builder strings.Builder
- for condition {
- builder.WriteString(str)
- }
- return builder.String()
-
使用 []byte:
- buf := make([]byte, 0)
- for condition {
- buf = append(buf, str...)
- }
- return string(buf)
-
strings.Builder 和 bytes.Buffer 底层都是 []byte 数组,但 strings.Builder 性能比 bytes.Buffer 略快约 10% 。一个比较重要的区别在于,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder 直接将底层的 []byte 转换成了字符串类型返回了回来。
- // To build strings more efficiently, see the strings.Builder type.
- func (b *Buffer) String() string {
- if b == nil {
- // Special case, useful in debugging.
- return "<nil>"
- }
- return string(b.buf[b.off:])
- }
-
- // String returns the accumulated string.
- func (b *Builder) String() string {
- return *(*string)(unsafe.Pointer(&b.buf))
- }
-
bytes.Buffer 的注释中还特意提到了:To build strings more efficiently, see the strings.Builder type.
使用slice2 := slice1[m:n]的方式在已有切片的基础上创建新的切片,不会创建新的底层数组。因此如果原切片由大量元素组成,即使新创建的切片只用到了其中一小部分元素,原切片的整个底层数组占用的内存还是会一直得不到释放。因此推荐的做法是使用copy():
- result := make([]int, 2)
- copy(result, origin[len(origin)-2:])
- return result
-
一般来说,有以下三种方式遍历slice:
- // 1. 使用下标
- for i := 0; i < len(slice1); i++ {
- item := slice1[i]
- }
-
- // 2. 使用range遍历下标访问
- for i := range slice1 {
- item := slice1[i]
- }
-
- // 3. 使用range同时遍历下标和元素
- for i, item := range slice1{
- // ...
- }
-
上面几种方式中,方法#1和#2的性能是差不多的,而方法#3的差别就在于每次遍历元素时,都需要给当前遍历的元素创建一个拷贝,因此如果slice中的元素占用内存较高(比如一个元素很多的struct,而且还不是指针形式),那么方法#3的性能就会显著不如前两者。
对于集合来说,只需要 map 的键,而不需要值。即使是将值设置为 bool 类型,也会多占据 1 个字节。因此,可以将值定义为空结构体struct{}{},节省内存空间:
- strSet := make(map[string]struct{})
-
空结构体不占用任何的内存空间,我们可以通过下面的代码查看它占用的字节数:
- fmt.Println(unsafe.Sizeof(struct{}{}))
-
Go 语言标准库 sync 提供了 2 种锁,互斥锁(sync.Mutex)和读写锁(sync.RWMutex)。读写锁的特点是在没有加写锁的情况下,多个读锁是不会阻塞的。因此,在读多写少的场景下,读写锁的性能会优于互斥锁的性能。
sync.Pool可以保存和复用临时对象,好处是在高并发场景下,如果一个临时对象需要不断的被创建和使用,那么使用sync.Pool可以避免每次进行内存分配和垃圾回收,从而提升程序的性能。sync.Pool 用于存储那些被分配了但是没有被使用,而未来可能会使用的值。这样就可以不用再次经过内存分配,可直接复用已有对象。
sync.Pool 是并发安全的,同时大小也是可伸缩的,高负载时会动态扩容,存放在池中的对象如果不活跃了会被自动清理。
使用 sync.Pool 只需实现New函数即可:
- type Student struct{
- name string
- }
-
- var studentPool = sync.Pool{
- New: func() interface{} {
- return new(Student)
- },
- }
-
获取 sync.Pool 中存储的对象:
- // 当调用 Get 方法时,如果pool里缓存了对象,就直接返回缓存的对象。如果没有,则调用 New 函数创建一个新的对象。
- stu := studentPool.Get().(*Student) // 返回为interface类型,所以需要做类型转换
- // do something
- // 对象使用完毕后,放回pool,一般会先将对象清理为空
- studentPool.Put(stu)
-
- var users map[string]string{
- "name": "password",
- }
- var mu sync.Mutex
-
- func CheckUser(name, password string) bool {
- mu.Lock()
- defer mu.Unlock()
- // access map
- // do something
- return
- }
-
优化版本:
- func CheckUser(name, password string) bool {
- func() {
- mu.Lock()
- defer mu.Unlock()
- // access map
- }()
- // do something
- return
- }
-
- atomic.Value
- atomic.Load // load, store, swap ...
-
Go 程序会在 2 个地方为变量分配内存,一个是全局的堆(heap)空间用来动态分配内存,另一个是每个 goroutine 的栈(stack)空间。如何判断一个变量是分配在栈上还是堆上?一般来说,我们在函数内部申请一个变量,这个变量会被分配到栈上,函数结束时自动回收;但是,如果这个变量被外部引用了,比如函数返回了该变量的指针,那么这个变量就不能随着函数的结束而回收,因此会被分配到堆上。
对于Go语言,开发者在定义变量时不需要关心变量是分配到堆上还是栈上,这一判断是由编译器完成的,叫做逃逸分析(escape analysis)。
在堆上分配和在栈上分配的一个很大区别就是性能开销。在栈上分配内存的开销很小,仅需要两个CPU指令PUSH和POP就能完成分配和回收。而在堆上分配内存则需要Go的垃圾回收进行清理,带来很大的额外开销。因此,减少在堆上分配的内存,可以减少GC的压力,提升运行速度。
我们可以在编译时使用go build -gcflags '-m'命令来观察变量的逃逸情况。除此之外,我们也可以关注一些容易发生逃逸的情况: