A Go implementation of coroutines that provides cooperative multitasking using Go's native coroutine support.
- Create and manage coroutines that can yield control flow
- Strongly typed inputs and outputs with generics
- Support for yielding values and suspending execution
- Complete panic handling and propagation
- Cancellation mechanism for coroutines
go get github.com/webriots/coroImportant
The -ldflags=-checklinkname=0 flag is required when building and testing this library since it uses the //go:linkname directive to access internal Go runtime functions. As of Go 1.23, accessing internal symbols requires this flag as an "escape hatch" to bypass the new package handshake requirement.
- Go 1.23.1 or later
package main
import (
"fmt"
"github.com/webriots/coro"
)
func main() {
// Create a coroutine that yields values
resume, cancel := coro.New(func(yield func(string) int, suspend func() int) string {
// Yield a value and wait for input
input := yield("first value")
fmt.Printf("Received: %d\n", input)
// Yield another value
input = yield("second value")
fmt.Printf("Received: %d\n", input)
// Return final value
return "final value"
})
defer cancel() // Always cancel to avoid leaks
// Start the coroutine and get the first yielded value
value, running := resume(0)
fmt.Printf("Coroutine yielded: %s, still running: %t\n", value, running)
// Output: Coroutine yielded: first value, still running: true
// Resume the coroutine with a value and get the next yielded value
value, running = resume(1)
fmt.Printf("Coroutine yielded: %s, still running: %t\n", value, running)
// Output: Coroutine yielded: second value, still running: true
// Resume one last time to get the return value
value, running = resume(2)
fmt.Printf("Coroutine returned: %s, still running: %t\n", value, running)
// Output: Coroutine returned: final value, still running: false
}package main
import (
"fmt"
"github.com/webriots/coro"
)
// Create a generator that yields numbers in a sequence
func createSequence(max int) func() (int, bool) {
resume, cancel := coro.New(func(yield func(int) struct{}, suspend func() struct{}) int {
for i := 0; i < max; i++ {
yield(i)
}
return -1 // Final return value
})
// Return a simple generator function
return func() (int, bool) {
value, running := resume(struct{}{})
if !running {
cancel() // Clean up resources
}
return value, running
}
}
func main() {
// Create a generator that yields numbers 0-9
nextValue := createSequence(10)
// Iterate through all values
for {
value, hasMore := nextValue()
if !hasMore {
break
}
fmt.Printf("Generated: %d\n", value)
}
}package main
import (
"fmt"
"github.com/webriots/coro"
)
func main() {
resume, cancel := coro.New(func(yield func(string) int, suspend func() int) string {
// Suspend execution without yielding a value
input := suspend()
fmt.Printf("Resumed with: %d\n", input)
// Yield a value and suspend
input = yield("yielded value")
fmt.Printf("Resumed with: %d\n", input)
return "done"
})
defer cancel()
// First resume starts the coroutine, which immediately suspends
value, running := resume(0)
fmt.Printf("Value: %q, Running: %t\n", value, running)
// Output: Value: "", Running: true
// Resume the suspended coroutine
value, running = resume(1)
fmt.Printf("Value: %q, Running: %t\n", value, running)
// Output: Value: "yielded value", Running: true
// Final resume to get the return value
value, running = resume(2)
fmt.Printf("Value: %q, Running: %t\n", value, running)
// Output: Value: "done", Running: false
}package main
import (
"fmt"
"github.com/webriots/coro"
)
func main() {
resume, cancel := coro.New(func(yield func(string) int, suspend func() int) string {
// Use defer to catch cancellation
defer func() {
if r := recover(); r != nil {
fmt.Printf("Coroutine was canceled: %v\n", r)
}
}()
// Yield a value
yield("before cancel")
// This will never be reached if the coroutine is canceled
fmt.Println("This won't be executed if canceled")
return "completed"
})
// Start the coroutine
value, running := resume(0)
fmt.Printf("Value: %q, Running: %t\n", value, running)
// Cancel the coroutine
cancel()
// Attempting to resume will panic with ErrCanceled
// If you want to handle this, you need to wrap in a recover()
func() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Panic when resuming canceled coroutine: %v\n", r)
}
}()
resume(1)
}()
}Coroutines are created using the New function, which returns a pair of functions: resume and cancel. The provided function to New is executed as a coroutine, and receives two control flow functions: yield and suspend.
resume, cancel := coro.New[In, Out](fn func(yield func(Out) In, suspend func() In) Out)Parameters:
| Parameter | Description |
|---|---|
In |
Type parameter for values passed to the coroutine when resuming |
Out |
Type parameter for values yielded from the coroutine |
fn |
Function to execute as a coroutine |
yield |
Function to return a value to the caller and pause execution |
suspend |
Function to pause execution without returning a value |
Returns:
| Function | Description |
|---|---|
resume(In) (Out, bool) |
Function to resume the coroutine with a value and get its yielded value and status |
cancel() |
Function to cancel the coroutine's execution |
The resume function is used to start and continue a coroutine's execution:
value, running := resume(inputValue)Parameters:
| Parameter | Description |
|---|---|
inputValue |
Value of type In to pass to the coroutine |
Returns:
| Value | Description |
|---|---|
value |
The value yielded by the coroutine (of type Out) or the coroutine's final return value |
running |
Boolean indicating whether the coroutine is still running (true) or has completed (false) |
The yield function allows a coroutine to return a value to the caller and pause execution until resumed:
inputValue := yield(outputValue)When yield is called:
- The coroutine pauses execution and returns control to the caller
- The
outputValueis passed to the caller via theresumefunction - When the coroutine is resumed,
yieldreturns the value passed toresume
The suspend function pauses a coroutine without yielding a value:
inputValue := suspend()When suspend is called:
- The coroutine pauses execution and returns control to the caller
- No value is returned to the caller (the zero value of
Outis returned fromresume) - When the coroutine is resumed,
suspendreturns the value passed toresume
The cancel function is used to terminate a coroutine early:
cancel()When a coroutine is canceled:
- If the coroutine is currently suspended, resuming it will cause a panic with
ErrCanceled - If the coroutine calls
yieldorsuspendafter being canceled, it will panic withErrCanceled - The panic can be caught inside the coroutine with a deferred recover block
Panics within a coroutine are captured and propagated through the resume function:
defer func() {
if r := recover(); r != nil {
// Handle the panic
fmt.Printf("Coroutine panicked: %v\n", r)
}
}()
resume, cancel := coro.New(...)
defer cancel()
// This will panic if the coroutine panics
resume(inputValue)Additionally, a special panicError type is used to wrap panics and provide stack trace information. You can access the stack trace using the error's methods:
defer func() {
if r := recover(); r != nil {
if pe, ok := r.(interface{ DebugString() string }); ok {
// Get detailed error information with stack trace
errorInfo := pe.DebugString()
fmt.Println(errorInfo)
}
}
}()The New function uses generics for type safety:
// A coroutine that takes integers and yields strings
resume, cancel := coro.New[int, string](func(yield func(string) int, suspend func() int) string {
// yield returns string and receives int
input := yield("hello")
return "final"
})
// Type-safe usage
value, _ := resume(42) // value is of type string-
Always defer
cancel()to ensure proper cleanup when you're done with a coroutine:resume, cancel := coro.New(...) defer cancel() // Prevents resource leaks
-
Handle eventual completion by checking the boolean return value from
resume:value, running := resume(input) if !running { // Coroutine has completed }
-
Avoid letting
yieldandsuspendescape the coroutine function. If these functions escape and are called after the coroutine is done, they will panic. -
Use recover within coroutines to handle cancellation gracefully:
resume, cancel := coro.New(func(yield func(string) int, suspend func() int) string { defer func() { if r := recover(); r != nil { // Handle cancellation or other panics } }() // Coroutine logic })
-
Build higher-level abstractions like generators, iterators, or state machines on top of the core coroutine functionality.
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License.