Hone logo
Hone
Problems

Implementing Context Cancellation in Go

Go's context package is crucial for managing request-scoped values, cancellation signals, and deadlines across API boundaries. The context.WithCancel function is a fundamental tool for creating a new context that can be explicitly canceled. This challenge will test your understanding of how to create and utilize a cancellable context to control the lifecycle of goroutines.

Problem Description

Your task is to implement a function that simulates the behavior of context.WithCancel. This function should return two values: a new context.Context and a context.CancelFunc. When the context.CancelFunc is called, the returned context.Context should signal that it has been canceled.

You will need to create a custom cancelCtx type that embeds the context.Context interface and holds a channel that will be used to signal cancellation.

Key Requirements:

  1. myWithCancel(parent context.Context) function: This function will accept a parent context.Context and return a new context.Context and a context.CancelFunc.
  2. cancelCtx type: This struct should contain:
    • A reference to its parent context.Context.
    • A channel (e.g., chan struct{}) that will be closed when cancellation is triggered.
  3. myCancelFunc type: This will be a function type that calls the cancellation logic.
  4. Cancellation Signaling: When the returned context.CancelFunc is invoked, the returned context.Context must signal cancellation. This is typically done by closing the internal channel.
  5. Done() method: The returned context.Context must implement the Done() method. This method should return a channel that is closed if and when the context is canceled.
  6. Err() method: The returned context.Context must implement the Err() method. This method should return a non-nil error if Done() is closed, indicating the reason for cancellation (e.g., context.Canceled).
  7. Propagating Cancellation: If the parent context is canceled, the child context returned by myWithCancel should also be considered canceled.

Expected Behavior:

  • A context created with myWithCancel should initially not be canceled.
  • Calling the returned cancel function should immediately mark the context as canceled.
  • Any goroutine waiting on the Done() channel of the canceled context should be unblocked.
  • The Err() method should return context.Canceled after cancellation.
  • If the parent context is canceled before myCancelFunc is called, the child context should also signal cancellation.

Examples

Example 1: Basic Cancellation

package main

import (
	"context"
	"fmt"
	"time"
)

// Assume myWithCancel and myCancelFunc are defined elsewhere and work as expected.

func main() {
	parentCtx := context.Background()
	ctx, cancel := myWithCancel(parentCtx)
	defer cancel() // Ensure cancel is called even if an error occurs

	fmt.Println("Context created. Is it done?", ctx.Done() == nil) // Should print false

	go func() {
		select {
		case <-ctx.Done():
			fmt.Println("Goroutine: Context was canceled!")
			fmt.Println("Goroutine: Error:", ctx.Err()) // Should print context.Canceled
		case <-time.After(2 * time.Second):
			fmt.Println("Goroutine: Timeout reached (should not happen in this example)")
		}
	}()

	time.Sleep(50 * time.Millisecond) // Give the goroutine time to start
	fmt.Println("Main: Calling cancel...")
	cancel() // Trigger cancellation

	time.Sleep(100 * time.Millisecond) // Allow goroutine to react
	fmt.Println("Main: Finished.")
}

Expected Output:

Context created. Is it done? false
Main: Calling cancel...
Goroutine: Context was canceled!
Goroutine: Error: context canceled
Main: Finished.

Example 2: Parent Context Cancellation

package main

import (
	"context"
	"fmt"
	"time"
)

// Assume myWithCancel and myCancelFunc are defined elsewhere and work as expected.

func main() {
	parentCtx, parentCancel := context.WithCancel(context.Background())
	ctx, cancel := myWithCancel(parentCtx)
	defer cancel()

	go func() {
		time.Sleep(1 * time.Second)
		fmt.Println("Main: Parent context is being canceled...")
		parentCancel()
	}()

	fmt.Println("Main: Waiting for context to be done...")
	<-ctx.Done()
	fmt.Println("Main: Context is done.")
	fmt.Println("Main: Error:", ctx.Err()) // Should print context.Canceled
}

Expected Output:

Main: Waiting for context to be done...
Main: Parent context is being canceled...
Main: Context is done.
Main: Error: context canceled

Constraints

  • Your implementation of myWithCancel should not use the standard context.WithCancel function. You must build the cancellation mechanism from scratch.
  • The cancelCtx type must embed context.Context (or a similar mechanism) to delegate methods like Value().
  • The Done() channel must be a chan struct{}.
  • The Err() method should return context.Canceled upon cancellation.

Notes

  • Think about how to signal cancellation effectively using channels.
  • Consider how to handle the parent context's cancellation. The select statement is your friend here.
  • Remember to implement the Deadline() and Value() methods for your custom context, even if they just delegate to the parent, to fully satisfy the context.Context interface.
  • The context.Canceled error is a predefined error value in the context package.
  • The defer cancel() pattern is crucial for ensuring cleanup.
Loading editor...
go