前言
本文总结了 Go 语言日常开发中经常使用到的代码性能优化小技巧,每个知识点都可以深挖其原理,但本文只给出结论,并附上 benchmark 结果,从测试数据来直观地看性能差异,具体的原理分析可以看每一小节给出的参考文章。
前置知识:Go benchmark 详解
切片预分配容量
如果我们事先知道数据量大小,则可以提前分配切片容量,避免扩容时的元素拷贝。
before
func BenchmarkBefore(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
var s []int // 未指定切片容量
b.StartTimer()
for j := 0; j < 10000; j++ {
s = append(s, j)
}
}
}
after
func BenchmarkAfter(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
s := make([]int, 0, 1000) // 预先指定切片容量
b.StartTimer()
for j := 0; j < 10000; j++ {
s = append(s, j)
}
}
}
benchmark
$ go test -bench="." -benchmem
goos: windows
goarch: amd64
pkg: learnGolang
BenchmarkBefore-4 16641 76656 ns/op 386304 B/op 20 allocs/op
BenchmarkAfter-4 21256 56961 ns/op 334465 B/op 7 allocs/op
PASS
ok learnGolang 6.305s
reference
大量字符串拼接
Go 语言的字符串为不可变类型,使用 +
拼接字符串会创建一个新的对象,在大量字符串拼接的场景下,使用 strings.Builder
可以显著提升性能。
before
func Before(str string, n int) string {
var s string
for i := 0; i < n; i++ {
s += str // 直接使用 + 来拼接
}
return s
}
after
func After(str string, n int) string {
var buf strings.Builder
for i := 0; i < n; i++ {
buf.WriteString(str) // 使用 strings.Builder 优化拼接
}
return buf.String()
}
benchmark
var (
str = "hello,世界"
n = 1000
)
func BenchmarkBefore(b *testing.B) {
for i := 0; i < b.N; i++ {
Before(str, n)
}
}
func BenchmarkAfter(b *testing.B) {
for i := 0; i < b.N; i++ {
After(str, n)
}
}
$ go test -bench="." -benchmem
goos: windows
goarch: amd64
pkg: learnGolang
BenchmarkBefore-4 756 1391783 ns/op 6366122 B/op 999 allocs/op
BenchmarkAfter-4 82411 15351 ns/op 53232 B/op 15 allocs/op
PASS
ok learnGolang 2.705s
reference
正则表达式预编译
如果正则表达式是确定不变的,则可以将其定义为全局变量并预先编译,避免每次使用时现编译。
before
func ParseIPv4Before(ip string) bool {
re := regexp.MustCompile(`(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`)
return re.MatchString(ip)
}
after
var IPv4Regex = regexp.MustCompile(`(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`)
func ParseIPv4After(ip string) bool {
return IPv4Regex.MatchString(ip)
}
benchmark
func BenchmarkBefore(b *testing.B) {
for i := 0; i < b.N; i++ {
ParseIPv4Before("192.168.2.255")
}
}
func BenchmarkAfter(b *testing.B) {
for i := 0; i < b.N; i++ {
ParseIPv4After("192.168.2.255")
}
}
$ go test -bench="." -benchmem
goos: windows
goarch: amd64
pkg: learnGolang
BenchmarkBefore-4 80751 12892 ns/op 11046 B/op 83 allocs/op
BenchmarkAfter-4 3840087 371 ns/op 0 B/op 0 allocs/op
PASS
ok learnGolang 2.992s