Go 陷阱之 for 循环调用函数

循环调用 defer 错误 #

defer 在函数退出时才会执行,在循环中执行 defer 释放资源时,由于延迟可能会引发 资源泄露问题

错误的做法 #

package main

import (
	"log"
	"os"
)

func main() {
	for i := 0; i < 5; i++ {
		f, err := os.Open("/path/to/file")
		if err != nil {
			log.Fatal(err)
		}
		defer f.Close()
	}

	zero := 0
	println(1 / zero) // 程序执行到这里异常退出,那么上面的循环中打开的 5 个文件句柄全部无法泄露
}

错误的原因在于: 极端情况下(比如 for 循环执行完程序异常,或者 for 还没执行完程序异常),将导致所有文件句柄无法释放,造成资源泄露

再比如在第 4 次循环的时候,打开文件报错了,接着调用 log.Fatal(err) 结束程序,这时候,前面 3 次循环打开的 3 个文件句柄资源无法被释放,造成资源泄露

正确的做法 #

解决的方法: 可以在 for 中构造一个局部函数,然后在局部函数内执行 defer 函数释放资源, 这样即使极端情况下程序异常退出,但是已经打开的文件句柄已经全部被释放,不会造成资源泄露。

package main

import (
	"log"
	"os"
)

func main() {
	for i := 0; i < 5; i++ {
		func() {
			f, err := os.Open("/path/to/file")
			if err != nil {
				log.Fatal(err)
			}
			defer f.Close()
		}()
	}

	zero := 0
	println(1 / zero) // 程序执行到这里异常退出,但是上面的循环中打开的 5 个文件句柄资源已经全部释放,不会造成任何影响
}

循环调用 goroutine 错误 #

错误的做法 #

package main

import (
	"fmt"
	"time"
)

func main() {
	numbers := []int{1, 2, 3, 4, 5}

	for _, n := range numbers {
		go func() {
			fmt.Printf("%v ", n)
		}()
	}

	time.Sleep(time.Second)
}

// $ go run main.go
// 输出如下
/**
  5 5 5 5 5
*/

错误的原因在于: 每次循环时 goroutinen 都是同一个变量,循环结束后,该变量等于最后一次赋值后的值,也就是 5

正确的做法 #

下面描述了 2 种解决问题的方法。

1. goroutine 函数参数

package main

import (
	"fmt"
	"time"
)

func main() {
	numbers := []int{1, 2, 3, 4, 5}

	for _, n := range numbers {
		go func(val int) {
			fmt.Printf("%v ", val)
		}(n) // 将当前元素作为参数传入
	}

	time.Sleep(time.Second)
}

// $ go run main.go
// 输出如下,你的输出可能和这里的不一样 
/**
  5, 4, 3, 1, 2
*/

2. 变量重新赋值

package main

import (
	"fmt"
	"time"
)

func main() {
	numbers := []int{1, 2, 3, 4, 5}

	for _, n := range numbers {
		cur := n
		go func() {
			fmt.Printf("%v ", cur)
		}()
	}

	time.Sleep(time.Second)
}

// $ go run main.go
// 输出如下,你的输出可能和这里的不一样 
/**
  5, 4, 1, 3, 2
*/

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。

© 蛮荆 | 陕公网安备 61011302001681 号 | 陕ICP备2023004378号-1 | Rendered by Hugo