高质量编程与代码规范 | 青训营笔记

114 阅读4分钟

高质量编程及代码规范

编程原则

  • 简单性

    消除多余的复杂性,代码逻辑简单清晰

  • 可读性

    编写可维护代码的第一步是代码可读

  • 生产力

    团队的工作效率更高

编码规范

代码格式:使用gofmt自动格式化代码

注释:注释应该做到解释代码的作用,解释代码如何做,解释代码实现的原因,解释代码什么情况下会出错

命名规范:命名应简洁明了,当变量名的详略不影响对代码的理解时,代码应从简,而命名的简化不应牺牲对代码的理解难度。函数和变量名缩略词应全大写,当出现在前段且不需导出时,可以全部小写。包名只有小写字母组成,简短并包含一定的上下文信息

控制流程:避免嵌套,保持逻辑的清晰

性能优化

测试性能工具Benchmark

借助benchmark工具测试基准性能

对于完成的fib.go文件,建立fib_test.go文件。譬如测试函数fib的性能,需要在fib_test.go文件中编写函数测试代码,函数命名规则为Benchmark<待测试函数名,首字母大写>

// fib.go
func fib(n int) int {
	if n == 1 {
		return 1
	}
	if n == 0 {
		return 1
	}
	return fib(n-1) + fib(n-2)
}

// fib_test.go
import (
	"testing"
)

func BenchmarkFib(b *testing.B) {
	for i := 1; i <= b.N; i++ {
		fib(5)
	}
}

在终端输入如下命令

go test -bench=. -benchem 

测试结果

goos: darwin
goarch: arm64
pkg: awesomeProject
BenchmarkFib-8          49861150                24.86 ns/op            0 B/op          0 allocs/op
PASS
ok      awesomeProject  1.712s

第四行共有五组数据,第一组表示测试函数名,后接CPU核数,第二组表示一共执行几次,即b.N的值,第三组为每次执行花费时间,第四组表示每次执行申请多大内存,第五组表示每次执行申请几次内存

性能优化 Slice

Slice预分配内存,尽量在使用make()初始化时给定容量信息

func NoPreAlloc(size int) {
	data := make([]int, 0)
	for i := 0; i < size; i++ {
		data = append(data, i)
	}
}

func PreAlloc(size int) {
	data := make([]int, 0, size)
	for i := 0; i < size; i++ {
		data = append(data, i)
	}
}
BenchmarkNoPreAlloc-8            8790717               134.9 ns/op           504 B/op          6 allocs/op
BenchmarkPreAlloc-8             39776478                29.66 ns/op          160 B/op          1 allocs/op

可见预先给定切片容量,在进行插入元素时,执行效率更高,需求分配的内存次数更少,程序运行速率更快

Slice 原理

image.png Slice的逻辑空间和物理空间都类似于顺序表

type Slice struct{
    array unsafe.Pointer
    len int
    cap int
}

cap表示Slice的容量,即不扩容情况下可使用的最大空间。当需要扩容时,会重新申请一段足够的内存空间,将原有元素复制到新的空间中,此操作会改变指向那一内存空间的指针

性能优化 map

Map预分配内存,尽量在使用make()初始化时给定容量信息

func PreAllocMap(size int) {
	data := make(map[int]int, size)
	for i := 0; i < size; i++ {
		data[i] = 1
	}
}

func NoPreAllocMap(size int) {
	data := make(map[int]int)
	for i := 0; i < size; i++ {
		data[i] = 1
	}
}
BenchmarkNoPreAllocMap-8         1887325               636.3 ns/op           941 B/op          3 allocs/op
BenchmarkPreAllocMap-8           3074744               389.7 ns/op           603 B/op          1 allocs/op

不断向map中添加元素操作会出发map的扩容机制,根据性能测试结果可知,提前分配好空间可以减少内存拷贝和rehash的消耗

性能优化 字符串

func Plus(n int, str string) string {
	s := ""
	for i := 0; i < n; i++ {
		s += str
	}
	return s
}

func StringBuilder(n int, str string) string {
	var builder strings.Builder
	for i := 0; i < n; i++ {
		builder.WriteString(str)
	}
	return builder.String()
}

func ByteBuffer(n int, str string) string {
	buf := new(bytes.Buffer)
	for i := 0; i < n; i++ {
		buf.WriteString(str)
	}
	return buf.String()
}

image (1).png 参照测试结果,+拼接性能最差,string.Builder和bytes.Buffer的性能较高

在Go语言中,字符串属于不可变类型,因此+每次都会重新分配内存,效率最低。后二者的底层都是[]bytes数组,不需要每次拼接都内存扩容,效果相对更好

性能优化 空结构体

空结构体实例不占用任何内存空间,在一些场景下可作为占位符使用

实现Set,可以考虑用map来实现,只需要用到map的键