网站首页 > 技术文章 正文
虽然 Go 容易学习,但新手还是比较容易犯一些错误的。本文总结了 5 个常见的错误,你检验下自己犯过没有?!
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 这个 slice 中的元素都是 3。实际上很容易解释为什么会这样:在每次迭代中,我们都将 v append 到 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
看一段代码:
var wg sync.WaitGroup
wg.Add(len(tasks))
for _, t := range tasks {
go func(t *task) {
defer group.Done()
}(t)
// group.Wait()
}
group.Wait()
WaitGroup 常用来等待多个 goroutine 运行完成。但如果 Wait 在循环内部调用,即代码中第 7 行的位置,得到的结果就不是预期的了。这个错误犯错率应该比较低。
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
}()
}
2、channel 堵塞
一般认为 goroutine + channel 是 Go 的利器。Go 强调不要通过共享内存来通讯,而是通过通讯来共享内存。
但在使用 channel 的过程中,需要注意堵塞问题,避免导致 goroutine 泄露。比如下面的代码:
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 执行 do 函数并通过第 6 行的通道 ch 将结果发送回父 goroutine。子 goroutine 将在第 6 行阻塞,直到父 goroutine 在第 9 行从 ch 接收到结果为止。同时,父 goroutine 将在 select 阻塞,直到子 goroutine 将结果发送给 ch(第 9 行)或超时(第 11 行)。如果超时先发生,则父 goroutine 将从 doReq 第 12 行返回,这会导致没有 goroutine 从 ch 读取数据,子 goroutine 就会一直堵塞在第 6 行。解决办法是将 ch 从无缓冲的通道改为有缓冲的通道,因此子goroutine 即使在父 goroutine 退出后也始终可以发送结果。
这个错误出现概率不会低。还有特别要注意的一点,就是 time.After 导致的内存泄露问题,只要注意程序不是频繁执行上面的 select 即可(毕竟 time.After 到时间了还是会回收资源的)。
3、不使用接口
接口可以使代码更灵活。这是在代码中引入多态的一种方法。接口允许你定义一组行为而不是特定类型。不使用接口可能不会导致任何错误,但是会导致代码简单性,灵活性和扩展性降低。
在 Go 接口中, 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,显然不太适合再创建另外一个 Save 方法,这时应该用 io.Writer:
func (o *obj) Save(w io.Writer) error
另外,你应该知道的重要注意事项是,始终关注行为。在上面的示例中,虽然 io.ReadWriteCloser 也可以使用,但你只需要 Write 方法。接口越大,抽象性越弱。在 Go 中,通常提倡小接口。
所以,我们应该优先考虑使用接口,而不是具体类型。
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 = (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{}
}
当你使用大型常用类型时,可能会导致性能问题。但是不用担心,你不必手动处理所有结构。这工具可以轻松的解决此类问题:https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment。
5、测试中不使用 race 探测器
数据争用会导致莫名的故障,通常是在代码已部署到线上很久之后才出现。因此,它们是并发系统中最常见且最难调试的错误类型。为了帮助区分此类错误,Go 1.1 引入了内置的数据争用检测器(race detector)。可以简单地添加 -race flag 来使用。
nbsp;go test -race pkg # to test the package
nbsp;go run -race pkg.go # to run the source file
nbsp;go build -race # to build the package
nbsp;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
总结
错误不可怕,但我们需要从错误中吸取教训,避免再次掉入同样的坑里。掉入一个坑, 我们应该想办法探究出原因,知道为什么,下次再掉坑的可能性就会小很多。
除了以上这几点,还有哪些你常碰到的错误或坑呢?欢迎留言交流!
猜你喜欢
- 2024-10-12 大牛巧用一文带你彻底搞懂解释器的内部构造和解释执行过程
- 2024-10-12 JAVA中锁的深入理解与解析 java 锁的是什么
- 2024-10-12 C++核心准则CP.44:记得为lock_guards和unique_locks命名
- 2024-10-12 深入JVM锁机制1-synchronized jvm的锁
- 2024-10-12 一文搞懂Linux线程同步原理 linux多线程同步机制
- 2024-10-12 【C++并发编程】(三)互斥锁 互斥锁实现原理
- 2024-10-12 C语言中的并发编程技巧:提高程序的并行性和效率
- 2024-10-12 如何使用C语言进行并发编程? c并发编程实战 中文版 pdf
- 2024-10-12 C++20 新特性(15):协程(Coroutines )
- 2024-10-12 Go中读写锁RWMutex的基本用法 go 读写锁
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)