JV
GO

Java to Go

10 lessons

Progress0%
1Variables & Types2Classes → Structs3Interfaces4Exception Handling → Error Values5Threads → Goroutines6Slices and Collections7Packages and Modules8Testing9Go Standard Library10Context and Cancellation
All Mirror Courses
JV
GO
Context and Cancellation
MirrorLesson 10 of 10
Lesson 10

Context and Cancellation

Go's context.Context propagates deadlines and cancellation across goroutine trees — replacing Java's CompletableFuture.cancel(), ExecutorService.shutdownNow(), and request-scoped thread-locals.

Introduction

In this lesson, you'll learn about context and cancellation in Go. Coming from Java, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.

Mirror Card
JV
From Java:

In Java, you're familiar with go's context.context propagates deadlines and cancellation across goroutine trees — replacing java's completablefuture.cancel(), executorservice.shutdownnow(), and request-scoped thread-locals..

GO
In Go:

Go has its own approach to go's context.context propagates deadlines and cancellation across goroutine trees — replacing java's completablefuture.cancel(), executorservice.shutdownnow(), and request-scoped thread-locals., which we'll explore step by step.

The Go Way

Let's see how Go handles this concept. Here's a typical example:

GO
Go Example
package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

// Functions accept ctx as FIRST parameter by convention
func fetchData(ctx context.Context, url string) ([]byte, error) {
    // Create HTTP request bound to the context
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil { return nil, err }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        // ctx.Err() tells you WHY: DeadlineExceeded or Canceled
        return nil, fmt.Errorf("fetch failed: %w (ctx: %v)", err, ctx.Err())
    }
    defer resp.Body.Close()
    // ... read body ...
    return nil, nil
}

func processItems(ctx context.Context, items []int) error {
    for _, item := range items {
        // Check cancellation between items
        select {
        case <-ctx.Done():
            return ctx.Err()  // context.Canceled or DeadlineExceeded
        default:
        }
        process(item)
    }
    return nil
}

func main() {
    // Timeout: auto-cancelled after 5s
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()  // ALWAYS defer cancel to free resources

    if err := processItems(ctx, []int{1, 2, 3}); err != nil {
        fmt.Println("stopped:", err)
    }
}

Comparing to Java

Here's how you might have written similar code in Java:

JV
Java (What you know)
import java.util.concurrent.*;

public class Cancellation {
    CompletableFuture<String> fetchAsync(String url) {
        return CompletableFuture.supplyAsync(() -> {
            // No built-in way to check "am I cancelled?"
            return doFetch(url);
        });
    }

    void run() throws Exception {
        var future = fetchAsync("https://api.example.com/data");

        // Cancel after 2 seconds
        ScheduledExecutorService sched = Executors.newSingleThreadScheduledExecutor();
        sched.schedule(() -> future.cancel(true), 2, TimeUnit.SECONDS);

        try {
            String result = future.get(5, TimeUnit.SECONDS);
            System.out.println(result);
        } catch (CancellationException e) {
            System.err.println("Cancelled");
        } catch (TimeoutException e) {
            System.err.println("Timed out");
        }
    }
}
Mirror Card
JV
From Java:

You may be used to different syntax or behavior.

GO
In Go:

context.Context is passed explicitly as the first function parameter — Java stores it in thread-locals

Mirror Card
JV
From Java:

You may be used to different syntax or behavior.

GO
In Go:

context.WithTimeout/WithDeadline auto-cancel; context.WithCancel gives you a manual cancel() function

Mirror Card
JV
From Java:

You may be used to different syntax or behavior.

GO
In Go:

Always defer cancel() immediately after WithTimeout/WithCancel — prevents context leak

Mirror Card
JV
From Java:

You may be used to different syntax or behavior.

GO
In Go:

Check ctx.Done() channel in long loops; ctx.Err() reports reason (Canceled vs DeadlineExceeded)

Mirror Card
JV
From Java:

You may be used to different syntax or behavior.

GO
In Go:

http.NewRequestWithContext binds a context to an HTTP request — the request is aborted when ctx is done

Step-by-Step Breakdown

1. Creating Contexts

Start with context.Background() at the top level. Wrap with WithTimeout for deadlines or WithCancel for manual cancellation.

JV
Java
var future = executor.submit(task);
future.get(5, TimeUnit.SECONDS);
GO
Go
// Timeout:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Manual cancel:
ctx, cancel := context.WithCancel(context.Background())
go func() { time.Sleep(2*time.Second); cancel() }()

2. Pass ctx as First Argument

By convention, context is always the first parameter. This makes the cancellation chain explicit and visible in all function signatures.

JV
Java
CompletableFuture<String> fetchAsync(String url)
GO
Go
// Convention: ctx is always first
func fetchUser(ctx context.Context, id int) (*User, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := http.DefaultClient.Do(req)
    // ...
}

3. Checking Cancellation

In long-running loops, select on ctx.Done() to break early when cancelled or timed out.

JV
Java
if (Thread.currentThread().isInterrupted()) break;
GO
Go
for _, item := range items {
    select {
    case <-ctx.Done():
        return ctx.Err() // Canceled or DeadlineExceeded
    default:
    }
    process(item)
}

4. Context Values (Request-Scoped Data)

context.WithValue attaches request-scoped data (user ID, trace ID) without passing through every function signature. Use typed keys to avoid collisions.

JV
Java
// Java: ThreadLocal.set(userId); ThreadLocal.get()
GO
Go
type ctxKey string
const userIDKey ctxKey = "userID"

// In middleware:
ctx = context.WithValue(ctx, userIDKey, user.ID)

// In handler:
if uid, ok := ctx.Value(userIDKey).(int); ok {
    fmt.Println("user:", uid)
}
Rule of Thumb
Use context values only for request-scoped data like trace IDs and auth tokens — not for optional function parameters.

Common Mistakes

When coming from Java, developers often make these mistakes:

  • context.Context is passed explicitly as the first function parameter — Java stores it in thread-locals
  • context.WithTimeout/WithDeadline auto-cancel; context.WithCancel gives you a manual cancel() function
  • Always defer cancel() immediately after WithTimeout/WithCancel — prevents context leak
Common Pitfall
Don't assume Go works exactly like Java. While the concepts may be similar, the syntax and behavior can differ significantly.

Key Takeaways

  • context.Background() → WithTimeout/WithCancel wraps it; always defer cancel() to free resources
  • Pass ctx as the first parameter to every function that does I/O or long work
  • select { case <-ctx.Done(): return ctx.Err() } breaks loops on cancellation
  • http.NewRequestWithContext binds context to HTTP calls; context.WithValue for request-scoped metadata
Rule of Thumb
The best way to learn is by doing. Try rewriting some of your Java code in Go to practice these concepts.
PreviousFinish