Internals of the context package
Who am I?
Damiano Petrungaro
Who am I?
Staff Engineer @Odin
Who am I?
Milan, Italy
Who am I?
Manga and anime
The context package
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
package context
func Background() Context { ... }
func TODO() Context { ... }
func WithValue(parent Context, key, val any) Context { ... }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { ... }
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { ... }
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { ... }
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) { ... }
func Cause(c Context) error { ... }
Using the context pkg
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
package main
import "context"
func main() {
ctx := context.Background()
// ...
}
Background and TODO
Internals
package main
import "context"
func main() {
ctx := context.Background()
// ...
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L205
var (
background = new(emptyCtx)
)
func Background() Context {
return background
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L177
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
Background
Internals
package main
import "context"
func main() {
ctx := context.TODO()
// ...
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L205
var (
todo = new(emptyCtx)
)
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L177
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
Internals
TODO
Using the context pkg
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
package main
import "context"
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
ctx.Value("two")
ctx.Value("one")
ctx.Value("zero")
}
Value
Internals
package main
import "context"
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
ctx.Value("two")
ctx.Value("one")
ctx.Value("zero")
}
Value
Internals
emptyCtx
Value
Visualizing the internals
package main
import "context"
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
ctx.Value("two")
ctx.Value("one")
ctx.Value("zero")
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L583
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L598
type valueCtx struct {
Context
key, val any
}
Value
Internals
emptyCtx
valueCtx
{key:"one",val:1}
Value
Visualizing the internals
package main
import "context"
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
ctx.Value("two")
ctx.Value("one")
ctx.Value("zero")
}
Value
Internals
emptyCtx
valueCtx
{key:"one",val:1}
valueCtx
{key:"two",val:2}
Value
Visualizing the internals
package main
import "context"
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
ctx.Value("two")
ctx.Value("one")
ctx.Value("zero")
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L598
type valueCtx struct {
Context
key, val any
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L622
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
// omitted code that behaves as previous line `c = ctx.Context`
case *emptyCtx:
return nil
default:
return c.Value(key)
}
}
}
Value
Internals
emptyCtx
valueCtx
{key:"one",val:1}
valueCtx
{key:"two",val:2}
✅
🔍
Value("two")
Value
Visualizing the internals
package main
import "context"
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
ctx.Value("two")
ctx.Value("one")
ctx.Value("zero")
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L598
type valueCtx struct {
Context
key, val any
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L622
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
// omitted code that behaves as previous line `c = ctx.Context`
case *emptyCtx:
return nil
default:
return c.Value(key)
}
}
}
Value
Internals
emptyCtx
valueCtx
{key:"one",val:1}
✅
Value("one")
🔍
❌
valueCtx
{key:"two",val:2}
🔍
Value("one")
Value
Visualizing the internals
package main
import "context"
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
ctx.Value("two")
ctx.Value("one")
ctx.Value("zero")
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L598
type valueCtx struct {
Context
key, val any
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L622
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
// omitted code that behaves as previous line `c = ctx.Context`
case *emptyCtx:
return nil
default:
return c.Value(key)
}
}
}
Value
Internals
emptyCtx
valueCtx
{key:"one",val:1}
❌
Value("zero")
🔍
❌
valueCtx
{key:"two",val:2}
🔍
Value("zero")
❌
Value("zero")
🔍
Value
Visualizing the internals
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
Using the context pkg
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(/* TODO */, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
Cancel
Internals
Cancel
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(/* TODO */, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
Internals
emptyCtx
Cancel
Visualizing the internals
Cancel
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(/* TODO */, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
Internals
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L238
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L271
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, c)
return c
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L296
func newCancelCtx(parent Context) *cancelCtx {
return &cancelCtx{Context: parent}
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L304
func propagateCancel(parent Context, child canceler) {
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L318
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
// ...
// Here the child is stored in the parent, initialising a map if this is the first one.
// The map is used in favor of a slice for performance purposes mainly as for searching a single child will have an impact of O(1).
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
p.mu.Unlock()
}
// ...
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L396
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
emptyCtx
cancelCtx
Cancel
Visualizing the internals
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(/* TODO */, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(/* TODO */, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
Cancel
Internals
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L238
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L271
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, c)
return c
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L296
func newCancelCtx(parent Context) *cancelCtx {
return &cancelCtx{Context: parent}
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L304
func propagateCancel(parent Context, child canceler) {
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L318
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
// ...
// Here the child is stored in the parent, initialising a map if this is the first one.
// The map is used in favor of a slice for performance purposes mainly as for searching a single child will have an impact of O(1).
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
p.mu.Unlock()
}
// ...
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L396
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
emptyCtx
cancelCtx
func work
cancelCtx
cancelCtx
func main
Cancel
Visualizing the internals
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(/* TODO */, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(/* TODO */, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
Cancel
Internals
emptyCtx
cancelCtx
func work
cancelCtx
cancelCtx
func slowFn
valueCtx
valueCtx
{key:"one",val:1}
{key:"one",val:1}
func main
Cancel
Visualizing the internals
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(/* TODO */, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
Cancel
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(/* TODO */, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
Internals
emptyCtx
cancelCtx
func work
cancelCtx
cancelCtx
func slowFn
valueCtx
valueCtx
{key:"one",val:1}
{key:"one",val:1}
valueCtx
{key:"two",val:2}
valueCtx
{key:"two",val:2}
func main
Cancel
Visualizing the internals
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(/* TODO */, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
Cancel
Internals
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L396
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L598
type valueCtx struct {
Context
key, val any
}
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(4 * time.Second, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
Cancel
Internals
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L238
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L453
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
// ...
// Note: this code section is simplified from the original one for reading purposes.
// TLDR:
// Sets the reason why the context was canceled.
// If the error is set and the context is marked again as canceled it will not take any action,
// in this case, is set as `context.Canceled`
// defined here https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L163
c.err = err
// ...
// If the context has a channel in the `done` field
// it means that a goroutine _may_ be interested in knowing
// the context is now done, so the channel is closed.
d, _ := c.done.Load().(chan struct{})
close(d)
// ...
// Cancels all the registered children.
for child := range c.children {
child.cancel(false, err, cause)
}
c.children = nil
// ...
if removeFromParent {
removeChild(c.Context, c)
}
}
emptyCtx
cancelCtx
func work
cancelCtx
cancelCtx
func slowFn
valueCtx
valueCtx
{key:"one",val:1}
{key:"one",val:1}
valueCtx
{key:"two",val:2}
valueCtx
{key:"two",val:2}
func main
{err: Cancelled}
{err: Cancelled}
defer cancel()
defer cancel()
defer cancel()
{err: Cancelled}
Cancel
Visualizing the internals
Cancel
Internals
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(2 * time.Second, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L238
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L453
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
// ...
// Note: this code section is simplified from the original one for reading purposes.
// TLDR:
// Sets the reason why the context was canceled.
// If the error is set and the context is marked again as canceled it will not take any action,
// in this case, is set as `context.Canceled`
// defined here https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L163
c.err = err
// ...
// If the context has a channel in the `done` field
// it means that a goroutine _may_ be interested in knowing
// the context is now done, so the channel is closed.
d, _ := c.done.Load().(chan struct{})
close(d)
// ...
// Cancels all the registered children.
for child := range c.children {
child.cancel(false, err, cause)
}
c.children = nil
// ...
if removeFromParent {
removeChild(c.Context, c)
}
}
package main
import (
"context"
"log"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
work(ctx, i)
wg.Done()
}(i)
}
time.AfterFunc(2 * time.Second, func() { cancel() })
wg.Wait()
log.Println("completed")
}
func work(ctx context.Context, i int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
slowFn(ctx, i)
}
func slowFn(ctx context.Context, i int) {
ctx = context.WithValue(ctx, "one", 1)
ctx = context.WithValue(ctx, "two", 2)
log.Printf("slow function %d started\n", i)
select {
case <-time.Tick(3 * time.Second):
log.Printf("slow function %d finished\n", i)
case <-ctx.Done():
log.Printf("slow function %d too slow: %s\n", i, ctx.Err())
}
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L396
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
emptyCtx
cancelCtx
func work
cancelCtx
cancelCtx
func slowFn
valueCtx
valueCtx
{key:"one",val:1}
{key:"one",val:1}
valueCtx
{key:"two",val:2}
valueCtx
{key:"two",val:2}
func main
{err: Cancelled}
{err: Cancelled}
cancel()
{err: Cancelled}
{key:"one",val:1,err:Cancelled}
{key:"two",val:2,err:Cancelled}
{key:"one",val:1,err:Cancelled}
{key:"two",val:2,err:Cancelled}
Cancel
Visualizing the internals
Go 1.20 update
package context
func Background() Context { ... }
func TODO() Context { ... }
func WithValue(parent Context, key, val any) Context { ... }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { ... }
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { ... }
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { ... }
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) { ... }
func Cause(c Context) error { ... }
WithCancelCause
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L266
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L453
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
// ...
if cause == nil {
cause = err
}
c.err = err
c.cause = cause
// ...
}
Cause
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L286
func Cause(c Context) error {
if cc, ok := c.Value(&cancelCtxKey).(*cancelCtx); ok {
cc.mu.Lock()
defer cc.mu.Unlock()
return cc.cause
}
return nil
}
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
Using the context pkg
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
Timer
Internals
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
Timer
Internals
Timer
emptyCtx
Visualizing the internals
Timer
Internals
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L494
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// If the parent deadline is prior to the new one we're creating
// it returns a cancelCtx since we don't need the overhead of a `timerCtx`.
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
// Creates a timerCtx.
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// Ensures the timerCtx will be canceled when the parent is marked as such.
propagateCancel(parent, c)
// If the deadline is already passed (maybe you passed a time.Time in the past?)
// it immediately cancels timerCtx and returns a CancelFunc that will do nothing when invoked.
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, nil) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
// Creates a timer that will mark the context done as `DeadlineExceeded` when the deadline is reached.
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, nil)
})
// Returns the timerCtx and a CancelFunc that can be called before the timer is triggered.
return c, func() { c.cancel(true, Canceled, nil) }
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L567
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L525
type timerCtx struct {
*cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L542
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
Timer
emptyCtx
timerCtx
{deadline: now+7s}
Visualizing the internals
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
Timer
Internals
Timer
emptyCtx
timerCtx
func work
timerCtx
func main
{deadline: now+1s}
{deadline: now+7s}
Visualizing the internals
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
Timer
Internals
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L494
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// If the parent deadline is prior to the new one we're creating
// it returns a cancelCtx since we don't need the overhead of a `timerCtx`.
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
// Creates a timerCtx.
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// Ensures the timerCtx will be canceled when the parent is marked as such.
propagateCancel(parent, c)
// If the deadline is already passed (maybe you passed a time.Time in the past?)
// it immediately cancels timerCtx and returns a CancelFunc that will do nothing when invoked.
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
// Creates a timer that will mark the context done as `DeadlineExceeded` when the deadline is reached.
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
// Returns the timerCtx and a CancelFunc that can be called before the timer is triggered.
return c, func() { c.cancel(true, Canceled) }
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L567
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L525
type timerCtx struct {
*cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
Timer
emptyCtx
timerCtx
func work
timerCtx
func main
cancelCtx
{deadline: now+1s}
{deadline: now+7s}
Visualizing the internals
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
Timer
Internals
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L494
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// If the parent deadline is prior to the new one we're creating
// it returns a cancelCtx since we don't need the overhead of a `timerCtx`.
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
// Creates a timerCtx.
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// Ensures the timerCtx will be canceled when the parent is marked as such.
propagateCancel(parent, c)
// If the deadline is already passed (maybe you passed a time.Time in the past?)
// it immediately cancels timerCtx and returns a CancelFunc that will do nothing when invoked.
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, nil) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
// Creates a timer that will mark the context done as `DeadlineExceeded` when the deadline is reached.
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, nil)
})
// Returns the timerCtx and a CancelFunc that can be called before the timer is triggered.
return c, func() { c.cancel(true, Canceled, nil) }
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L567
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L525
type timerCtx struct {
*cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L542
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
Timer
emptyCtx
timerCtx
func work
timerCtx
timerCtx
func main
cancelCtx
{deadline: now+1s}
{deadline: now-1s}
{deadline: now+7s}
{err:DeadlineExceeded}
Visualizing the internals
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
Timer
Internals
Timer
emptyCtx
timerCtx
func work
timerCtx
timerCtx
func main
cancelCtx
valueCtx
{key:"id",val:1}
valueCtx
{key:"id",val:2}
{deadline: now+1s}
{deadline: now-1s}
{deadline: now+7s}
valueCtx
{key:"id",val:3}
{err:DeadlineExceeded}
Visualizing the internals
func slowTask
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
Timer
Internals
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L494
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// If the parent deadline is prior to the new one we're creating
// it returns a cancelCtx since we don't need the overhead of a `timerCtx`.
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
// Creates a timerCtx.
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// Ensures the timerCtx will be canceled when the parent is marked as such.
propagateCancel(parent, c)
// If the deadline is already passed (maybe you passed a time.Time in the past?)
// it immediately cancels timerCtx and returns a CancelFunc that will do nothing when invoked.
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, nil) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
// Creates a timer that will mark the context done as `DeadlineExceeded` when the deadline is reached.
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, nil)
})
// Returns the timerCtx and a CancelFunc that can be called before the timer is triggered.
return c, func() { c.cancel(true, Canceled, nil) }
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L567
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L525
type timerCtx struct {
*cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L542
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L494
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// If the parent deadline is prior to the new one we're creating
// it returns a cancelCtx since we don't need the overhead of a `timerCtx`.
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
// Creates a timerCtx.
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// Ensures the timerCtx will be canceled when the parent is marked as such.
propagateCancel(parent, c)
// If the deadline is already passed (maybe you passed a time.Time in the past?)
// it immediately cancels timerCtx and returns a CancelFunc that will do nothing when invoked.
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, nil) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
// Creates a timer that will mark the context done as `DeadlineExceeded` when the deadline is reached.
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, nil)
})
// Returns the timerCtx and a CancelFunc that can be called before the timer is triggered.
return c, func() { c.cancel(true, Canceled, nil) }
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L567
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L525
type timerCtx struct {
*cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L542
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
Timer
emptyCtx
timerCtx
func work
timerCtx
timerCtx
func main
cancelCtx
valueCtx
{key:"id",val:1}
valueCtx
{key:"id",val:2}
{deadline: now+1s}
{deadline: now-1s}
{deadline: now+7s}
valueCtx
{key:"id",val:3}
{err:DeadlineExceeded}
defer cancel()
Visualizing the internals
func slowTask
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
Timer
Internals
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L494
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// If the parent deadline is prior to the new one we're creating
// it returns a cancelCtx since we don't need the overhead of a `timerCtx`.
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
// Creates a timerCtx.
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// Ensures the timerCtx will be canceled when the parent is marked as such.
propagateCancel(parent, c)
// If the deadline is already passed (maybe you passed a time.Time in the past?)
// it immediately cancels timerCtx and returns a CancelFunc that will do nothing when invoked.
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, nil) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
// Creates a timer that will mark the context done as `DeadlineExceeded` when the deadline is reached.
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, nil)
})
// Returns the timerCtx and a CancelFunc that can be called before the timer is triggered.
return c, func() { c.cancel(true, Canceled, nil) }
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L567
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L525
type timerCtx struct {
*cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L542
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
Timer
emptyCtx
timerCtx
func work
timerCtx
timerCtx
func main
cancelCtx
valueCtx
{key:"id",val:1}
valueCtx
{key:"id",val:2}
{deadline: now+1s}
{deadline: now-1s}
{deadline: now+7s}
valueCtx
{key:"id",val:3}
{err:DeadlineExceeded}
{err:Cancelled}
cancel()
Visualizing the internals
func slowTask
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
Timer
Internals
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L494
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// If the parent deadline is prior to the new one we're creating
// it returns a cancelCtx since we don't need the overhead of a `timerCtx`.
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
// Creates a timerCtx.
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// Ensures the timerCtx will be canceled when the parent is marked as such.
propagateCancel(parent, c)
// If the deadline is already passed (maybe you passed a time.Time in the past?)
// it immediately cancels timerCtx and returns a CancelFunc that will do nothing when invoked.
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, nil) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
// Creates a timer that will mark the context done as `DeadlineExceeded` when the deadline is reached.
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, nil)
})
// Returns the timerCtx and a CancelFunc that can be called before the timer is triggered.
return c, func() { c.cancel(true, Canceled, nil) }
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L567
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L525
type timerCtx struct {
*cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L542
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
Timer
emptyCtx
timerCtx
func work
timerCtx
timerCtx
func main
cancelCtx
func slowTask
valueCtx
{key:"id",val:1}
valueCtx
{key:"id",val:2}
{deadline: now+1s}
{deadline: now-1s}
{deadline: now+7s}
valueCtx
{key:"id",val:3}
{err:DeadlineExceeded}
{err:Cancelled}
time AfterFunc
{err:DeadlineExceeded}
{err:DeadlineExceeded}
Visualizing the internals
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for i := 0; i < 3; i++ {
wg.Add(1)
go work(ctx, wg, i)
}
wg.Wait()
fmt.Println("done")
}
func work(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
switch id {
case 0:
ctx, cancel := context.WithTimeout(ctx, time.Second)
time.AfterFunc(500*time.Millisecond, func() { cancel() })
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 1 second", id))
case 1:
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of 10 second", id))
case 2:
ctx, cancel := context.WithTimeout(ctx, -1*time.Second)
defer cancel()
slowTask(ctx, id, fmt.Sprintf("worker %d had a timeout of -1 second", id))
}
}
func slowTask(ctx context.Context, id int, prefix string) {
ctx = context.WithValue(ctx, "id", id)
fmt.Printf("%s: started\n", prefix)
select {
case <-time.Tick(15 * time.Second):
fmt.Printf("%s: finished\n", prefix)
case <-ctx.Done():
fmt.Printf("%s: too slow... returning: %s\n", prefix, ctx.Err())
}
}
Timer
Internals
package context
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L525
type timerCtx struct {
*cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
// https://github.com/golang/go/blob/go1.20.5/src/context/context.go#L453
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
// ...
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
// ...
}
Recap
Contacts
- Twitter: @damiano_dev
- LinkedIn: /in/damianopetrungaro
- Blog: damianopetrungaro.com