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/coro
Important
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 cancelled: %v\n", r)
}
}()
// Yield a value
yield("before cancel")
// This will never be reached if the coroutine is cancelled
fmt.Println("This won't be executed if cancelled")
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 cancelled 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
outputValue
is passed to the caller via theresume
function - When the coroutine is resumed,
yield
returns 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
Out
is returned fromresume
) - When the coroutine is resumed,
suspend
returns 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
yield
orsuspend
after 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
yield
andsuspend
escape 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.