Go by Example: Issue: Goroutine Leak

Types of goroutine leak:

1) The forgotten sender

2) The abandoned receiver

In short: Never start a goroutine without knowing how it will stop

package to detect goroutine leak uber-go/goleak

package main
import (
    "context"
    "fmt"
    "time"
)

1) sender with no stopping mechanism it does not know when to stop

func forgottenSender() <-chan int {
    ch := make(chan int)
    go func() {
        var n int
        for {
            fmt.Println("1) blocked ", n)
            ch <- n
            n++
            time.Sleep(200 * time.Millisecond)
        }
    }()
    return ch
}

1-fixed) sender with cancel mechanism

func sender_with_cancel(ctx context.Context) <-chan int {
    ch := make(chan int)
    go func() {
        var n int
        for {
            select {

stop sending if canceled

            case <-ctx.Done():
                return
            case ch <- n:
                fmt.Println("1-fixed) blocked ", n)
                n++
                time.Sleep(200 * time.Millisecond)
            }
        }
    }()
    return ch
}

2) abandonedWorker

func abandonedWorker(ch chan int) {
    for i := range ch {
        fmt.Println("2) received", i)
    }
}

worker disaptcher should know when it stop produce
but he forgot.
fatal error: all goroutines are asleep - deadlock!

func getWork_deadlock() chan int {
    workCh := make(chan int)
    go func() {
        for i := 0; i < 5; i++ {
            workCh <- i
        }
    }()
    return workCh
}

2-fixed) close the channel

func getWork_fixed() chan int {
    workCh := make(chan int)
    go func() {
        for i := 0; i < 5; i++ {
            workCh <- i
        }
        close(workCh)
    }()
    return workCh
}

1) forgottenSender is left blocked

func main() {
    fmt.Println("1) forgottenSender is left blocked")
    for n := range forgottenSender() {
        fmt.Println("1) received", n)
        if n == 5 {
            break
        }
    }
    fmt.Println("")
    fmt.Println("1-fixed) fixed with context cancel")

1-fixed) fixed with context cancel

    ctx, cancel := context.WithCancel(context.Background())
    for n := range sender_with_cancel(ctx) {
        fmt.Println("1-fixed) received", n)
        if n == 5 {
            cancel() // remember to cancel
            break
        }
    }
    fmt.Println("2) abandonedWorker")

2) abandonedWorker fatal error: all goroutines are asleep - deadlock!

    //// workCh := getWork_deadlock()
    //// abandonedWorker(workCh)

2-fixed) abandonedWorker

    fmt.Println("2-fixed) abandonedWorker")
    workCh_ := getWork_fixed()
    abandonedWorker(workCh_)
}

ref: https://betterprogramming.pub/common-goroutine-leaks-that-you-should-avoid-fe12d12d6ee

$ go run issue-goroutine-leak.go
1) forgottenSender is left blocked
1) blocked  0
1) received 0
...
1) blocked  5
1) received 5
1-fixed) fixed with context cancel
1-fixed) blocked  0
1-fixed) received 0
1-fixed) blocked  1
1-fixed) received 1

we can see the leaked goroutine of example-1

1) blocked  6       // the forgottne sender
1-fixed) blocked  2
1-fixed) received 2
...
1-fixed) blocked  5
1-fixed) received 5
2) abandonedWorker
2-fixed) abandonedWorker
2) received 0
2) received 1
2) received 2
2) received 3

Next example: encoding/json.