Golang 避坑指南

2024-06-11 08:22:40 浏览数 (1)

本文将介绍 Golang 初学者容易菜的坑,希望广告 Gopher 避而远之。

1. Channel 与 Goroutine 泄露

当 channel 不恰当使用时,就可能导致 Goroutine 发生永久阻塞从而造成资源泄露。

先看一下 channel 不同状态下的读写与 close 操作的结果。

操作

未关闭

已关闭

nil

发数据

阻塞或成功发送

panic

永久阻塞

取数据

阻塞或成功接收

成功接收或零值

永久阻塞

关闭

成功关闭

panic

panic

1.1 发送不接收

对于一个已满的 channel(buffered channel 容量已满或是 unbuffered channel),继续向 其发送数据将会导致当前goroutine阻塞。为了避免这种情况需要使用其他机制通知发送者。

代码语言:javascript复制
// 错误示例
func produce() <-chan int {
    ch := make(chan int)
    
    go func() {
        defer close(ch)
        for i := 0; i < 10; i   {
            ch <- i
        }
    }()
    return ch
}

func main() {
    ch := produce()
    
    for num := range ch {
        if num == 2 {
             // 不想接收了,直接退出吧
             break
        }
        fmt.Println(num)
    }
    
    // 虽然此段代码能正常运行,但
    // produce产生goroutine将永远
    // 阻塞于 ch <- i上,造成资源泄露
}

// 修正
func produce(doneCh chan struct{}) <-chan int {
    ch := make(chan int)
    
    go func() {
        defer close(ch)
    loop:
        for i := 0; i < 10; i   {
            select {
                case ch <- i:
                case <-doneCh:
                    break loop
            }
         }
     }()
     return ch
}
func main() {
    doneCh := make(chan struct{})
    ch := produce(doneCh)
    
    for num := range ch {
        if num == 2 {
            // 不想接收了,先通知一下生产者
             close(doneCh)
             break
        } 
        fmt.Println(num)
    }
}

1.2 接收不发送

与前述情况相反,若接收者一直在一个不会再产生数据的 channel 上等待,将导致其所在routine 阻塞而泄露。 在Go中从一个 closed channel 读取数据:

  • 不会阻塞且获取对应类型的零值
  • for-range将退出
  • v, ok := <-ch中ok将为false 所以可以利用上述性质通知接收方结束数据读取。
代码语言:javascript复制
// 错误示例
func produce(doneCh chan struct{}) <-chan int {
    ch := make(chan int)
    go func() {
        select {
            case ch<-1:
            case <-doneCh:
            break
        }
        // 任务完成,直接退出
    }()
    return ch
}
func main() {
    doneCh := make(chan struct{})
    ch := produce(doneCh)
    for num := range ch {
        fmt.Println(num)
    }
    close(doneCh)
    // Output:
    // 1
    // fatal error: all goroutines are asleep - deadlock!
}

// 修正
func produce(doneCh chan struct{}) <-chan int {
    ch := make(chan int)
    go func() {
        // 退出前先关闭channel防止有routine阻塞在上面
        defer close(ch)
        select {
            case ch<-1:
            case <-doneCh:
                break
        }
    }()
    return ch
}
func main() {
    doneCh := make(chan struct{})
    ch := produce(doneCh)
    for num := range ch {
        fmt.Println(num)
    }
    close(doneCh)
    // Output:
    // 1
}

1.3 nil channel

向 nil channel 发送和接收数据都将会导致阻塞。这种情况可能在我们定义 channel 时忘记初始化的时候发生。

代码语言:javascript复制
func main() {
    defer func() {
        time.Sleep(time.Second)
        fmt.Println("num of routines: ", runtime.NumGoroutine())
    }()

    var ch chan int
    go func() {
        <-ch
        // ch<-
    }()
}

2. 跳出 for-switch 或 for-select

没有指定标签的 break 只会跳出 switch/select 语句, 若不能使用 return 语句跳出的话,可为 break 跳出标签指定的代码块。

注意 goto 虽然也能跳转到指定位置,但依旧会再次进入 for-switch,死循环。

代码语言:javascript复制
// break 配合 label 跳出指定代码块
func main() {
loop:
    for {
        switch {
        case true:
            fmt.Println("breaking out...")
            // break    // 死循环,一直打印 breaking out...
            break loop
        }
    }
    fmt.Println("out...")
}

3.for 迭代变量

3.1 闭包中的for迭代变量

for 语句中的迭代变量在每次迭代中都会重用,即 for 中创建的闭包函数 接收到的参数始终是同一个变量,所以在 goroutine 开始执行时都会得到同一个迭代值:

代码语言:javascript复制
// 错误示例
func main() {
    n := 2
    wg := sync.WaitGroup{}
    wg.Add(n)
    for i := 0; i < n; i   {
        go func() {
            defer wg.Done()
            fmt.Print(i)
        }()
    }
    wg.Wait()
    // Output:
    // 22
}

// 修正
func main() {
    n := 2
    wg := sync.WaitGroup{}
    wg.Add(n)
    for i := 0; i < n; i   {
        num := i
        go func() {
            defer wg.Done()
            fmt.Print(num)
        }()
        /*
        当然也可以这样
        go func(num int) {
            defer wg.Done()
            fmt.Println(num)
        }(i)
        */
    }
    wg.Wait()
    // Output:
    // 01 或 10 
}

3.2 for range 迭代变量

for range 循环中迭代变量的短声明只会在开始时执行一次,后面都是直接赋值,所以迭代变量的变量地址是不变的,避免将其赋值给指针。

代码语言:javascript复制
// 错误示例
slice1 := []int32{1, 2, 3, 4, 5}
slice2 := make([]*int32, len(slice1))
for i, item := range slice1 {
    slice2[i] = &item
}
for _, item := range slice2 {
    fmt.Printf("%v", *item)
}
// 55555

// 修正
func Int32(v int32) *int32 {
    return &v
}
func main() {
    slice1 := []int32{1, 2, 3, 4, 5}
    slice2 := make([]*int32, len(slice1))
    for i, item := range slice1 {
        slice2[i] = Int32(item)
    }
    for _, item := range slice2 {
        fmt.Printf("%v", *item)
    }
    // 12345
}

4. 循环内的 defer

对 defer 延迟执行的函数,会在调用它的函数结束时执行,而不是在调用它的语句块结束时执行,注意区分开。

代码语言:javascript复制
// 错误示例
type Resource struct {/*内部有一些需要释放的内容 */
}

func (r Resource) Destroy() { /*...*/ }

func getResource() Resource { /*...*/ }

func main() {
    for i := 0; i < 10000; i   {
        res := getResource()
        defer res.Destroy()
        // 会一直延迟至main结束才会释放
        // do something
    }
}

// 修正
type Resource struct { /* 内部有一些需要释放的内容 */ 
}

func (r Resource) Destroy() { /*...*/ }

func getResource() Resource { /*...*/ }

func main() {
    for i := 0; i < 10000; i   {
        func () {
            res := getResource()
            defer res.Destroy()
            // 下次循环前就会释放,当然你也可以在最后直接调用Destroy
            
            // do something
        }()
    }
}

5.defer 函数的参数值

defer 只会延迟其后函数的执行,而不会延迟函数的参数的求值,若希望延迟其参数 求值,通常会加上一层匿名函数。

代码语言:javascript复制
func main() {
    var i = 1
    times := func(num int) int {
        return num * 2
    }

    defer fmt.Println("resultA: ", times(i))
    defer func() {
        fmt.Println("resultB: ", func() int { return i * 2 }())
    }()
    i  

    // Output:
    //  resultB: 4
    //  resultA: 2
}

6.nil interface 和 nil interface 值

Golang 中 interface 类型变量的实现中包含值与类型,只有两者都为 nil 时该变量才为nil。

代码语言:javascript复制
// 错误示例
type Foo interface {
    Bar()
}

type FooImpl struct {
    num int
}

func (f *FooImpl) Bar() { fmt.Println(f.num) }

func GenFoo(num int) (Foo, error) {
    var f *FooImpl

    if num != 0 {
        f = &FooImpl{num}
    }
    return f, nil
}

func main() {
	f, _ := GenFoo(0)

	// this comparison is never true
	if f == nil {
		return
	}
	// Panic
	f.Bar()
}

// 正确示例
func GenFoo(num int) (Foo, error) {
    if num != 0 {
        f := &FooImpl{num}
        return f, nil
    }
    return nil, errors.New("num is zero")
}

那么如何判断 interface{} 的值是否为 nil 呢?

代码语言:javascript复制
func IsNil(i interface{}) {
	if i != nil {
		if reflect.ValueOf(i).IsNil() {
			fmt.Println("i is nil")
			return
		}
		fmt.Println("i isn't nil")
	}
	fmt.Println("i is nil")
}

7.结构体指针访问属性前先判空

当结构体指针为nil时,直接访问结构体属性会报空指针

代码语言:javascript复制
// 错误示例
type Struct1 struct {
    id int32
}
func main() {
    var a *Struct1
    //panic: runtime error: invalid memory address or nil pointer dereference
    a.id = 1
}

// 修正
type Struct1 struct {
    id int32
}
func main() {
    var a *Struct1
    if a != nil {
        a.id = 1
    }
}

8.读取有顺序需要的不能使用map结构

Go 里面的map存储是无序的,for循环读取与写入的顺序并不同,需要排序的功能不能使用map,而需要使用slice。

代码语言:javascript复制
// map 读取情况
intMap := make(map[int]int, 10)

for i := 0; i < 10; i   {
   intMap[i] = i
}

for _, v := range intMap {
   fmt.Println(v)
}
//9
//3
//7
//……
//没有按照写入顺序输出,乱序的

// slice 读取情况 
intSlice := make([]int, 0, 10)

for i := 0; i < 10; i   {
   intSlice = append(intSlice, i)
}

for _, v := range intSlice {
   fmt.Println(v)
}
//0
//1
//2
//...
//读取是有序的

参考文献

Go 神坑 1 —— interface{} 与 nil 的比较 - CSDN 50 Shades of Go: Traps, Gotchas, and Common Mistakes 50 Shades of Go: Traps, Gotchas, and Common Mistakes中文翻译 如何防止 goroutine 泄露

0 人点赞