golang性能提升方法论
目前使用golang 进行项目开发已经有一年多了,针对很多代码优化有一些心得体会以文字的形式输出做个总结。
接口完整性检查
另外,我们可以看到,Go 语言的编译器并没有严格检查一个对象是否实现了某接口所有的接口方法,如下面这个示例:
type Shape interface {
Sides() int
Area() int
}
type Square struct {
len int
}
func (s* Square) Sides() int {
return 4
}
func main() {
s := Square{len: 5}
fmt.Printf("%d\n",s.Sides())
}
可以看到,Square 并没有实现 Shape 接口的所有方法,程序虽然可以跑通,但是这样的编程方式并不严谨,如果我们需要强制实现接口的所有方法,那该怎么办呢?在 Go 语言编程圈里,有一个比较标准的做法:
var _ Shape = (*Square)(nil)
声明一个 _ 变量(没人用)会把一个 nil 的空指针从 Square 转成 Shape,这样,如果没有实现完相关的接口方法,编译器就会报错。
时间
时间有时区、格式、精度等问题,其复杂度不是一般人能处理的。所以,一定要重用已有的时间处理,而不是自己干。在 Go 语言中,你一定要使用 time.Time 和 time.Duration 这两个类型。
- 在命令行上,flag 通过 time.ParseDuration 支持了 time.Duration。
- JSON 中的 encoding/json 中也可以把time.Time 编码成 RFC 3339 的格式。
- 数据库使用的 database/sql 也支持把 DATATIME 或 TIMESTAMP 类型转成 time.Time。
- YAML 也可以使用 gopkg.in/yaml.v2 支持 time.Time 、time.Duration 和 RFC 3339 格式。
最后,如果你要做全球化跨时区的应用,一定要把所有服务器和时间全部使用 UTC 时间。性能提示
深度比较
当我们复制一个对象时,这个对象可以是内建数据类型、数组、结构体、Map……在复制结构体的时候,如果我们需要比较两个结构体中的数据是否相同,就要使用深度比较,而不只是简单地做浅度比较。这里需要使用到反射 reflect.DeepEqual() ,下面是几个示例:
import (
"fmt"
"reflect"
)
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2))
//prints: v1 == v2: true
m1 := map[string]string{"one": "a","two": "b"}
m2 := map[string]string{"two": "b", "one": "a"}
fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2))
//prints: m1 == m2: true
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2))
//prints: s1 == s2: true
}
性能提示:
Go 语言是一个高性能的语言,但并不是说这样我们就不用关心性能了,我们还是需要关心的。下面我给你提供一份在编程方面和性能相关的提示。
- 如果需要把数字转换成字符串,使用 strconv.Itoa() 比 fmt.Sprintf() 要快一倍左右。
- 尽可能避免把String转成[]Byte ,这个转换会导致性能下降。
- 如果在 for-loop 里对某个 Slice 使用 append(),请先把 Slice 的容量扩充到位,这样可以避免内存重新分配以及系统自动按 2 的 N 次方幂进行扩展但又用不到的情况,从而避免浪费内存。
- 使用bytes.Buffer 或是strings.Builder 来拼接字符串,性能会比使用 + 或 +=高三到四个数量级。
package main
import (
"bytes"
"strings"
"testing"
)
var strLen int = 1000
func BenchmarkConcatString(b *testing.B) {
var str string
i := 0
b.ResetTimer()
for n := 0; n < b.N; n++ {
str += "x"
i++
if i >= strLen {
i = 0
str = ""
}
}
}
func BenchmarkConcatBuffer(b *testing.B) {
var buffer bytes.Buffer
i := 0
b.ResetTimer()
for n := 0; n < b.N; n++ {
buffer.WriteString("x")
i++
if i >= strLen {
i = 0
buffer = bytes.Buffer{}
}
}
}
func BenchmarkConcatBuilder(b *testing.B) {
var builder strings.Builder
i := 0
b.ResetTimer()
for n := 0; n < b.N; n++ {
builder.WriteString("x")
i++
if i >= strLen {
i = 0
builder = strings.Builder{}
}
}
}
LWTdeMBP:benchmarks lwt$ go test -bench=. -benchmem
goos: darwin
goarch: amd64
BenchmarkConcatString-8 8688772 126 ns/op 530 B/op 0 allocs/op
BenchmarkConcatBuffer-8 163425741 7.25 ns/op 2 B/op 0 allocs/op
BenchmarkConcatBuilder-8 465009902 2.64 ns/op 2 B/op 0 allocs/op
PASS
ok _/Users/lwt/benchmarks 4.677s
LWTdeMBP:benchmarks lwt$
- 尽可能使用并发的 goroutine,然后使用 sync.WaitGroup 来同步分片操作。
- 避免在热代码中进行内存分配,这样会导致 gc 很忙。尽可能使用 sync.Pool 来重用对象。
- 使用 lock-free 的操作,避免使用 mutex,尽可能使用 sync/Atomic包。
- 使用 I/O 缓冲,I/O 是个非常非常慢的操作,使用 bufio.NewWrite() 和 bufio.NewReader() 可以带来更高的性能。
- 对于在 for-loop 里的固定的正则表达式,一定要使用 regexp.Compile() 编译正则表达式。性能会提升两个数量级。如果你需要更高性能的协议,
- 就要考虑使用 protobuf 或 msgp 而不是 JSON,因为 JSON 的序列化和反序列化里使用了反射。
- 你在使用 Map 的时候,使用整型的 key 会比字符串的要快,因为整型比较比字符串比较要快。