I’d like to share a collection of ways to block Go programs from exiting, and there are still some brainy ways to do it.

In a program like the one below, the program exits as soon as it runs. The reason is that the main goroutine is finished executing, and the child goroutine exists, but it fails to block the main goroutine’s execution.

1
2
3
4
5
package main
import "net/http"
func main() {
    go http.ListenAndServe(":8080", nil)
}

I’ve compiled a list of 11 ways to see if you’ve thought of them already.

Method 1: Infinite Loop

This method is a silly method that will consume a CPU core idle.

1
2
3
4
5
6
7
package main
import "net/http"
func main() {
    go http.ListenAndServe(":8080", nil)
    for {
    }
}

Method 2: select{}

This is a common trick I use when writing examples, using a select statement without a case, with very few words, so I like to use it.

1
2
3
4
5
6
package main
import "net/http"
func main() {
    go http.ListenAndServe(":8080", nil)
    select {}
}

Method 3: Read or write data from a channel without cache

Both methods will be blocked.

1
2
3
4
5
6
7
8
package main
import "net/http"
func main() {
    go http.ListenAndServe(":8080", nil)
    c := make(chan struct{})
    // <-c
    c <- struct{}{}
}

Method 4: Read data from or write data to a nil channel

Both methods will be blocked.

1
2
3
4
5
6
7
8
package main
import "net/http"
func main() {
    go http.ListenAndServe(":8080", nil)
    var c chan struct{}
    // <-c
    c <- struct{}{}
}

Method 5: Mutual Exclusion Locks

Read and write locks are similar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main
import (
    "net/http"
    "sync"
)
func main() {
    go http.ListenAndServe(":8080", nil)
    var m sync.Mutex
    m.Lock()
    m.Lock()
}

Method 6: WaitGroup

Perhaps you have mastered the laws, many synchronization primitives are available, such as Cond, semaphore, we will not repeat the introduction.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main
import (
    "net/http"
    "sync"
)
func main() {
    go http.ListenAndServe(":8080", nil)
    var wg sync.WaitGroup
    wg.Add(1)
    wg.Wait()
}

Method 7: Blocking I/O

The simplest one is to use os.Stdin, but you can also use files, sockets, etc., as long as they are blocking I/O.

1
2
3
4
5
6
7
8
9
package main
import (
    "net/http"
    "os"
)
func main() {
    go http.ListenAndServe(":8080", nil)
    os.Stdin.Read(make([]byte, 1))
}

Method 8: Use signal.Notify

This is a trick I use a lot in my products to capture os.Signal and achieve a graceful exit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main
import (
    "net/http"
    "os"
    "os/signal"
)
func main() {
    go http.ListenAndServe(":8080", nil)
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)
    <-c
}

Method 9: Use the signal.Notify variant

Use Context, you can pass the context to the subgoroutine, Context is also suitable for use with select.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main
import (
    "context"
    "net/http"
    "os"
    "os/signal"
)
func main() {
    go http.ListenAndServe(":8080", nil)
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
    defer stop()
    <-ctx.Done()
}

Method 10: fmt.Scanln()

A more concise way of blocking I/O.

1
2
3
4
5
6
7
8
9
package main
import (
    "fmt"
    "net/http"
)
func main() {
    go http.ListenAndServe(":8080", nil)
    fmt.Scanln()
}

Method 11: runtime.Goexit()

The main program exits but the function does not return until the subgoroutine finishes.

1
2
3
4
5
6
7
8
9
package main
import (
    "net/http"
    "runtime"
)
func main() {
    go http.ListenAndServe(":8080", nil)
    runtime.Goexit()
}

Ref

  • https://colobu.com/2023/05/22/how-to-block-the-go-app-exit/