golang中常见的认知错误记录

网友投稿 540 2022-05-30

最近的一个项目中, 我采用了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)

我多次回答了同样的问题:从go 中的函数返回什么更可取,一片结构还是一片指向这些结构的指针?所以我决定写这篇文章来展示这两种方法之间的区别。

换句话说,问题是以下哪个功能更好。

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

更好的,在这种情况下,工作手段的快d使用较少的内存。最简单的方法是使用 golang 测试包提供的工具。我写了两个类似的函数,它们创建、填充和返回一个数组。我为他们写了一个基准。

golang中常见的认知错误记录

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

结论

我们看到函数ReturnSliceWithStructs的分配更少。每次操作使用的内存也更少,性能更好。

同时,函数ReturnSliceWithPointers看起来更糟:性能和内存效率更低。

它有更多的内存分配:一个分配给一个切片,一个分配给一个切片中的每个项目。

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

正因为如此,它会在 GC 上产生更多的负载。

那么使用哪一种呢?看起来选择是显而易见的,但并非总是如此。在某些情况下,您可以更喜欢一种方法而不是另一种方法。首先,问问自己,你有必要在乎它吗?如果是,那么决定完全取决于您的应用程序设计和您使用的库的接口。请记住:您始终可以使用类似的基准来查找提示。

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

这些是我在编写 Go 时犯的错误。虽然这些可能不会导致任何类型的错误,但它们可能会影响软件。

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

如您所见,out切片中的所有元素都是 3。实际上很容易解释为什么会发生这种情况:在每次迭代中,我们都将 的地址附加v到out切片中。如前所述,v是一个在每次迭代中都采用新值的单个变量。因此,正如您在输出的第二行中看到的那样,地址是相同的,并且所有地址都指向相同的值。

简单的解决方法是将循环迭代器变量复制到一个新变量中:

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

同样的问题可以发现循环迭代变量正在 Goroutine 中使用。

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

输出将是:

3 3 3

可以使用上述相同的解决方案来修复它。请注意,没有使用 Goroutine 运行该函数,代码会按预期运行。

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() }

在上面的例子中,如果你使用第 8 行而不是第 10 行,下一次迭代不能持有互斥锁,因为锁已经被使用并且循环永远阻塞。

如果您真的需要在循环内使用 defer,您可能需要委托另一个函数来完成这项工作。

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 }() }

但是,有时defer在循环中使用可能会变得方便。所以你真的需要知道你在做什么。

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. 不使用接口

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

在众多的接口,io.Reader并且io.Writer可能是最可爱的人。

type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) }

这些接口可能非常强大。假设您要将一个对象写入文件,因此您定义了一个Save方法:

func (o *obj) Save(file os.File) error

如果你需要写到http.ResponseWriter第二天怎么办?您不想定义新方法。你?所以使用io.Writer.

func (o *obj) Save(w io.Writer) error

还有一个重要的注意事项,您应该知道的是,始终询问您将要使用的行为。在上面的示例中,请求 anio.ReadWriteCloser也可以工作,但是当您要使用的唯一方法是Write. 接口越大,抽象越弱。

所以大多数时候你最好保持行为而不是具体的类型。

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 }

似乎两种类型的大小都相同,均为 21 字节,但结果显示出完全不同的内容。使用 编译代码GOARCH=amd64,BadOrderedPerson类型分配 32 个字节,而OrderedPerson类型分配24 个字节。为什么?嗯,原因是数据结构对齐。在 64 位架构中,内存分配 8 字节的连续数据包。需要添加的 Padding 可以通过以下方式计算:

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{} }

当您有一个大的常用类型时,它可能会导致性能问题。但别担心,您不必手动处理所有结构。使用maligned您可以轻松检查您的代码是否存在此问题。

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

启用竞争检测器后,编译器将记录在代码中访问内存的时间和方式,同时runtime监视对共享变量的非同步访问。

当发现数据竞争时,竞争检测器会打印一份报告,其中包含冲突访问的堆栈跟踪。下面是一个例子:

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

最后的话

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

Go HTTP

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:华为洪方明:云+AI为煤炭行业智能化升级注入新动能
下一篇:【上电即上华为云】华为云openCPU智联模组_Cat.1_MC615-CN_L610-CN_OTA升级
相关文章