Essential Go Channels and select edit forum

Channel idioms

Channels exhibit the following properties:

Send to a nil channel blocks forever

package main

func main() {
        var ch chan bool
        ch <- true // deadlocks because ch is nil
}

Uninitialized value of a channel is nil so the above program blocks forever.

There's no valid use case for that so it's always a bug. Don't do it.

Receive from a nil channel blocks forever

package main

import "fmt"

func main() {
        var ch chan bool
        fmt.Printf("Value received from ch is: %v\n", <-ch) // deadlock because c is nil
}

Similarly, receive from nil channel blocks forever and it's always a bug. Don't do it.

Send to a closed channel panics

package main

import (
	"fmt"
	"time"
)

func main() {
	var ch = make(chan int, 100)
	go func() {
		ch <- 1
		time.Sleep(time.Second)
		close(ch)
		ch <- 1
	}()
	for i := range ch {
		fmt.Printf("i: %d\n", i)
	}
}

Output:

i: 1
panic: send on closed channel

goroutine 5 [running]:
main.main.func1(0x452000, 0xc99)
	/tmp/sandbox307976305/main.go:14 +0xa0
created by main.main
	/tmp/sandbox307976305/main.go:10 +0x60

You should architecture your programs so that one sender controls the lifetime of a channel.

This rule emphasizes that: if there's only one channel sender then there's no problem making sure that you never write to a closed channel.

If you have multiple senders then it becomes hard: if one sender closes a channel, how other senders are supposed to not crash?

Instead of trying to find solution to the above problem, re-architect your code so that there's only one sender that controls the lifetime of a channel.

Receive from a closed channel returns the zero value immediately

package main

import "fmt"

func main() {
	// show
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	close(ch)
	for i := 0; i < 3; i++ {
		fmt.Printf("%d ", <-ch) // -> 1 2 0
	}
	// show end
}

This is not ideal: we've read a value from a channel that was never sent.

It's easy to remedy:

package main

import "fmt"

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	close(ch)
	// show
	for {
		v, ok := <-ch
		if !ok {
			break
		}
		fmt.Printf("%d ", v) // -> 1 2
	}
	// show end
}

And even better and more idiomatically:

package main

import "fmt"

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	close(ch)
	// show
	for v := range ch {
		fmt.Printf("%d ", v) // -> 1 2
	}
	// show end
}

Closing channel to signal a goroutine has finished

Sometimes you need to wait until a goroutine has finished.

The fact that a receive from a closed channel returns immediately but otherwise blocks can be used to coordinate between goroutines by a shared "done" channel.

One goroutine closes the channel at the end.

The other goroutine can either wait indefinitely until that happens with a receive: chDone <-

In more complex scenarios it can receive from multiple channels with select statement. For example, to limit a waiting time:

select {
	case <- chDone:
		// goroutine has finished
	case <- time.After(time.Second *5):
		// goroutine didn't finish but we don't want to wait
		// more than 5 seconds
}

To check if channel is closed (i.e. goroutine has finished) without waiting:

package main

import "fmt"

// show
func checkState(ch chan struct{}) {
	select {
	case <-ch:
		fmt.Printf("channel is closed\n")
	default:
		fmt.Printf("channel is not closed\n")
	}
}
// show end

func main() {
	// show
	ch := make(chan struct{})
	checkState(ch)
	close(ch)
	checkState(ch)
	// show end
}

This technique is used in context.Done() channel.

  ↑ ↓ to navigate     ↵ to select     Esc to close