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)
我多次回答了同样的问题:从go 中的函数返回什么更可取,一片结构还是一片指向这些结构的指针?所以我决定写这篇文章来展示这两种方法之间的区别。
换句话说,问题是以下哪个功能更好。
func ReturnSliceWithPointers() []*Person
func ReturnSliceWithStructs() []Person
更好的,在这种情况下,工作手段的快d使用较少的内存。最简单的方法是使用 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
带有空default
case的语句,这样如果没有 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
最后的话
唯一真正的错误是我们一无所获。
- 点赞
- 收藏
- 关注作者
评论(0)