Junedayday Blog

六月天天的个人博客

0%

Go编程模式 - 2.基础编码下

注:本文的灵感来源于GOPHER 2020年大会陈皓的分享,原PPT的链接可能并不方便获取,所以我下载了一份PDF到git仓,方便大家阅读。我将结合自己的实际项目经历,与大家一起细品这份文档。

目录

注:切勿过早优化!

Time

这部分的内容实战项目中用得不多,大家记住耗子叔总结出来的一个原则即可:

尽量用time.Timetime.Duration,如果必须用string,尽量用time.RFC3339

然而现实情况并没有那么理想,实际项目中用得最频繁,还是自定义的2006-01-02 15:04:05

1
time.Now().Format("2006-01-02 15:04:05")

Performance1

Itoa性能高于Sprint

主要性能差异是由于Sprint针对的是复杂的字符串拼接,底层有个buffer,会在它的基础上进行一些字符串的拼接;

Itoa直接通过一些位操作组合出字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 170 ns/op
func Benchmark_Sprint(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprint(rand.Int())
}
}

// 81.9 ns/op
func Benchmark_Itoa(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strconv.Itoa(rand.Int())
}
}

减少string到byte的转换

主要了解go的string[]byte的转换还是比较耗性能的,但大部分情况下无法避免这种转换。

我们注意一种场景即可:从[]byte转换为string,再转换为[]byte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 43.9 ns/op
func Benchmark_String2Bytes(b *testing.B) {
data := "Hello world"
w := ioutil.Discard
for i := 0; i < b.N; i++ {
w.Write([]byte(data))
}
}

// 3.06 ns/op
func Benchmark_Bytes(b *testing.B) {
data := []byte("Hello world")
w := ioutil.Discard
for i := 0; i < b.N; i++ {
w.Write(data)
}
}

切片能声明cap的,尽量初始化时声明

了解slice的扩容机制就能很容易地理解。切片越长,影响越大。

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

// 4494 ns/op
func Benchmark_NoCap(b *testing.B) {
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
}

// 2086 ns/op
func Benchmark_Cap(b *testing.B) {
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
}

避免用string做大量字符串的拼接

频繁拼接字符串的场景并不多,了解即可。

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
var strLen = 10000

// 0.0107 ns/op
func Benchmark_StringAdd(b *testing.B) {
var str string
for n := 0; n < strLen; n++ {
str += "x"
}
}

// 0.000154 ns/op
func Benchmark_StringBuilder(b *testing.B) {
var builder strings.Builder
for n := 0; n < strLen; n++ {
builder.WriteString("x")
}
}

// 0.000118 ns/op
func Benchmark_BytesBuffer(b *testing.B) {
var buffer bytes.Buffer
for n := 0; n < strLen; n++ {
buffer.WriteString("x")
}
}

Performance2

并行操作用sync.WaitGroup控制

热点内存分配用sync.Pool

注意一下,一定要是热点,千万不要 过早优化

倾向于使用lock-free的atomic包

除了常用的CAS操作,还有atomic.ValueStoreLoad操作,这里简单地放个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
v := atomic.Value{}
type demo struct {
a int
b string
}

v.Store(&demo{
a: 1,
b: "hello",
})

data, ok := v.Load().(*demo)
fmt.Println(data, ok)
// &{1 hello} true
}

复杂场景下,还是建议用mutex

对磁盘的大量读写用bufio包

bufio.NewReader()bufio.NewWriter()

对正则表达式不要重复compile

1
2
3
4
5
6
7
// 如果匹配的格式不会变化,全局只初始化一次即可
var compiled = regexp.MustCompile(`^[a-z]+[0-9]+$`)

func main() {
fmt.Println(compiled.MatchString("test123"))
fmt.Println(compiled.MatchString("test1234"))
}

用protobuf替换json

go项目内部通信尽量用protobuf,但如果是对外提供api,比如web前端,json格式更方便。

map的key尽量用int来代替string

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
var size = 1000000

// 0.0442 ns/op
func Benchmark_MapInt(b *testing.B) {
var m = make(map[int]struct{})
for i := 0; i < size; i++ {
m[i] = struct{}{}
}
b.ResetTimer()
for n := 0; n < size; n++ {
_, _ = m[n]
}
}

// 0.180 ns/op
func Benchmark_MapString(b *testing.B) {
var m = make(map[string]struct{})
for i := 0; i < size; i++ {
m[strconv.Itoa(i)] = struct{}{}
}
b.ResetTimer()
for n := 0; n < size; n++ {
_, _ = m[strconv.Itoa(n)]
}
}

示例中strconv.Itoa函数对性能多少有点影响,但可以看到stringint的差距是在数量级的。

Further

PPT中给出了8个扩展阅读,大家根据情况自行阅读。

如果说你的时间只够读一个材料的话,我推荐大家反复品读一下Effective Go

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili:https://space.bilibili.com/293775192

公众号:golangcoding