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
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.
GOKe requires Go 1.27.0 or newer.
go get github.com/kjkrol/goke/v3| 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 |
| Bulk operations | Editor/ValueEditor/Remover, staged via Query.BeginMigrate/Add/Commit, batch add/remove-component and remove-entity changes for entities matched by a Query — one block memory copy per contiguous run instead of a move per entity |
| Query forks | Query.Fork is another walk over what a query matches, with a cursor of its own over the same baked tables: queries walk and seek on several goroutines at once, as long as none writes a component another reads or writes |
| Single-entity operations | CmdBuf.AddOne/RemoveOne/RemoveCompOne edit or remove one entity reached without iterating a Query (an external event, a saved id) — the complement to bulk operations, not a substitute for them inside a Query loop |
| World persistence | Save the whole world's state to a file and restore it exactly: every registered component type, the archetypes entities are grouped into, and each entity's own identity. |
| Module composition | Package a coherent set of components and systems as one self-contained Module — a game wires it up without knowing its internals, and can plug it into its own one-time Setup (world seeding) or per-tick Plan (simulation) |
💡 See the Performance & Scalability section below for benchmark results validated from 2¹⁰ to 2²⁰ entities.
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 (
Benchmarks against other Go ECS libraries (Ark, Donburi, Ento, etc.) are maintained in a dedicated projects:
- go-ecs-benchmarks by @mlange-42.
- kjkrol/go-ecs-benchmarks by @kjkrol (should contains latest GOKe results)
⚠️ Before drawing conclusions, verify which GOKe version (tag) is used in the comparison, as published results may lag behind the latest release.
Benchmarked at a population of 1,024 entities (100,000 for Remove Entity) on an Intel i5-8265U (see BENCHMARKS.md). Structural operations (Editor/ValueEditor/Remover) stay in the tens-of-ns/entity range regardless of how many components an edit touches; query paths (Query.All/Pick/Seek/SeekH) report zero allocations across every component count tested.
| Category | Operation | Observed Cost | Allocs | Technical Mechanism |
|---|---|---|---|---|
| Throughput | Iteration (Query.All) | 0.39 - 4.12 ns/ent | 0 | Linear SoA (0-10 components) |
| Subset Query | Pick (per-entity) | 5.3 - 17.1 ns/ent | 0 | Per-entity record lookup + pointer math |
| Direct Access | Seek (single entity) | 4.0 - 17.9 ns/ent | 0 | Index lookup, independent of include/exclude mask |
| Direct Access | SeekH (homogeneous, cached archetype) | 2.1 - 15.7 ns/ent | 0 | Skips generation/mask checks after a prior Seek |
| Structural | Batch Create | 8.0 - 18.8 ns/ent | 0-1 | Factory-based chunk writes |
| Structural | Add Component | 5.9 - 18.7 ns/ent | 0 (grows past N=8) | Archetype migration (1 → 1+N components) |
| Structural | Add Tag | 5.6 - 12.9 ns/ent | 0 | Archetype migration (zero-size component) |
| Structural | Remove Component | 14.1 - 19.4 ns/ent | 0 (grows at N=1) | Archetype migration (10 → 10-N components) |
| Structural | Add Component + Value (ValueEditor) | 6.2 - 43.7 ns/ent | 0 | Archetype migration + per-entity value write |
| Structural | Bulk Remove (Remover) | 14.4 - 86.2 ns/ent | 0 | CmdBuf-batched migration, one Sync per chunk |
| Structural | Remove Entity | 101.7 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.
Run the benchmark suite on your own hardware:
make benchThe following demo showcases a simple collision simulation built with GOKe and Ebitengine, via the gram companion repository (formerly gokebiten).
It simulates thousands of moving AABBs while maintaining a fixed 120 TPS update loop using archetype-based storage, cache-friendly iteration, and parallel systems.
gokebiten-2306-50.mp4Stats: 2306 colliding AABBs | 120 TPS | 50 collisions/tick |
gokebiten-524-15.mp4Stats: 524 colliding AABBs | 120 TPS | 15 collisions/tick |
|---|
Source code: gram/examples/collision-demo
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/v3"
"github.com/kjkrol/uid"
)
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]
var entityID uid.UID64
// Query/Editor/Factory can only be constructed inside a System's Init —
// ecs.Setup runs one-off systems for world seeding, exactly once.
ecs.Setup(goke.SystemFn{
OnInit: func(si *goke.SysInit) {
factory := si.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}
},
})
// Register the movement system: builds its own Query in Init, iterates in Update.
var query *goke.Query
movementSystem := ecs.RegSys(goke.SystemFn{
OnInit: func(si *goke.SysInit) {
query = si.NewQueryBuilder(&pos, &vel, &acc).Build()
},
OnUpdate: func(_ *goke.CmdBuf, _ time.Duration) {
// SoA layout: Query.All advances chunk by chunk — the inner loop
// iterates over contiguous memory for cache-friendly access.
cursor := query.Cursor()
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
}
}
},
})
// Register the report system: reads and prints, same pattern as
// movementSystem — reads flow through systems too, not raw lookups
// pulled out of the ECS.
var reportQuery *goke.Query
reportSystem := ecs.RegSys(goke.SystemFn{
OnInit: func(si *goke.SysInit) {
reportQuery = si.NewQueryBuilder(&pos).Build()
},
OnUpdate: func(_ *goke.CmdBuf, _ time.Duration) {
if reportQuery.Seek(entityID) {
p := pos.At(reportQuery.Cursor())
fmt.Printf("Final Position: {X: %.2f, Y: %.2f}\n", p.X, p.Y)
}
},
})
// Configure the execution plan and synchronization points.
ecs.SetPlan(func(ctx goke.RunCtx, d time.Duration) {
ctx.Run(movementSystem, d)
ctx.Sync()
ctx.Run(reportSystem, d)
})
// Execute a single simulation step (120 TPS).
ecs.Tick(time.Second / 120)
}Check the examples/ directory for complete, ready-to-run projects.
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/bulk |
Bulk-operation contract — ChunkSnapshot (point-in-time chunk address, guarded by the source table's structural version) and the Migrator/ValueMigrator interfaces; the shared vocabulary of chunk-level batch commands |
internal/ent |
Entity lifecycle — delegates ID allocation and address tracking to addr.Book, manages batch entity creation via Factory, and bulk archetype migration via Editor (add/remove component spec), Remover (bulk unlink), and ValueEditor (add one component and write a caller-supplied per-entity value into it) |
internal/query |
Query layer: Matcher bakes the archetypes a query matches into precomputed per-column offsets and a Walker walks them with a cursor of its own: zero-allocation bulk iteration (All), per-entity subset iteration (Pick), and O(1) single-entity access (Seek); Query.Fork is another Walker over the same Matcher |
internal/persist |
World snapshot encoding — Save/Load a gzip-wrapped file format covering component definitions, archetype layout, entity data, and the entity ID pool state |
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. Construction is gated through systems: SysInit (available in a System's Init, or via ecs.Setup for one-off world seeding) is the only way to get a Query or Factory; Editor/ValueEditor are then built from that Query. System/SystemFn/CmdBuf round out the scheduling API |
Current development focus and planned improvements:
- 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 ↗
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.
- Maximum component types: 512 by default. The archetype system uses a fixed-size bitmask (
[8]uint64) for fast component membership checks, and a component ID is 16 bits. Projects requiring more component types can increase this limit by modifyingMaskSizeininternal/comp(e.g.MaskSize = 16gives 1,024 component types) and recompiling GOKe —MaxComponentsis derived automatically as64 * MaskSize. This is a compile-time configuration, not a runtime setting. A wider mask leaves iteration, Seek and migrations as they are; announcing a new archetype to the queries grows with it (see BENCHMARKS.md). - Maximum archetypes: 4,095. Every distinct combination of components and tags entities carry is an archetype; a world holds at most 4,095 (
arch.MaxID4096 ininternal/arch, ID 0 unused), fixed at compile time. Asking for one more panics with the limit and the combination asked for.
GOKe is licensed under the MIT License. See the LICENSE file for more details.
- 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.