golang 笔记

总结自己遇过的各种问题

一些优化

循环内捕获变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"sync"
)

func main() {
wg := &sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}

这里不是输出0到1,可能是10个10,因为捕获的i在后面被修改了,改成加个复制就好了

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

import (
"fmt"
"sync"
)

func main() {
wg := &sync.WaitGroup{}
for i := 0; i < 10; i++ {
i := i
wg.Add(1)
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}

make slice 的两种写法

1
2
sliceA := make([]int, 10)
sliceB := make([]int, 0, 10)

第一种是创建一个slice,容量和长度都是10,可以直接 sliceA[index] 下标访问,初始值是该类型的默认值,比如bool是false,int是0,指针是nil
第二种是创建一个slice,容量是10,长度是0,不能直接下标访问,必须要先 sliceB=append(sliceB, 1)后,有数据了才能下标访问
slice进行append时不是协程安全的,多协程同时append需要加锁。

time.Since 优化

Since在1.16后做了一次优化,使用了runtime的纳秒计数器,少了一次调用 now(),优化了一次syscall。调用的 runtimeNano() 返回的是runtime的毫秒计数器,不需要系统调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// runtimeNano returns the current value of the runtime clock in nanoseconds.
//go:linkname runtimeNano runtime.nanotime
func runtimeNano() int64
// Since returns the time elapsed since t.
// It is shorthand for time.Now().Sub(t).
func Since(t Time) Duration {
var now Time
if t.wall&hasMonotonic != 0 {
// Common case optimization: if t has monotonic time, then Sub will use only it.
now = Time{hasMonotonic, runtimeNano() - startNano, nil}
} else {
now = Now()
}
return now.Sub(t)
}

所以既然有内部计时器,我们可以自己整一个优化的耗时计数。

1
2
3
4
5
6
7
8
// runtimeNano returns the current value of the runtime clock in nanoseconds.
//go:linkname runtimeNano runtime.nanotime
func runtimeNano() int64

func UseRuntimeNano() int64 {
nw := runtimeNano()
return runtimeNano() - nw
}

下面是 benchmark 的代码

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
// runtimeNano returns the current value of the runtime clock in nanoseconds.
//go:linkname runtimeNano runtime.nanotime
func runtimeNano() int64

func UseTimeNano() int64 {
nw := time.Now()
return time.Since(nw).Nanoseconds()
}

func UseRuntimeNano() int64 {
nw := runtimeNano()
return runtimeNano() - nw
}

func BenchmarkTimeNano(b *testing.B) {
for i := 0; i < b.N; i++ {
UseTimeNano()
}
}

func BenchmarkRuntimeNano(b *testing.B) {
for i := 0; i < b.N; i++ {
UseRuntimeNano()
}
}

benchmark 结果如下:

1
2
3
cpu: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz
BenchmarkTimeNano-4 84066586 13.18 ns/op 0 B/op 0 allocs/op
BenchmarkRuntimeNano-4 167052464 7.103 ns/op 0 B/op 0 allocs/op

差不多快了一倍

fmt 和 strconv

strconv使用的是查表,甚至还有32位系统的取模优化,性能很不错。
fmt使用的是反射,还有各种边界检验,计算量比strconv高很多

benchmark 代码:

1
2
3
4
5
6
7
8
9
10
11
func BenchmarkUseFmt(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%d", i)
}
}

func BenchmarkUseStrconv(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strconv.Itoa(i)
}
}

benchmark 结果:

1
2
3
cpu: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz
BenchmarkUseFmt-4 10489794 96.20 ns/op 16 B/op 1 allocs/op
BenchmarkUseStrconv-4 37340638 32.84 ns/op 7 B/op 0 allocs/op

快了差不多3倍

字符串拼接 和 strings.Builder 和 内存预申请

很常见的优化了,go的string是不可修改的。append时需要拷贝,少量拼接的话无伤大雅,但是量太大的话,建议使用strings.Builder,如果知道最终字符串会有多大,预先申请会带来更高的性能优化。

benchmark 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func BenchmarkSimpleConcat(b *testing.B) {
res := ""
for i := 0; i < b.N; i++ {
res += strconv.Itoa(i)
}
_ = res
}

func BenchmarkUseStringBuilder(b *testing.B) {
sb := &strings.Builder{}
for i := 0; i < b.N; i++ {
sb.WriteString(strconv.Itoa(i))
}
_ = sb.String()
}
func BenchmarkUseStringBuilderWithPreAlloc(b *testing.B) {
sb := &strings.Builder{}
sb.Grow(b.N * 8) // 假设每个数字都是8位的长度
for i := 0; i < b.N; i++ {
sb.WriteString(strconv.Itoa(i))
}
_ = sb.String()
}

结果:

1
2
3
4
cpu: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz
BenchmarkSimpleConcat-4 92258 23054 ns/op 223894 B/op 1 allocs/op
BenchmarkUseStringBuilder-4 25517612 45.30 ns/op 55 B/op 0 allocs/op
BenchmarkUseStringBuilderWithPreAlloc-4 34258104 35.47 ns/op 15 B/op 0 allocs/op

明显快了非常多。同样的,还有bytes.Buffer,同样的使用方法,可以优化byte的拼接性能。

内存预申请从而实现优化的,标准库里还有 url.PathEscapeurl.QueryEscape,两个都是调用内部方法 escape

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
func escape(s string, mode encoding) string {
spaceCount, hexCount := 0, 0
// 这里在计算空格数量和需要转码的字符数量
for i := 0; i < len(s); i++ {
c := s[i]
if shouldEscape(c, mode) {
if c == ' ' && mode == encodeQueryComponent {
spaceCount++
} else {
hexCount++
}
}
}

if spaceCount == 0 && hexCount == 0 {
return s
}

var buf [64]byte
var t []byte
// 一个需要转码的字符会变成 %XX,也就是需要多两倍的空间存放XX
// 算完直接一次性申请内存,避免重复申请和重复拷贝
required := len(s) + 2*hexCount
if required <= len(buf) {
t = buf[:required]
} else {
t = make([]byte, required)
}

...
}

sync.pool

  1. 使用lock-free的双向队列作为缓存
  2. 使用victim作为待gc缓存,每次gc开头,把victim队列的东西清空,然后把local队列转到victim队列
  3. 在获取时,优先在local,然后victim,然后去别的p的pool拿,最后使用New。local中优先使用无锁的private(pin了,不会有竞争),没有再从shared拿,可以pushHead/popHead,别的p只能popTail
  4. 每个P享有一个pool。G操作pool时,需要锁P

编译相关

race detector

1
2
go run -race
go build -race

所有内联tag

1
go tool link --help

所有gc tag

1
go tool compile --help

减小二进制文件大小

1
-ldflags="-s -w"

方便 dlv 调试(关闭优化、关闭内联)

1
-gcflags '-N -l'

某些技巧

runtime.LockOSThread

runtime.LockOSThread()可以把当前goroutine绑定在物理线程上,但是如果在goroutine绑定后没有UnlockOSThread,就会导致goroutine结束时,这条线程被杀掉