GOKe
June 25, 2026 · View on GitHub
GOKe is a type-safe, archetype-based Entity Component System (ECS) for Go. It uses a Structure of Arrays (SoA) storage model and Data-Oriented Design principles to enable cache-friendly iteration and efficient processing of large numbers of entities.
Features • Installation • Benchmarks • Performance • Example • Architecture • Roadmap • Documentation
Design Goals
GOKe is primarily an ECS for game development, but its archetype-based SoA architecture also makes it well suited for simulations, AI agents, real-time analytics, and other performance-critical workloads.
The project is built around a few core principles:
- Abstractions that reflect ideas, not implementation details
- Predictable performance with no hidden costs
- Cache-friendly data layouts
- Zero-allocation hot paths
- Inlining-friendly hot paths
- Type-safe APIs without reflection
- Native Go development without CGO dependencies
While native C and Rust ECS frameworks may achieve higher peak throughput, GOKe is designed to maximize performance within the Go ecosystem. For many projects, avoiding CGO boundaries, external dependencies, and cross-language integration costs can outweigh the gains of a faster foreign implementation.
📦 Installation
GOKe requires Go 1.26 or newer.
go get github.com/kjkrol/goke/v2
✨ Key Features
| Capability | How |
|---|---|
| Zero-allocation hot paths | Chunk-based SoA layout with direct pointer arithmetic — no GC pressure during iteration or component access |
| Predictable iteration speed | Linear SoA memory access — cache-friendly, branch-free inner loops; sub-nanosecond per entity at scale |
| Predictable iteration cost | Per-entity overhead stays constant regardless of how much logic runs in the loop body |
| O(1) component lookup | Entity-to-storage is a direct array index, not a hash map — constant time at any world size |
| Safe entity recycling | 64-bit generational IDs detect stale references after deletion, preventing ABA bugs |
| Cache-friendly storage | Contiguous SoA chunks; growth appends new chunks, removal uses swap-and-pop — no fragmentation |
| Batch entity creation | Factory writes components directly into chunk-shaped batches — no per-entity allocation |
| Type-safe component API | Fully generic — no reflection, no interface boxing, no runtime type assertions |
| Built-in scheduler | Declarative Plan wires systems into an execution graph — a full ECS runtime, not just a component store |
| Command Buffer | Structural changes during iteration are queued and flushed at explicit Sync() points — enables safe RunParallel |
💡 See the Performance & Scalability section below for benchmark results validated from 2¹⁰ to 2²⁰ entities.
⏱️ Performance & Scalability
GOKe is designed for predictable performance at scale. By utilizing a Centralized Record System (dense array lookup) instead of traditional hash maps, structural operations and query execution remain effectively independent of the total entity count ().
📊 Cross-framework comparison
Benchmarks against other Go ECS libraries (Arche, Donburi, Ento, etc.) are maintained in a dedicated project — go-ecs-benchmarks by @mlange-42.
⚠️ Before drawing conclusions, verify which GOKe version (tag) is used in the comparison, as published results may lag behind the latest release.
Scalability Validation
Editor/Factory.Create/Query.All were validated across worlds ranging from 2¹⁰ (1,024) to 2²⁰ (1,048,576) entities on an Apple M1 Max. Structural per-entity cost stays nearly constant across that range; Query.All's per-entity cost roughly doubles at 2²⁰ once the working set outgrows cache (a known trade-off of the chunked SoA layout). Pick/Seek/Remove are currently only benchmarked at a fixed population.
| Category | Operation | Observed Cost (2¹⁰–2²⁰ Entities) | Allocs | Technical Mechanism |
|---|---|---|---|---|
| Throughput | Iteration (Query.All) | 0.32 - 3.41 ns/ent | 0 | Linear SoA (0-10 components) |
| Subset Query | Pick (per-entity, 1,024 only) | 3.6 - 11.2 ns/ent | 0 | Per-entity record lookup + pointer math |
| Direct Access | Seek (single entity, 1,024 only) | 2.9 - 10.7 ns/ent | 0 | Index lookup, independent of include/exclude mask |
| Structural | Batch Create | 3.9 - 9.0 ns/ent | 0-1 | Factory-based chunk writes |
| Structural | Add Component | 32 - 90 ns/op | 0 at 2¹⁰; grows with N at 2²⁰ | Archetype migration (1 → 1+N components) |
| Structural | Add Tag | 30 - 71 ns/op | 0 at 2¹⁰; grows with N at 2²⁰ | Archetype migration (zero-size component) |
| Structural | Remove Component | 83 - 110 ns/op | 0 at 2¹⁰; grows with N at 2²⁰ | Archetype migration (10 → 10-N components) |
| Structural | Remove Entity | 3.2 ns/op (population 100,000) | 0 | Swap-and-pop + index recycling |
Deep Dive: For the full per-component-count breakdown, methodology, and reproduction instructions, see BENCHMARKS.md.
Reproducing Results
Run the benchmark suite on your own hardware:
make bench
Real-World Example
The following demo showcases a simple collision simulation built with GOKe and Ebitengine.
It simulates thousands of moving AABBs while maintaining a fixed 120 TPS update loop using archetype-based storage, cache-friendly iteration, and parallel systems.
|
Stats: 2306 colliding AABBs | 120 TPS | 50 collisions/tick |
Stats: 524 colliding AABBs | 120 TPS | 15 collisions/tick |
|---|
Source code: examples/ebiten-demo
Example
New to ECS? Check out the Getting Started with GOKe guide for a step-by-step deep dive into building your first simulation.
package main
import (
"fmt"
"time"
"github.com/kjkrol/goke/v2"
)
type Pos struct{ X, Y float32 }
type Vel struct{ X, Y float32 }
type Acc struct{ X, Y float32 }
func main() {
// Initialize the ECS world.
ecs := goke.New()
// Comp[T] gives typed read/write access to a component. The same
// instance can be reused across factory, editor and query — pass &comp
// directly, no wrapping.
var pos goke.Comp[Pos]
var vel goke.Comp[Vel]
var acc goke.Comp[Acc]
// Create a factory for bulk entity spawning.
factory := ecs.NewFactory(&pos, &vel, &acc)
cursor := &factory.Cursor
factory.Create(1)
factory.Next()
entityID := factory.IDs[0]
pos.Slice(cursor)[0] = Pos{X: 0, Y: 0}
vel.Slice(cursor)[0] = Vel{X: 1, Y: 1}
acc.Slice(cursor)[0] = Acc{X: 0.1, Y: 0.1}
// Create a query — declares which components to iterate.
query := ecs.NewQueryBuilder(&pos, &vel, &acc).Build()
cursor = query.Cursor()
// Register a system using the functional pattern.
movementSystem := ecs.RegSysFn(func(_ *goke.CmdBuf, _ time.Duration) {
// SoA layout: Query.All advances chunk by chunk — the inner loop
// iterates over contiguous memory for cache-friendly access.
query.All()
for query.Next() {
posSlice := pos.Slice(cursor)
velSlice := vel.Slice(cursor)
accSlice := acc.Slice(cursor)
for i := range cursor.IDs {
velSlice[i].X += accSlice[i].X
velSlice[i].Y += accSlice[i].Y
posSlice[i].X += velSlice[i].X
posSlice[i].Y += velSlice[i].Y
}
}
})
// Configure the execution plan and synchronization points.
ecs.SetPlan(func(ctx goke.RunCtx, d time.Duration) {
ctx.Run(movementSystem, d)
ctx.Sync()
})
// Execute a single simulation step (120 TPS).
ecs.Tick(time.Second / 120)
// Read a single entity's component via Seek (cursor-based, typed).
lookup := ecs.NewQueryBuilder(&pos).Build()
if lookup.Seek(entityID) {
p := pos.At(lookup.Cursor())
fmt.Printf("Final Position: {X: %.2f, Y: %.2f}\n", p.X, p.Y)
}
}
Check the examples/ directory for complete, ready-to-run projects.
Architecture
GOKe is an archetype-based ECS built around data-oriented design principles. The internal packages each own a single, well-defined responsibility:
| Package | Responsibility |
|---|---|
github.com/kjkrol/uid | 64-bit generational entity identifiers — safe index recycling, ABA prevention |
iter | Lowest-level column-access primitives — Cursor (current iteration position) and ArrayRef[T] (typed pointer arithmetic into a Cursor, zero-allocation Slice/At); the public Comp[T] wraps this |
internal/comp | Shared component primitives used across all internal packages — type registration, metadata, and blueprint definitions |
internal/chunk | Cache-aligned chunked memory layout — L1-cache-sized fixed slabs, field offset calculation, slot tracking within a growing slab collection; keeps one spare slab on shrink so repeated grow/shrink cycles stay allocation-free |
internal/colstore | Column-oriented storage for a single archetype — manages component columns over chunk.Pack chunks, resolves component IDs to memory locations in O(1) |
internal/arch | Archetype identity, archetype graph, and SoA table storage — creates archetypes on demand and caches structural transitions as graph edges |
internal/addr | Entity address book — manages entity ID lifecycle (uid pool) and maps each ID to its current storage address (Entry) via a flat index in O(1); generation check guards against stale references |
internal/ent | Entity lifecycle — delegates ID allocation and address tracking to addr.Book, manages component migration (add/remove moves entity to a new archetype), and batch entity creation via Factory |
internal/query | Query layer: Matcher bakes component masks into precomputed per-archetype offsets, enabling zero-allocation bulk iteration (All), per-entity subset iteration (Pick), and O(1) single-entity access (Seek) |
internal/orch | Plan-based task orchestrator: sequential/parallel execution, deferred mutations via command buffers |
internal/reg | Top-level world registry — wires together all subsystems and exposes the unified API for entity and component management |
goke (public) | The package you import. ECS wires reg.Registry + orch.Scheduler; Comp[T] gives typed access to a component; NewFactory/NewQueryBuilder/NewEditorBuilder are the only construction paths for Factory/Query/Editor; System/SystemFn/CmdBuf round out the scheduling API |
🗺️ Roadmap
Current development focus and planned improvements:
- Ebitengine Integration: Dedicated helpers for seamless state synchronization between GOKe systems and Ebitengine's loop — partially prototyped in the ebiten-demo, with the goal of extracting it into a separate companion repository.
- Entity Relations via Tags: Extend the Tag system to model relationships between entities (parent-child, links, ownership, ...) — adding relational semantics on top of the existing archetype-mask machinery, without sacrificing the zero-allocation hot loop.
Live Feature Tracker We manage our long-term goals through GitHub Issues. View all planned core engine expansions and functional capabilities here: Explore all Pending Features ↗
When NOT to Use GOKe
GOKe is optimized for large-scale, data-oriented workloads. It may not be the best fit for every project.
- Small Data Sets — For a few hundred objects, plain Go structs and slices are often simpler and sufficiently fast.
- Deep Hierarchies — ECS excels at flat data layouts. Tree-oriented domains such as UI systems or DOM-like structures may be better served by traditional object graphs.
- High Structural Churn — Archetype migration is efficient, but workloads that continuously add and remove components from large numbers of entities every frame may reduce the benefits of archetype-based storage.
- Behavior-Centric Designs — If your application is primarily organized around objects and methods rather than data transformations, an ECS may introduce unnecessary complexity.
Limitations
- Maximum component types: 128 by default. The archetype system uses a fixed-size bitmask (
[2]uint64) for fast component membership checks. Projects requiring more component types can increase this limit by modifyingMaskSizeininternal/comp(e.g.MaskSize = 4gives 256 component types) and recompiling GOKe —MaxComponentsis derived automatically as64 * MaskSize. This is a compile-time configuration, not a runtime setting.
License
GOKe is licensed under the MIT License. See the LICENSE file for more details.
📖 Documentation
- API Reference: Detailed documentation and examples are available on pkg.go.dev.
- Wiki & Guides: For a step-by-step deep dive into building your first simulation, check the Getting Started with GOKe guide.
- Internal Mechanics: For a technical breakdown of the engine's core, check the
doc.gofiles within theinternalpackages.