golang中常见的认知错误记录

最近的一个项目中, 我采用了go作为我的后端基础,需求总体上并不复杂,代码写着写着就变多了,除去脚手架生成的代码,代码其实并不多;期间遇到不少关于go语法认知的小问题,早就想开个帖子单独记录下,这周终于有空开始发发博客了,整理下集中放一个帖子,帖子上面放我自己的一些收集,下面部分放一些网络上的相关帖子.

PART.A

  • golang中的switch(参考https://yourbasic.org/golang/switch-statement/,https://www.runoob.com/go/go-switch-statement.html,https://studygolang.com/articles/28415,https://www.cnblogs.com/yahuian/p/11615408.html)

需要注意的点,代码段中自带break,由于这点多条件语句不能像其他语言中那样写,多条件的语法是单行中逗号这种形式,由于经常写不同的语言,我不倾向于使用fallthrough这个关键词;

  • 由于golang中存在指针,虽然他的解指针等等已经做的很舒适了,但是其实容易犯一种不易察觉的错误,slice中存储了同一个指针,循环中操作到最后所有的值其实是同一个;
  • gorm使用很方便,但是我有个有个比较常犯的错误,查询出错并不包含查询到0条记录;
  • 待续

PART.B

Go: what to return? A slice of structs vs a slice of pointers?(https://andrii-kushch.medium.com/go-what-to-return-a-slice-of-structs-vs-a-slice-of-pointers-42647912530a)

func ReturnSliceWithPointers() []*Person 
func ReturnSliceWithStructs() []Person
package main

import "testing"

type Person struct {
   Age int
}

func ReturnSliceWithPointers(size int) []*Person {
   res := make([]*Person, size)

   for i := 0; i < size; i++ {
      res[i] = &Person{}
   }

   return res
}


func ReturnSliceWithStructs(size int) []Person {
   res := make([]Person, size)

   for i := 0; i < size; i++ {
      res[i] = Person{}
   }

   return res
}


func Benchmark_ReturnSliceWithPointers(b *testing.B) {
   for i := 0; i < b.N; i++ {
      ReturnSliceWithPointers(10000)
   }
}

func Benchmark_ReturnSliceWithStructs(b *testing.B) {
   for i := 0; i < b.N; i++ {
      ReturnSliceWithStructs(10000)
   }
}

让我们运行它

go test -bench=. -benchmem -benchtime=10000x

结论

res := make([]*Person, size)
for i := 0; i < size; i++ {
   res[i] = &Person{}
}

5 Mistakes I’ve Made in Go(https://medium.com/swlh/5-mistakes-ive-made-in-go-75fb64b943b8)

To err is human, to forgive divine.
— Alexander Pope

1. 内循环

有几种方法可以在循环中弄乱您需要注意的问题。

1.1 使用引用来循环迭代器变量

由于效率原因,循环迭代器变量是单个变量,在每次循环迭代中采用不同的值。它可能会导致不知情的行为。

in := []int{1, 2, 3}

var out []*int
for  _, v := range in {
	out = append(out, &v)
}

fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])

结果将是:

Values: 3 3 3
Addresses: 0xc000014188 0xc000014188 0xc000014188
in := []int{1, 2, 3}

var out []*int
for  _, v := range in {
	v := v
	out = append(out, &v)
}

fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
Values: 1 2 3
Addresses: 0xc0000b6010 0xc0000b6018 0xc0000b6020
list := []int{1, 2, 3}

for _, v := range list {
	go func() {
		fmt.Printf("%d ", v)
	}()
}
3 3 3

1.2 循环调用WaitGroup.Wait

这个错误可以使用类型的共享变量来犯WaitGroup,如下面的代码所示Wait(),当Done()第 5 行被调用len(tasks)次数时,第7 行只能被解除阻塞,因为它被用作在第 2 行调用的参数Add()。但是,在Wait()循环内部调用了 ,因此它会在下一次迭代中阻止在第 4 行创建 Goroutine。简单的解决方案是将Wait()out的调用从循环中移出。

var wg sync.WaitGroup
wg.Add(len(tasks))
for _, t := range tasks {
	go func(t *task) { 
		defer group.Done()
	}(t)
	// group.Wait()
}

group.Wait()

1.3 在循环中使用 defer

defer函数返回之前不会执行。defer除非您确定自己在做什么,否则不应在循环中使用。

var mutex sync.Mutex
type Person struct {
	Age int
}
persons := make([]Person, 10)
for _, p := range persons {
	mutex.Lock()
	// defer mutex.Unlock()
	p.Age = 13
	mutex.Unlock()
}
var mutex sync.Mutex
type Person struct {
	Age int
}
persons := make([]Person, 10)
for _, p := range persons {
	func() {
		mutex.Lock()
		defer mutex.Unlock()
		p.Age = 13
	}()
}

2. 发送到无保障频道

您可以将值从一个 Goroutine 发送到通道,然后将这些值接收到另一个 Goroutine。默认情况下,发送和接收阻塞,直到对方准备好。这允许 Goroutines 在没有显式锁或条件变量的情况下进行同步。

func doReq(timeout time.Duration) obj {
	// ch :=make(chan obj)
	ch := make(chan obj, 1)
	go func() {
		obj := do()
		ch <- result
	} ()
	select {
	case result = <- ch :
		return result
	case<- time.After(timeout):
		return nil 
	}
}

让我们检查上面的代码。该doReq函数在第 4 行创建一个子 Goroutine 来处理请求,这是 Go 服务器程序中的常见做法。子 Goroutine在第 6 行执行do函数并通过 channel 将结果发送回父ch。子将在第 6 行阻塞,直到父ch在第 9 行收到结果。同时,父将阻塞,select直到子将结果发送到ch(第 9 行)或发生超时时(第 11 行)。如果超时发生得更早,父doReq进程将在第 12 行从func返回,并且没有其他人可以再收到结果ch,这导致子进程被永远阻塞。解决方法是改变ch从一个无缓冲通道到一个缓冲通道,这样子 Goroutine 总是可以发送结果,即使父 Goroutine 已经退出。另一个解决方法是在第 6 行使用一个select带有空defaultcase的语句,这样如果没有 Goroutine 接收ch,default就会发生。尽管此解决方案可能并不总是有效。

...
select { 
case ch <- result: 
default:
}
...

3. 不使用接口

接口可以使代码更加灵活。这是在代码中引入多态的一种方式。接口允许您请求一组行为而不是特定类型。不使用接口可能不会导致任何错误,但可能会导致代码不那么简单、不灵活和可扩展性较差。

type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}
func (o *obj) Save(file os.File) error
func (o *obj) Save(w io.Writer) error

4. 错误的有序结构

这个错误也不会导致任何错误,但它会导致更多的内存使用。


type BadOrderedPerson struct {
	Veteran bool   // 1 byte
	Name    string // 16 byte
	Age     int32  // 4 byte
}

type OrderedPerson struct {
	Name    string
	Age     int32
	Veteran bool
}
padding = (align - (offset mod align)) mod align
aligned = offset + padding
        = offset + ((align - (offset mod align)) mod align)
type BadOrderedPerson struct {
	Veteran bool     // 1 byte
	_       [7]byte  // 7 byte: padding for alignment
	Name    string   // 16 byte
	Age     int32    // 4 byte
	_       struct{} // to prevent unkeyed literals
	// zero sized values, like struct{} and [0]byte occurring at 
	// the end of a structure are assumed to have a size of one byte.
	// so padding also will be addedd here as well.
	
}

type OrderedPerson struct {
	Name    string
	Age     int32
	Veteran bool
	_       struct{} 
}

5. 在测试中不使用种族检测器

数据竞争会导致神秘的失败,通常是在代码部署到生产之后很久。因此,这些是并发系统中最常见和最难调试的错误类型。为了帮助区分这些类型的错误,Go 1.1 引入了一个内置的数据竞争检测器。只需添加-race标志即可使用。

$ go test -race pkg    // to test the package
$ go run -race pkg.go  // to run the source file
$ go build -race       // to build the package
$ go install -race pkg // to install the package
WARNING: DATA RACE
Read by goroutine 185:
  net.(*pollServer).AddFD()
      src/net/fd_unix.go:89 +0x398
  net.(*pollServer).WaitWrite()
      src/net/fd_unix.go:247 +0x45
  net.(*netFD).Write()
      src/net/fd_unix.go:540 +0x4d4
  net.(*conn).Write()
      src/net/net.go:129 +0x101
  net.func·060()
      src/net/timeout_test.go:603 +0xaf
Previous write by goroutine 184:
  net.setWriteDeadline()
      src/net/sockopt_posix.go:135 +0xdf
  net.setDeadline()
      src/net/sockopt_posix.go:144 +0x9c
  net.(*conn).SetDeadline()
      src/net/net.go:161 +0xe3
  net.func·061()
      src/net/timeout_test.go:616 +0x3ed
Goroutine 185 (running) created at:
  net.func·061()
      src/net/timeout_test.go:609 +0x288
Goroutine 184 (running) created at:
  net.TestProlongTimeout()
      src/net/timeout_test.go:618 +0x298
  testing.tRunner()
      src/testing/testing.go:301 +0xe8

最后的话

唯一真正的错误是我们一无所获。

(完)