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:
myWithCancel(parent context.Context)function: This function will accept a parentcontext.Contextand return a newcontext.Contextand acontext.CancelFunc.cancelCtxtype: 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.
- A reference to its
myCancelFunctype: This will be a function type that calls the cancellation logic.- Cancellation Signaling: When the returned
context.CancelFuncis invoked, the returnedcontext.Contextmust signal cancellation. This is typically done by closing the internal channel. Done()method: The returnedcontext.Contextmust implement theDone()method. This method should return a channel that is closed if and when the context is canceled.Err()method: The returnedcontext.Contextmust implement theErr()method. This method should return a non-nil error ifDone()is closed, indicating the reason for cancellation (e.g.,context.Canceled).- Propagating Cancellation: If the parent context is canceled, the child context returned by
myWithCancelshould also be considered canceled.
Expected Behavior:
- A context created with
myWithCancelshould initially not be canceled. - Calling the returned
cancelfunction 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 returncontext.Canceledafter cancellation. - If the parent context is canceled before
myCancelFuncis 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
myWithCancelshould not use the standardcontext.WithCancelfunction. You must build the cancellation mechanism from scratch. - The
cancelCtxtype must embedcontext.Context(or a similar mechanism) to delegate methods likeValue(). - The
Done()channel must be achan struct{}. - The
Err()method should returncontext.Canceledupon cancellation.
Notes
- Think about how to signal cancellation effectively using channels.
- Consider how to handle the parent context's cancellation. The
selectstatement is your friend here. - Remember to implement the
Deadline()andValue()methods for your custom context, even if they just delegate to the parent, to fully satisfy thecontext.Contextinterface. - The
context.Cancelederror is a predefined error value in thecontextpackage. - The
defer cancel()pattern is crucial for ensuring cleanup.