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)
    Closing a closed channel causes a panic.

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).

Whoever comes first has to wait for the other one.

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. Closing is only necessary when the receiver must be told there are no more values coming, such as to terminate a range loop.

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.

Receiving from a closed unbuffered channel returns the zero-value immediately.

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 that indicates whether the receive operation obtained a value from an open channel.

  • true: A value was successfully received. The channel is still open.
  • false: The channel is closed and empty, so no more values can be received.

Example:

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

	go func(ch chan int) {
		ch <- 1
		ch <- 2
		close(ch)
		fmt.Println("Closed.")
	}(ch)

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

// Received: 1
// Received: 2
// Closed.
// Channel is closed. Received (zero-value): 0
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.

How it works:

  • 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.

Example:

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.
A select statement is not a loop, it executes one case even if multiple are ready and then exits.

In the example above, two goroutines remain blocked after select receives from one channel because the other two channels are never received. In a short-lived program like this, it is not noticeable because the program exits immediately afterward. However, in a long-running program, those blocked goroutines never terminate, causing a goroutine leak that also keeps their memory allocated.

This can be avoided using any cancellation mechanism.

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 chanType): Can only receive values. Sending is not allowed.
  • Write-Only (chan<- chanType): 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...")
                time.Sleep(time.Second)
            }
        }
    }
    
    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 returns, 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.

ch1 ----\
         \
          ---> merge ---> out
         /
ch2 ----/

Example:

func merge(chs ...<-chan string) <-chan string {
	out := make(chan string)

	var wg sync.WaitGroup
	wg.Add(len(chs))

	for _, ch := range chs {
		go func(ch <-chan string) {
			defer wg.Done()

			for v := range ch {
				out <- v
			}
		}(ch)
	}

	go func() {
		wg.Wait()
		close(out)
	}()

	return out
}

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

	go func() {
		defer close(ch)

		// Example business logic that writes output to the channel:
		for _, n := range nums {
			ch <- fmt.Sprintf("Hi %v!", n)
		}
	}()

	return ch
}

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

	for v := range merge(a, b) {
		fmt.Println(v)
	}
}

// Hi 1!
// Hi 2!
// Hi 4!
// Hi 3!
// Hi 6!
// Hi 5!

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

Context

In Go, a context is a mechanism for propagating cancellation signals, deadlines, timeouts, and request-scoped values through function calls and goroutines.

The hierarchy:

  • Contexts form an immutable tree structure (we do not modify a context, we add subtrees to a context).
  • Child contexts inherit cancellation, deadlines, and values from their parent.
  • Cancelling a parent cancels all of its descendants.
  • A child cannot cancel its parent or its siblings.

How it works:

  • WithCancel() (or one of the other variants) creates a child context and returns a cancellation function.
  • Every context provides a Done channel (ctx.Done()).
  • Calling cancel() (or when a timeout expires or a deadline is reached) closes the ctx.Done() channel.
  • Anyone depending on that context is immediately notified through the ctx.Done() channel.

ctx.Err() returns the reason the context ended (nil if it hasn't ended yet).

Context only signals cancellation by closing the ctx.Done() channel. It does not stop your code automatically. Every function or goroutine that accepts a context is responsible for checking ctx.Done() and implementing its own cancellation or cleanup logic.

  • Creating Contexts:

    The root context, the starting point for creating child contexts.

    ctx := context.Background()
    • Cancellation:

      ctx, cancel := context.WithCancel(ctx)
    • Timeout:

      ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    • Deadline:

      ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Second))
    • Value:

      ctx := context.WithValue(ctx, key, value)

Always call the returned cancel() function when a context is no longer needed. Calling cancel() multiple times is safe.

Examples:

  1. func myFunc(ctx context.Context) {
    	for {
    		select {
    		case <-ctx.Done():
    			return
    		default:
    			fmt.Println("Working...")
    			time.Sleep(time.Second)
    		}
    	}
    }
    
    func main() {
    	ctx := context.Background()
    	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    	defer cancel()
    
    	myFunc(ctx)
    
    	fmt.Println(ctx.Err())
    }
    
    // Working...
    // Working...
    // Working...
    // Working...
    // context deadline exceeded

On this page