Why a Simple for‑range Over a Go Channel Can Leak Goroutines

The article explains how using a for‑range loop to read from an unclosed Go channel can cause a goroutine to block forever, demonstrates the issue with a minimal example, compares explicit receives with for‑range, and shows that closing the channel is the only reliable fix, even for buffered channels.

Golang Shines
Golang Shines
Golang Shines
Why a Simple for‑range Over a Go Channel Can Leak Goroutines

In Go, iterating over a channel with for range is a common pattern, but if the channel is never closed the loop blocks indefinitely, leading to a goroutine leak. The author reproduces this classic pitfall with a minimal program that launches a collector goroutine, sends three values, and never closes the channel, causing the collector to hang and the program to keep two goroutines alive.

Leakage version: running this code never exits the collector

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    go func() {
        for {
            time.Sleep(2 * time.Second)
            fmt.Printf("当前 goroutine 数量: %d
", runtime.NumGoroutine())
        }
    }()

    leakyFunction()
    select {}
}

func leakyFunction() {
    ch := make(chan int)
    go func() {
        fmt.Println("收集器启动,开始等待数据...")
        for v := range ch { // ⚠️ problem: ch is never closed
            fmt.Println("收到:", v)
        }
        fmt.Println("收集器退出") // never executed
    }()

    // send three values
    for i := 1; i <= 3; i++ {
        ch <- i
        fmt.Printf("发送: %d
", i)
    }
    fmt.Println("leakyFunction 返回")
}

Running the program prints the sent and received values, then repeatedly shows the goroutine count staying at 2, confirming that the collector goroutine never terminates.

Why does for range block?

The loop ends only when the channel is closed. Unlike explicit receives that know how many values to read, for range keeps reading until it sees a close signal.

Comparison of two approaches

// Approach A: explicit receives (does not block)
ch := make(chan int)
go func() {
    <-ch // receive 1, then exit
    <-ch // receive 2, then exit
    <-ch // receive 3, then exit
}()
ch <- 1
ch <- 2
ch <- 3
// goroutine exits naturally

// Approach B: for‑range (blocks)
ch := make(chan int)
go func() {
    for v := range ch { // waits forever until close(ch)
        _ = v
    }
}()
ch <- 1
ch <- 2
ch <- 3
// no close(ch), goroutine blocks forever

Explicit receives know the exact number of values and finish, while for range cannot determine when to stop without a close.

Fix version: just add one line

func fixedFunction() {
    ch := make(chan int)
    go func() {
        fmt.Println("收集器启动,开始等待数据...")
        for v := range ch {
            fmt.Println("收到:", v)
        }
        fmt.Println("收集器退出") // now executed
    }()

    for i := 1; i <= 3; i++ {
        ch <- i
        fmt.Printf("发送: %d
", i)
    }
    close(ch) // ✅ crucial fix: close the channel
    fmt.Println("fixedFunction 返回")
}

After closing the channel, the collector exits and the program terminates as expected.

Important reminder: buffered channels don’t solve the problem

Even a buffered channel (e.g., make(chan int, 100)) will still block the receiving for range after all buffered values are read if the channel is never closed.

Summary

When using for range to read a channel, you must ensure the channel is closed at some point.

This establishes a contract:

Sender : after sending all data, call close() on the channel.

Receiver : use for range and rely on the close() signal to exit.

If the channel is not closed, the for range loop becomes an endless block, leaking the goroutine. Go developers should habitually ask, “Where is this channel closed, when, and by whom?” to avoid hidden leaks.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

concurrencygogoroutinechannelleakfor-rangeclose
Golang Shines
Written by

Golang Shines

We share daily the latest Golang technical articles, practical resources, language news, tutorials, and real-world projects to help everyone learn and improve.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.