Notessh2a
Concurrency

Channels

Overview

Channels in Go provide a way for goroutines to communicate by sending and receiving values.

  • Declare:
    ch := make(chan int)

    The channel type defines what kind of data it can carry. It can be any type, including named types.

  • Send:
    ch <- 10
  • Receive:
    value := <-ch
  • Close:
    close(ch)

    Channels usually do not need to be closed manually. Closing is only necessary when the receiver must be told there are no more values coming, such as to terminate a range loop.

Channel synchronization coordinates communication between goroutines. It ensures data is not lost and preserves the correct order.

  • Send operation: Blocks the current goroutine until another goroutine (the other side) is ready to receive.
  • Receive operation: Blocks the current goroutine until a value is available (from the other side).

Example:

func main() {
	ch := make(chan string)

	go expensiveFunc("Hello", ch)

	fmt.Println("Main")

	for range 4 {
		fmt.Println(<-ch)
	}

    fmt.Println("End")
}

func expensiveFunc(text string, ch chan string) {
	for i := range 4 {
		time.Sleep(500 * time.Millisecond)
		ch <- text + " " + fmt.Sprint(i)
	}
}

// Main
// Hello 0
// Hello 1
// Hello 2
// Hello 3
// End

No extra mechanism is required in main to wait for the goroutine. The <-ch operation blocks until a value is available. This behavior synchronizes main with expensiveFunc. Each loop iteration in main waits for a send from expensiveFunc. The loop is used only as an example, calling fmt.Println(<-ch) 4 times back to back would do the same.

In the example above, closing the channel is not required because main receives a fixed number of messages (4) before exiting. However, here is a modified version that requires the channel to be closed explicitly:

func main() {
	ch := make(chan string)

	go expensiveFunc("Hello", ch)

	fmt.Println("Main")

	for msg := range ch {
		fmt.Println(msg)
	}

	fmt.Println("Done.")
}

func expensiveFunc(text string, ch chan string) {
	defer close(ch)

	for i := range 4 {
		time.Sleep(500 * time.Millisecond)
		ch <- text + " " + fmt.Sprint(i)
	}
}

// Main
// Hello 0
// Hello 1
// Hello 2
// Hello 3
// Done.

The for msg := range ch { ... } syntax performs msg := <-ch internally, where blocking occurs.

range does not know how many values a channel will receive, it may be infinite. To stop the loop, close the channel to signal that no more values will be sent.

Buffered Channels

Buffered channels in Go have a defined capacity. They can hold a certain number of values before blocking.

  • A buffered channel blocks on send only when the buffer is full.
  • Receiving removes a value from the buffer. If the buffer is empty, it blocks until a value is available.
  • After closing, remaining buffered values can still be received.
  • Declare:
    ch := make(chan int, 3)

Example:

func main() {
	ch := make(chan string, 4)

	go myFunc("Hello", ch)

	fmt.Println("Main")

	for range 8 {
		time.Sleep(1000 * time.Millisecond)
		fmt.Println(<-ch)
	}

	fmt.Println("End")
}

func myFunc(text string, ch chan string) {
	for i := range 8 {
		ch <- text + " " + fmt.Sprint(i)
		fmt.Println("myFunc loop.", i)
	}

	close(ch)

	fmt.Println("myFunc End")
}

// Main
// myFunc loop. 0
// myFunc loop. 1
// myFunc loop. 2
// myFunc loop. 3
// Hello 0
// myFunc loop. 4
// Hello 1
// myFunc loop. 5
// Hello 2
// myFunc loop. 6
// Hello 3
// myFunc loop. 7
// myFunc End
// Hello 4
// Hello 5
// Hello 6
// Hello 7
// End

Channel Status

A receiver can check a channel's status using the second return value of a receive operation.

val, ok := <-ch

The second return value (ok) is a boolean and indicates whether the channel is open.

  • true: The channel is open, and values may still be received.
  • false: The channel is closed, and no more values will be received.
func main() {
	ch := make(chan int)

	go func(ch chan int) {
		ch <- 1
		ch <- 2
		close(ch)
	}(ch)

	for {
		val, ok := <-ch
		if !ok {
			fmt.Println("Channel is closed.")
			break
		}
		fmt.Println("Received:", val)
	}
}

// Received: 1
// Received: 2
// Channel is closed.
Sending on a closed channel causes a panic.

The select Statement

The select statement is a control structure for working with multiple channels simultaneously. It is similar to switch statement, but designed for channel operations.

  • select listens on multiple channels.
  • It executes the first ready case.
  • If multiple cases are ready, one is chosen randomly.
  • Without a default, it blocks until a case is ready.
  • With a default, it executes the default itself immediately if no cases are ready.
func main() {
	// Preparation:
	ch1 := make(chan int)
	ch2 := make(chan int)
	ch3 := make(chan int)

	go func(ch chan int) {
		time.Sleep(time.Second)
		ch <- 1
	}(ch1)

	go func(ch chan int) {
		time.Sleep(time.Second * 3)
		ch <- 2
	}(ch2)

	go func(ch chan int) {
		time.Sleep(time.Second * 2)
		ch <- 3
	}(ch3)

	time.Sleep(time.Second * 5)

	// Usage:
	select {
	case val1 := <-ch1:
		fmt.Println(val1)
	case val2 := <-ch2:
		fmt.Println(val2)
	case val3 := <-ch3:
		fmt.Println(val3)
	default:
		fmt.Println("No channels are ready.")
	}

	fmt.Println("Done.")
}

// 2
// Done.
The select statement is not a loop, it executes one case even if multiple are ready and then exits.

Read-Only & Write-Only Channels

Channels can be restricted to read-only or write-only. This defines communication direction and improves safety and clarity.

  • Read-Only (<-chan type): Can only receive values. Sending is not allowed.
  • Write-Only (chan<- type): Can only send values. Receiving is not allowed.
func sendData(ch chan<- int) { // ch is write-only
	ch <- 42
	// can't do: <-ch
	close(ch)
}

func receiveData(ch <-chan int) { // ch is read-only
	fmt.Println(<-ch)
	// can't do: ch <- 24
}

func main() {
	ch := make(chan int)

	go sendData(ch)
	receiveData(ch)
}

Concurrency Patterns

Done Channel

The Done Channel pattern signals cancellation or completion in goroutines. It uses a separate shared channel, which is closed to notify them to stop execution.

  • Without a Done Channel:

    func myFunc() {
    	for {
    		fmt.Println("Working...")
    	}
    }
    
    func main() {
    	go myFunc()
    
    	time.Sleep(1 * time.Hour)
    }

    The myFunc goroutine prints "Working..." continuously until the program exits.

  • With a Done Channel:

    func myFunc(doneCh <-chan struct{}) {
        for {
            select {
            case <-doneCh:
                return
            default:
                fmt.Println("Working...")
            }
        }
    }
    
    func main() {
        doneCh := make(chan struct{})
    
        go myFunc(doneCh)
    
        time.Sleep(5 * time.Second)
    
        close(doneCh)
    
        time.Sleep(1 * time.Hour)
    }

    The myFunc goroutine prints "Working..." for 5 seconds and then return, even though the main program continues to run for an hour.

    The struct{} type represents an empty struct in Go. It consumes zero bytes of memory, making it ideal for signaling and control purposes without causing any memory overhead.

Fan-In

The Fan-In pattern combines multiple input channels into a single output channel so one consumer can read from all sources.

func merge(ch1, ch2 <-chan int) <-chan int {
	out := make(chan int)

	go func() {
		for {
			select {
			case v := <-ch1:
				out <- v
			case v := <-ch2:
				out <- v
			}
		}
	}()

	return out
}

func myFunc(nums ...int) <-chan int {
	ch := make(chan int)

	// Business logic:
	go func() {
		for _, n := range nums {
			ch <- n
		}
	}()

	return ch
}

func main() {
	a := myFunc(1, 3, 5)
	b := myFunc(2, 4, 6)

	result := merge(a, b)

	for i := 0; i < 6; i++ {
		fmt.Println(<-result)
	}
}

// 1
// 3
// 2
// 5
// 4
// 6

Output order is not guaranteed because values are received as soon as they arrive from either channel.

On this page