Libgoc
A Go-style CSP concurrency runtime for C: threadpools, stackful coroutines, channels, select, async I/O, and garbage collection in one coherent API.
Install / Use
/learn @divs1210/LibgocREADME

libgoc
A Go-style CSP concurrency runtime for C: threadpools, stackful coroutines, channels, select, async I/O, and garbage collection in one coherent API.
libgoc is a runtime library for C programs that want Go-style CSP concurrency backed by a managed memory environment. It is written in plain C for maximum reach and portability.
The library provides stackful coroutines ("fibers"), channels, a select primitive (goc_alts), timeout channels, a managed thread pool, and optional runtime telemetry (goc_stats). Boehm GC is a required dependency and is linked automatically.
libgoc is built for:
- C developers who want a Go-style concurrency runtime without switching to Go.
- Language implementors targeting C/C++ as a compilation target, or writing multithreaded interpreters.
Dependencies:
| | |
|---|---|
| minicoro | fiber suspend/resume (cross-platform; POSIX and Windows) |
| libuv | event loop, threads, timers, cross-thread wakeup |
| Boehm GC | garbage collection |
| picohttpparser | HTTP/1.1 request parser (vendored MIT); used by goc_http; compiled in by default; disable with -DLIBGOC_SERVER=OFF |
Pre-built static libraries are available on the Releases page:
- Linux (x86-64)
- macOS (arm64)
- Windows (x86-64)
Helper libraries:
Also see:
Table of Contents
- Examples
- Public API
- Initialisation and shutdown
- Memory allocation
- String helpers
- Value type
- Channels
- Utilities
- Fiber launch
- minicoro limitations
- Channel I/O — callbacks (any context)
- Channel I/O — non-blocking (any context)
- Channel I/O — fiber context
- Channel I/O — blocking OS threads
- Select (
goc_alts) - Timeout channel
- RW mutexes
- Thread pool
- Scheduler loop access
- Async I/O →
- HTTP Client/Server →
- Best Practices
- Benchmarks
- Building and Testing
Examples
1. Ping-pong
Two fibers exchange a message back and forth over a pair of unbuffered channels. This is the canonical CSP "ping-pong" pattern — each fiber blocks on a take, then immediately puts to wake the other side.
#include "goc.h"
#include <stdio.h>
#define N_ROUNDS 5
typedef struct {
goc_chan* recv; /* channel this fiber reads from */
goc_chan* send; /* channel this fiber writes to */
const char* name;
} player_args_t;
static void player_fiber(void* arg) {
player_args_t* a = arg;
goc_val_t* v;
while ((v = goc_take(a->recv))->ok == GOC_OK) {
int count = goc_unbox_int(v->val);
printf("%s %d\n", a->name, count);
if (count >= N_ROUNDS) {
goc_close(a->send);
return;
}
goc_put(a->send, goc_box_int(count + 1));
}
}
static void main_fiber(void* _) {
goc_chan* a_to_b = goc_chan_make(0);
goc_chan* b_to_a = goc_chan_make(0);
player_args_t ping_args = { .recv = b_to_a, .send = a_to_b, .name = "ping" };
player_args_t pong_args = { .recv = a_to_b, .send = b_to_a, .name = "pong" };
goc_chan* done_ping = goc_go(player_fiber, &ping_args);
goc_chan* done_pong = goc_go(player_fiber, &pong_args);
/* Kick off the exchange with the first message. */
goc_put(a_to_b, goc_box_int(1));
/* Wait for both fibers to finish. */
goc_take(done_ping);
goc_take(done_pong);
}
int main(void) {
goc_init();
goc_go(main_fiber, NULL);
goc_shutdown();
return 0;
}
What this example demonstrates:
goc_chan_make(0)— unbuffered channels enforce a synchronous rendezvous: eachgoc_putblocks until the other fiber callsgoc_take, and vice versa.goc_go— spawns both player fibers on the current pool (or default pool when called outside fiber context) and returns a join channel that is closed automatically when the fiber returns.goc_close— when the round limit is reached the active fiber closes the forward channel, causing the partner's nextgoc_taketo returnGOC_CLOSEDand exit its loop cleanly.
2. Custom thread pool with goc_go_on
Use goc_pool_make when you need workloads isolated from the default pool —
for example, CPU-bound tasks that should not starve I/O fibers.
This example fans out several independent jobs onto a dedicated pool, then
collects all results from main using goc_take_sync.
#include "goc.h"
#include <stdio.h>
#include <stdlib.h>
/* -------------------------------------------------------------------------
* Worker fiber: sum integers in [lo, hi) and send the result back.
* In a real program this would be image processing, compression, etc.
* ------------------------------------------------------------------------- */
typedef struct {
long lo, hi;
goc_chan* result_ch;
} worker_args_t;
static void sum_range(void* arg) {
worker_args_t* a = arg;
long acc = 0;
for (long i = a->lo; i < a->hi; i++)
acc += i;
goc_put(a->result_ch, goc_box_int(acc));
}
/* =========================================================================
* main
* ========================================================================= */
#define N_WORKERS 4
#define RANGE 1000000L
int main(void) {
goc_init();
/*
* A dedicated pool for CPU-bound work. The default pool (started by
* goc_init) is left free for I/O fibers and other goc_go calls.
*/
goc_pool* cpu_pool = goc_pool_make(N_WORKERS);
goc_chan* result_ch = goc_chan_make(0);
worker_args_t workers[N_WORKERS];
long chunk = RANGE / N_WORKERS;
/* Fan out: spawn each worker on the CPU pool with goc_go_on. */
for (int i = 0; i < N_WORKERS; i++) {
workers[i].lo = i * chunk;
workers[i].hi = (i == N_WORKERS - 1) ? RANGE : (i + 1) * chunk;
workers[i].result_ch = result_ch;
goc_go_on(cpu_pool, sum_range, &workers[i]);
}
/* Fan in: collect results from the main thread with goc_take_sync. */
long total = 0;
for (int i = 0; i < N_WORKERS; i++) {
goc_val_t* v = goc_take_sync(result_ch);
if (v->ok == GOC_OK) total += (long)goc_unbox_int(v->val);
}
printf("sum [0, %ld) = %ld\n", RANGE, total);
/*
* Destroy the CPU pool.
* Optional, shown here for completeness.
* All undestroyed pools are automatically cleaned up by goc_shutdown().
*/
goc_pool_destroy(cpu_pool);
goc_shutdown();
return 0;
}
What this example demonstrates:
goc_pool_make/goc_pool_destroy— creates and tears down a dedicated pool, isolated from the default pool started bygoc_init.goc_go_on— pins each worker fiber to the CPU pool.- Fan-out / fan-in over a shared result channel — no explicit synchronisation primitives beyond channels.
goc_pool_destroyblocks till all the fibers running on the pool finish, then frees resources.goc_shutdowntears down the rest of the runtime.
3. Using goc_malloc
goc_malloc allocates memory on the Boehm GC heap. Allocations are collected
automatically when no longer reachable — no free is needed. This is the
intended allocator for long-lived program objects: nodes, buffers, application data, and so on.
#include "goc.h"
#include <stdio.h>
typedef struct node_t {
int value;
struct node_t* next;
} node_t;
static node_t* build_list(int n) {
node_t* head = NULL;
for (int i = n - 1; i >= 0; i--) {
node_t* node = goc_malloc(sizeof(node_t));
node->value = i;
node->next = head;
head = node;
}
return head;
}
int main(void) {
goc_init();
node_t* node = build_list(10);
while (node != NULL) {
printf("%d ", node->value);
node = node->next;
}
printf("\n");
goc_shutdown();
return 0;
}
A few things to keep in mind:
goc_mallocis a thin wrapper aroundGC_malloc. Memory is zero-initialised.
Public API
Initialisation and shutdown
| Function | Signature | Description |
|---|---|---|
| goc_init | void goc_init(void) | Initialise the runtime. Must be called exactly once as the first call in main(), from the process main thread. |
| goc_shutdown | void goc_shutdown(void) | Shut down the runtime. Blocks until all in-flight fibers finish, then tears down all pools, channels, and the event loop. Must be called from the process main thread. |
Memory allocation
| Function | Signature | Description |
|---|---|---|
| goc_malloc | void* goc_malloc(size_t n) | Allocate n bytes
