gofdocs

Concurrency

Tasks, channels, select, and cancellation — the current bootstrap concurrency baseline.

Concurrency is one of the reasons gof exists, so it should be learned carefully.

The current implementation already exposes a real baseline, but it is still a bootstrap baseline. That means you can write concurrent examples today, but you should not confuse that with a finished production runtime contract.

Step 1: go and await

fn square(x: int) -> int:
    return x * x

fn main() -> int:
    job: task[int] = go square(12)
    return await job

Current rules:

  • go currently spawns a top-level named function call
  • the result is a task value
  • await waits for that task value
  • await_result(task) joins any task as Result[T, RuntimeError] without changing the existing plain-await contract
  • await_result(task, token) lets the join-site return Result.Err(RuntimeError.Cancelled) if that token is cancelled before the task completes
  • when the spawned function explicitly declares -> Result[..., RuntimeError], task-boundary evaluator failures now come back as Result.Err(RuntimeError.TaskFailed(...)) or Result.Err(RuntimeError.TaskPanicked(...)) instead of tearing down the caller immediately

If you want recoverable joins for a plain task[T] today, use await_result(task):

fn lucky() -> int:
    return 7

fn main() -> Result[int, RuntimeError]:
    job: task[int] = go lucky()
    return await_result(job)
fn slow() -> int:
    sleep(25)
    return 7

fn main() -> Result[int, RuntimeError]:
    job: task[int] = go slow()
    return await_result(job, timeout_token(0))

The mental model is:

  • go creates concurrent work
  • await joins that work back into the current flow

Step 2: channels and explicit lifecycle

fn main() -> Result[int, RuntimeError]:
    ch: channel[int] = channel()
    send(ch, 4)?
    return recv(ch)

Current channel baseline:

  • channel[T] is a real parameterized builtin type annotation
  • channel() creates a bootstrap channel
  • channel(0) creates a rendezvous channel baseline
  • channel(n) for n > 0 creates a bounded channel baseline
  • close(channel) closes it explicitly
  • send(channel, value) returns Result[unit, RuntimeError]
  • recv(channel) returns Result[T, RuntimeError]

With channel lifecycle, the bootstrap runtime already distinguishes:

  • successful send/receive
  • closed channel
  • cancelled wait

Current capacity rules:

  • channel() keeps the existing unbounded queue-backed bootstrap path
  • channel(0) blocks send(...) until a receiver takes the value
  • channel(n) with n > 0 blocks send(...) when the buffer is full until a receiver frees space

Step 3: select

fn main() -> Result[int, RuntimeError]:
    left: channel[int] = channel()
    right: channel[int] = channel()
    send(right, 8)?
    select:
        value = recv(left):
            return Result.Ok(value? + 0)
        value = recv(right):
            return Result.Ok(value? + 1)

Current select contract:

  • receive arms, send arms, and a single default arm are supported
  • receive arms must be recv(channel):, value = recv(channel):, recv(channel, token):, or value = recv(channel, token):
  • send arms must be send(channel, value):, value = send(channel, value):, send(channel, value, token):, or value = send(channel, value, token):
  • default: executes immediately when no send/receive arm is ready during the current polling pass
  • the bootstrap runtime prepares each send/receive operation once at select-entry, then blocks on channel/token wakeups between polling passes until one resolves to Result.Ok(...) or Result.Err(...)
  • when multiple send/receive arms are already ready, the bootstrap runtime rotates the ready-arm start index in a deterministic round-robin baseline so the first source arm does not always win

Step 4: cancellation

fn main() -> bool:
    token = timeout_token(0)
    manual = cancel_token()
    cancel_after(manual, 0)
    return is_cancelled(token) and is_cancelled(manual)

Current cancellation baseline:

  • cancel_token() creates a cooperative token
  • cancel(token) marks the token as cancelled
  • is_cancelled(token) reports the current state
  • timeout_token(milliseconds) creates a token that cancels itself after a non-negative delay
  • cancel_after(token, milliseconds) schedules cancellation for an existing token after a non-negative delay
  • send(..., token), recv(..., token), and await_result(task, token) wake promptly when that token flips instead of waiting for fixed timeout polling ticks

What is still missing

This is a real concurrency baseline, but not the final story.

Still missing:

  • production-grade fairness guarantees beyond the current round-robin ready-arm baseline
  • automatic task panic and boundary-failure preservation for plain await task joins without opting into await_result(task)
  • deadline/context propagation beyond timeout-backed token baselines
  • production scheduler hardening

The right way to read current concurrency docs

Think of current gof concurrency as:

  • real enough to learn from
  • real enough to test
  • not yet strong enough to promise production-grade semantics

That distinction matters because concurrency is one of the easiest places for a language project to oversell itself.