# `go` — A Runtime-Agnostic Go-Style Concurrency Library (Design)

- Date: 2026-03-08
- Crate: `go` (edition 2024, requires Rust ≥ 1.85)
- Status: design approved, implementation plan pending

## 1. Goals and Positioning

`go` is a **runtime-agnostic** concurrency library that brings Go's concurrency
primitives (`go` spawn, channels, select, WaitGroup, timers) to Rust under a
single unified API. The user picks the underlying async runtime — **tokio /
async-std / smol** — at **compile time** via a Cargo feature, while the
application code stays unchanged.

Core value proposition: **the same code runs on any of the three runtimes;
switching the feature switches the runtime with zero API differences.**

### Non-Goals (explicitly out of scope for v1)

- No std synchronous-thread backend (async ecosystem only).
- No custom scheduler / M:N green threads.
- No runtime auto-detection or runtime-handle injection (compile-time feature
  selection only).
- No rendezvous (cap=0) unbuffered channel (see §4 narrowing point 1).
- `select!` does not support send branches (see §4 narrowing point 2).

## 2. Architecture

Approach: **a `Backend` trait with a compile-time-selected concrete type
(static dispatch).**

All backend differences are funneled behind one internal trait. Each backend is
a zero-sized type with an `impl Backend` and can be tested independently. The
public free functions are thin forwards to the selected backend, monomorphized
at compile time with zero runtime overhead. Runtime-agnostic primitives (chan /
WaitGroup / select / timeout) do not go through the trait and are implemented
once.

### Module Layout

```
src/
  lib.rs           // public re-exports, crate docs, feature guard
  backend/
    mod.rs         // Backend trait + cfg-selected `Active` type alias
    tokio.rs       // #[cfg(feature="rt-tokio")]     TokioBackend
    async_std.rs   // #[cfg(feature="rt-async-std")]  AsyncStdBackend
    smol.rs        // #[cfg(feature="rt-smol")]       SmolBackend
  spawn.rs         // go! macro + spawn() + JoinHandle + JoinError
  time.rs          // sleep() / timeout() / Elapsed
  chan.rs          // channel — runtime-agnostic (built on async-channel)
  waitgroup.rs     // WaitGroup — runtime-agnostic (atomic counter + event-listener)
  select.rs        // select! macro — runtime-agnostic
```

### Backend Seam

```rust
// backend/mod.rs
pub(crate) trait Backend {
    fn spawn<T>(fut: impl Future<Output = T> + Send + 'static) -> JoinHandle<T>
    where T: Send + 'static;

    fn sleep(dur: Duration) -> impl Future<Output = ()> + Send;
}

#[cfg(feature = "rt-tokio")]
pub(crate) type Active = tokio::TokioBackend;
#[cfg(feature = "rt-async-std")]
pub(crate) type Active = async_std::AsyncStdBackend;
#[cfg(feature = "rt-smol")]
pub(crate) type Active = smol::SmolBackend;
```

Only `spawn` and `sleep` are backend-specific; `timeout` is implemented
generically on top of `sleep` + race. Adding a backend = one new file
implementing `Backend` + one `Active` alias line.

## 3. Cargo Features

```toml
[features]
default = ["rt-tokio"]
rt-tokio     = ["dep:tokio"]
rt-async-std = ["dep:async-std"]
rt-smol      = ["dep:smol"]
```

- Default is `rt-tokio`. Switch backend:
  `go = { default-features = false, features = ["rt-async-std"] }`.
- A compile-time guard at the top of `lib.rs` emits `compile_error!` when the
  number of selected backends is **≠ 1** (covers both zero and ≥2).
- Runtime-agnostic primitives are unaffected by features and are always
  available.

### Dependencies

- Always: `async-channel`, `event-listener`, `futures-lite`, `pin-project-lite`.
- cfg-gated: `tokio` (features: rt + time + macros), `async-std`, `smol`.

## 4. Public API

### 4.1 spawn / go!

```rust
pub fn spawn<T: Send + 'static>(
    fut: impl Future<Output = T> + Send + 'static,
) -> JoinHandle<T>;
```

```rust
go!(async move { work().await });   // equivalent to spawn(...), returns JoinHandle
go!(some_async_fn(arg));            // spawns a future directly
```

- `JoinHandle<T>` wraps the backend's native handle via cfg fields (no Box, zero
  cost) and is `impl Future<Output = Result<T, JoinError>>`.
- **Drop semantics are unified to "detach / keep running"** (fire-and-forget, to
  match Go):
  - tokio / async-std: native handle detaches on drop, nothing to do.
  - smol: a `Task` cancels on drop by default, so it needs special handling —
    `JoinHandle` holds it as `Option<Task>`, `Drop` does `take().detach()`, and
    `await` does `take()` to poll it.

### 4.2 chan

```rust
go::chan::unbounded::<T>() -> (Sender<T>, Receiver<T>)
go::chan::bounded::<T>(cap: usize) -> (Sender<T>, Receiver<T>)   // cap ≥ 1
```

- Built on the runtime-agnostic `async-channel`. `send().await` / `recv().await`
  are both async.
- The channel closes when all Senders drop or via explicit `close()`.
- **Narrowing point 1:** no Go-style unbuffered (cap=0) rendezvous handoff
  (`async-channel` does not support cap=0). v1 provides only bounded(≥1) /
  unbounded; true cap=0 rendezvous is deferred.

### 4.3 select!

```rust
select! {
    v = rx1.recv() => { /* use v */ },
    _ = rx2.recv() => { ... },
    default => { ... },     // optional, non-blocking: runs if no branch is ready
}
```

- Runtime-agnostic, with a hand-written `poll` that fairly multiplexes several
  channel `recv` operations (the most complex piece in the library).
- **Narrowing point 2:** v1 supports only **recv branches + an optional
  default**; Go's **send branches** in select are significantly more complex and
  are deferred.

### 4.4 WaitGroup

```rust
let wg = WaitGroup::new();
wg.add(1);
go!(async move { /* ... */ wg.done(); });
wg.wait().await;
```

- Runtime-agnostic: `AtomicUsize` counter + `event-listener` notification.
- Ships with a convenience guard type that calls `done()` on drop (so `done()`
  cannot be forgotten).

### 4.5 time

```rust
pub async fn sleep(dur: Duration);
pub async fn timeout<T, F: Future<Output = T>>(dur: Duration, fut: F) -> Result<T, Elapsed>;
```

- `sleep` forwards to `Active` (the only backend-specific timer).
- `timeout` is implemented generically on top of `sleep` + race, so it is
  automatically runtime-agnostic.

## 5. Error Handling and Panic Semantics

- `JoinError { Panic(Box<dyn Any + Send>), Cancelled }`. `spawn` wraps the future
  in `catch_unwind` internally so the three backends' divergent panic behaviors
  (abort process / swallow / propagate) all converge to
  `Err(JoinError::Panic)`.
- channel: `SendError` (all receivers closed), `RecvError` (all senders closed
  and the buffer is empty), wrapping `async-channel`'s error types.
- `timeout` returns `Err(Elapsed)` on timeout.

## 6. Testing Strategy

- **Cross-runtime consistency (the crux):** one shared integration test suite
  compiled and run under each of the three features, proving the API is
  identical.
- Each backend impl is tested under its own feature:
  - `cargo test --no-default-features --features rt-tokio`
  - `cargo test --no-default-features --features rt-async-std`
  - `cargo test --no-default-features --features rt-smol`
- Runtime-agnostic units (chan / waitgroup / select / timeout) are tested once.
- All public API has doctests.
- CI runs a feature matrix over the three backends + a negative compile test for
  the feature guard (trybuild).

## 7. Unit Boundaries

| Unit | Responsibility | Depends on | Independently testable |
|---|---|---|---|
| `Backend` trait | Abstract spawn + sleep | — | yes (per backend) |
| `TokioBackend` / `AsyncStdBackend` / `SmolBackend` | per-runtime spawn/sleep impls | the runtime | yes |
| `spawn` / `JoinHandle` | forward + unify handle/panic semantics | Backend | yes |
| `chan` | runtime-agnostic channel | async-channel | yes |
| `select!` | recv multiplexing | chan | yes |
| `WaitGroup` | wait for a group of tasks | event-listener | yes |
| `time` | sleep / timeout | Backend (sleep only) | yes |

## 8. Future Work (beyond v1)

- Unbuffered (cap=0) rendezvous channel.
- Send branches in `select!`.
- std synchronous-thread backend.
- Runtime auto-detection / handle-injection mode.
