Java有经典的《Effective Java》,但是Google了很久也没有找到类似的golang书籍。可能golang本身足够简单,坑也比较少吧。总结了一些,自己觉得可能对golang新手有帮助的小TIPs。关于字符串的处理,这里没有列举,感兴趣的同学可以自己在网上找到相关资料。总的原则和Java等语言类似,如果有大量字符串处理的逻辑,最好不要直接使用+ 而是找一种有buffer的替代方式。
- for i := 0; i < 5; i++ {
- defer fmt.Printf("%d ", i)
- }
输出
4 3 2 1 0
对于一些简短的加锁-修改某个值-释放锁的操作可以这么做。但是如果在加锁之后,可能在不同的分支都需要执行释放锁,还是建议直接使用defer Mutex.Unlock, 否则在(以后)修改或者新增分支的时候可能忘记释放锁从而引入bug。除非必要,应该以代码的可维护性为主。
- var i int
- func unlockDefer(l *sync.Mutex) {
- l.Lock()
- defer l.Unlock()
- i = rand.Int()
- }
- func unlockDirect(l *sync.Mutex) {
- l.Lock()
- i = rand.Int()
- l.Unlock()
- }
- func BenchmarkUnlockDefer(b *testing.B) {
- l := &sync.Mutex{}
- for n := 0; n < b.N; n++ {
- unlockDefer(l)
- }
- }
- func BenchmarkUnlockDirect(b *testing.B) {
- l := &sync.Mutex{}
- for n := 0; n < b.N; n++ {
- unlockDirect(l)
- }
- }
-
- BenchmarkUnlockDefer-4 20000000 56.9 ns/op
- BenchmarkUnlockDirect-4 50000000 31.7 ns/op
[3]int和[4]int是不同的类型
- func modify(a [3]int) {
- a[0] = 0
- }
- func main() {
- x := [...]int{3,2,1}
- modify(x)
- fmt.Println(x)
- }
输出
[3 2 1]
golang 都是传值的,slice看起来像是传引用是因为slice的值本身包含了一个对底层数组的引用(指针)。通过查看下面的slice结构体描述就很清晰。src/runtime/slice.go
- type slice struct {
- array unsafe.Pointer
- len int
- cap int
- }
==操作符,但是array可以。slice 只能使用== 判断是否为nil。
- var s []int
- var wg sync.WaitGroup
- for i := 0; i < 10; i++ {
- wg.Add(1)
- go func(i int) {
- s = append(s, i)
- wg.Done()
- }(i)
- }
- wg.Wait()
- fmt.Println(s)
一种可能的输出:
[0 6 7 9]
golang language specification中关于variables的介绍 中有这样一句话
Structured variables of array, slice, and struct types have elements and fields that may be addressed individually. Each such element acts like a variable.
译:数组、片和结构类型的结构化变量具有可以单独寻址的元素和字段。每个这样的元素就像一个变量。
也就是说对于slice s而言,我们应该将s[i]看做是一个独立的变量(variable)。例如,对于下面这段代码,虽然a、b是两个完全不同是slice,但是a[1]与b[0]指向了同一块内存区域,等同于同一个变量。如果对a[1]与b[0]的并发访问不做可见性保护(如加锁),那么可能在代码中出现并发bug。(并不一定会触发这个bug,但是存在几率。因为现阶段groutine的加载速度没有main程序快,就和这个demo来说)
- func main() {
- a := []int{1,2}
- b := a[1:]
- go func() {
- a[1] = 0
- }()
- fmt.Println(b[0])
- }
- go run -race ./
- 2
- ==================
- WARNING: DATA RACE
- Write at 0x00c0000a2008 by goroutine 6:
- main.main.func1()
- /Users/vicxiao/workspace/go/test/main.go:11 +0x47
- Previous read at 0x00c0000a2008 by main goroutine:
- main.main()
- /Users/vicxiao/workspace/go/test/main.go:13 +0xaa
- Goroutine 6 (running) created at:
- main.main()
- /Users/vicxiao/workspace/go/test/main.go:10 +0x98
- ==================
- Found 1 data race(s)
如果将第5行a[1]=0 替换为a[0]=0,那么因为a[0]与b[0]指向的是不同内存区域,就不会有竞态发生。
例如,使用ioutil.ReadAll读取了一个10M的文件,并将其前512K切片为另外一个slice b使用。只要b还被引用,那么其指向的底层数组也不会被释放。
- import (
- "crypto/rand"
- "testing"
- )
- var (
- src = make([]byte, 512)
- dst = make([]byte, 512)
- )
- func genSource() {
- rand.Read(src)
- }
- func BenchmarkCopy(b *testing.B) {
- for n := 0; n < b.N; n++ {
- b.StopTimer()
- genSource()
- b.StartTimer()
- copy(dst, src)
- }
- }
- func BenchmarkAppend(b *testing.B) {
- for n := 0; n < b.N; n++ {
- b.StopTimer()
- genSource()
- b.StartTimer()
- dst = append(dst, src...)
- }
- }
注:
dst作为全局变量是防止编译器优化for-loop
测试时,机器不繁忙
- uptime;go version;go test -bench=. ./
- 11:56:10 up 294 days, 14:58, 3 users, load average: 0.58, 0.52, 0.63
- go version go1.14.1 linux/amd64
- goos: linux
- goarch: amd64
- pkg: copyvsappend
- BenchmarkCopy-40 9808320 116 ns/op
- BenchmarkAppend-40 479055 8740 ns/op
- PASS
ReadFull而不是ioutil.ReadAll常见的场景如读取HTTP body。如果HTTP header中包含了Content-Length那么我们先申请足够大小的内存,然后调用一次io.ReadFull 是最佳的选择。如果我们使用ioutil.ReadAll 则会发生多次不必要的内存申请和内存拷贝。因为ReadAll 是以一个较小的buffer开始,不断地循环调用ReadFull.
这个比较直观,因为有很多内存拷贝
- var X [1 << 15]struct {
- val int
- _ [4096]byte
- }
- var Result int
- func BenchmarkRangeIndex(b *testing.B) {
- var r int
- for n := 0; n < b.N; n++ {
- for i := range X {
- x := &X[i]
- r += x.val
- }
- }
- Result = r
- }
- func BenchmarkRangeValue(b *testing.B) {
- var r int
- for n := 0; n < b.N; n++ {
- for _, x := range X {
- r += x.val
- }
- }
- Result = r
- }
- func BenchmarkFor(b *testing.B) {
- var r int
- for n := 0; n < b.N; n++ {
- for i := 0; i < len(X); i++ {
- x := &X[i]
- r += x.val
- }
- }
- Result = r
- }
- BenchmarkRangeIndex-4 10000 127986 ns/op
- BenchmarkRangeValue-4 50 25939345 ns/op
- BenchmarkFor-4 10000 127817 ns/op
这个有什么好的替代方法吗,比如直接使用for i 遍历数组长度?(从根本解决,要么不要创建大数组,要么大数组不要遍历)
*T类型,那么S定义的函数也应该以*S为receiver为佳这个主要是防止引入bug,例如下面的并发控制器,错误地在外层将receiver定义为值类型。
- type ConcurrentLocker struct{
- sync.Map
- }
- type LeaveFunc func()
- func (cl ConcurrentLocker) Enter(key string) (bool, LeaveFunc) {
- if _, occupied := cl.LoadOrStore(key, struct{}{}); !occupied {
- return true, func(){
- cl.Delete(key)
- }
- }
- return false, nil
- }
- func main() {
- cl := ConcurrentLocker{}
- key := "some-key"
- fmt.Println(cl.Enter(key))
- fmt.Println(cl.Enter(key))
- }
这个错误的代码并不能达到并发控制的效果:
- $ go run ./
- true 0x10939e0
- true 0x10939e0
当然修复的办法可以是内嵌* sync.Map, 但是这样初始化的时候不够优雅,需要单独初始化内部的map指针。个人更倾向于与将Enter 的接受者申明为* ConcurrentLocker, 这样既逻辑正确,其作为整体暴露的函数接受者也统一。
reflect: reflect.Value.SetLen using unaddressable value这也是新手极易容易犯得错误。
如果出现这个错误
那么就说明在我们使用GORM的时候,某些地方对指针的使用出现错误,也便需要我们去检查代码中是否有些地方缺少指针的使用。例如我的代码中:

因上方datas已经被指针引用,而在使用GORM时,未使用指针的结构体,导致GORM在查询到数据赋值时出现错误,解决方式如下: