Golang 作为自带垃圾回收(Garbage Collection,GC)机制的语言,可以自动管理内存。但在实际开发中代码编写不当的话也会出现内存泄漏的情况。
内存池(如 sync.Pool)如果使用不当,可能会导致内存泄漏。例如,如果池中的对象持有对其他大型数据结构的引用,这些数据结构可能不会被及时回收。要避免这种情况,一种做法是在把对象放回 Pool 之前,确保清除该对象的所有外部引用。但是,这需要程序员非常理解自己代码的行为和 sync.Pool 的工作方式,否则就很容易出错。
var a []int func test(b []int) { a = b[:3] return }如果传入的slice b很大,全局量a引用了b的一小部分,这样新、旧slice指向的都是同一片内存地址,那么只要全局量a在,b就不会被回收,从而造成了所谓的内存泄漏。一般我们不会写这种代码,定义一个全局切片本身就不是一个好的设计。
func test(b []int) { a = make([]int, 3) copy(a, b[:3]) return }
这样,a 将持有 b 前三个元素的一个副本,而不是直接引用 b,所以即使 a 依然存在,b 也可以被垃圾回收器正常回收。
使用select时如果有case没有覆盖完全的情况且没有default分支进行处理,最终会导致内存泄漏。具体说来,如果你在 select 语句中等待来自某个 channel 的数据,但是没有更多的数据发送到这个 channel 上,或者没有其他协程来从这个 channel 接收数据,那么使用 select 的协程就会永远阻塞。因为这个协程无法结束,所以它占用的所有资源(包括内存)都无法被垃圾回收器释放,从而导致内存泄露。
要避免这种情况,一种做法是使用带有超时机制的 select。你可以通过 time.After 函数创建一个定时 channel,当超过指定时间后,这个 channel 就可以接收到一个数据,这样就可以防止 select 永久阻塞。
此外,还需要确保每个打开的 channel 最终都会正确关闭,这样接收方就可以得到一个零值以及一个标志,表示 channel 已经没有更多数据。对于发送方,如果知道没有更多接收器,应该关闭 channel,否则发送操作可能会永远阻塞。
// https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go#LL150C1-L158C2 // After waits for the duration to elapse and then sends the current time // on the returned channel. // It is equivalent to NewTimer(d).C. // The underlying Timer is not recovered by the garbage collector // until the timer fires. If efficiency is a concern, use NewTimer // instead and call Timer.Stop if the timer is no longer needed. func After(d Duration) <-chan Time { return NewTimer(d).C }代码注释已经描述的很清楚了,每次time.After(duration x)会产生NewTimer(), 在duration x到期之前,新创建的timer不会被GC,到期之后才会GC。随着时间推移,尤其是duration x很大的话,会产生内存泄露的问题。如果担心效率问题,可以使用 NewTimer 代替,如果不需要定时器可以调用 Timer.Stop 停止定时器。
package main import ( "fmt" "net/http" _ "net/http/pprof" "time" ) func main() { fmt.Println("start...") ch1 := make(chan string, 120) go func() { // time.Sleep(time.Second * 1) i := 0 for { i++ ch1 <- fmt.Sprintf("%s %d", "hello", i) } }() go func() { // http 监听8080, 开启 pprof if err := http.ListenAndServe(":8080", nil); err != nil { fmt.Println("listen failed") } }() for { select { case _ = <-ch1: // fmt.Println(res) case <-time.After(time.Minute * 3): fmt.Println("timeout") } } }在上面的程序中,time.After(time.Minute * 3) 设置了 3 分钟,也就是说 3 分钟后才会执行定时器任务。而这期间会不断被 for 循环调用 time.After,导致它不断创建和申请内存,内存就会一直往上涨。
package main import ( "fmt" "net/http" _ "net/http/pprof" "time" ) func main() { fmt.Println("start...") ch1 := make(chan string, 120) go func() { // time.Sleep(time.Second * 1) i := 0 for { i++ ch1 <- fmt.Sprintf("%s %d", "hello", i) } }() go func() { // http 监听8080, 开启 pprof if err := http.ListenAndServe(":8080", nil); err != nil { fmt.Println("listen failed") } }() duration := time.Minute * 2 timer := time.NewTimer(duration) defer timer.Stop() for { timer.Reset(duration) // 这里加上 Reset() select { case _ = <-ch1: // fmt.Println(res) case <-timer.C: fmt.Println("timeout") return } } }channel阻塞
func channelTest() { //声明未初始化的channel读写都会阻塞 var c chan int //向channel中写数据 go func() { c <- 1 fmt.Println("g1 send succeed") time.Sleep(1 * time.Second) }() //从channel中读数据 go func() { <-c fmt.Println("g2 receive succeed") time.Sleep(1 * time.Second) }() time.Sleep(10 * time.Second) }为避免这种情况,在声明 channel 类型的变量后,别忘了使用 make 函数进行初始化。
func channelTest() { var c = make(chan int) // 堆代码 duidaima.com //10个协程向channel中写数据 for i := 0; i < 10; i++ { go func() { c <- 1 fmt.Println("g1 receive succeed") time.Sleep(1 * time.Second) }() } //1个协程从channel读数据 go func() { <- c fmt.Println("g2 send succeed") time.Sleep(1 * time.Second) }() //会有写的9个协程阻塞得不到释放 time.Sleep(10 * time.Second) }通常我们不会编写像上面这样使多个 goroutine 阻塞的代码。对于无缓冲的 channel,一般来说,最佳做法是使一个 goroutine 发送数据,另一个 goroutine 接收数据,以确保数据的顺序性和同步性。
func channelTest() { var c = make(chan int, 8) //10个协程向channel中写数据 for i := 0; i < 10; i++ { go func() { c <- 1 fmt.Println("g2 send succeed") time.Sleep(1 * time.Second) }() } go func() { <- c fmt.Println("g1 receive succeed") time.Sleep(1 * time.Second) }() //会有写的几个协程阻塞写不进去 time.Sleep(10 * time.Second) }实际编程中,我们往往不会编写这样可能导致大量 goroutine 阻塞的代码。通常情况下,读操作是在一个循环中进行的,以便持续消费 channel 中的数据。例如,我们可以使用 range 来迭代一个 channel,这将持续获取 channel 直到它关闭。
go func() { for v := range c { fmt.Printf("Received: %d\n", v) } }()或者,在处理不能保证发送方总是比接收方快的场合,可以通过 select 结构添加超时机制来避免死锁。
func channelTest() { var c = make(chan int) //10个协程从channel中读数据 for i := 0; i < 10; i++ { go func() { <- c fmt.Println("g1 receive succeed") time.Sleep(1 * time.Second) }() } //1个协程向channel写数据 go func() { c <- 1 fmt.Println("g2 send succeed") time.Sleep(1 * time.Second) }() //会有写的9个协程阻塞得不到释放 time.Sleep(10 * time.Second) }有缓冲的channel因为缓冲区空了,读操作阻塞
func channelTest() { var c = make(chan int, 8) for i := 0; i < 10; i++ { go func() { <- c fmt.Println("g1 receive succeed") time.Sleep(1 * time.Second) }() } go func() { c <- 1 fmt.Println("g2 send succeed") time.Sleep(1 * time.Second) }() //会有读的几个协程阻塞 time.Sleep(10 * time.Second) }
读阻塞都可以通过一个方法来解决,那就是让负责写入的goroutine去关闭channel。当channel被关闭时,任何尝试从中读取数据的goroutine都将停止阻塞并得到通知,因此不会再等待更多的数据。这就是为什么我们在编程时,需要始终保持警惕并在适当的时机关闭channel,以避免可能出现的阻塞问题。
在实际编程中,我们需要注意以上所有可能引起 goroutine 泄漏的情况,并采用适当的设计和编程技巧来避免这些问题。例如,可以设定 I/O 操作的超时时间,以防止 goroutine 永远阻塞;在使用互斥锁时,要确保在所有路径(包括错误路径和异常路径)上都能正确释放锁;在创建 goroutine 时,要限制其数量并确保它们能在完成任务后正确结束。
1.及时释放不再使用的内存: 在 Go 中,不能直接释放内存,这是由垃圾回收器自动完成的。通过将指针设置为 nil,可以消除对原对象的引用,使其成为垃圾回收的目标。
2.注意闭包的使用: 如果闭包长期持有外部变量的引用,可能会造成内存泄漏。在闭包中最小化所需状态,并在完成后尽快使引用失效。限制全局变量的使用: 全局变量在程序运行过程中一直占用内存,而且如果它们是 map、slice 或 channel 等可增长的数据结构,可能会导致内存无限增长。
3.注意 goroutine 的使用: 确保每个 goroutine 都有明确的退出条件,并且不会被无限期阻塞。可以使用 context 包或 select 语句来控制 goroutine 的生命周期。
4.使用 defer 确保资源被释放: 在打开文件、获取数据库连接等操作后,立即使用 defer 来确保在函数结束时这些资源被关闭或释放。
5.正确使用channel: 确保发送和接收操作是平衡的,避免 goroutine 因等待未来不会发生的事件而被阻塞。确保在所有数据发送完毕后,关闭 channel。
6.合理使用缓存: 如果你使用缓存来提高性能,使用某种策略(如 LRU)来淘汰旧的或不常用的项,以限制缓存的大小。