GOKe

June 25, 2026 · View on GitHub

GOKe Logo
Go Version GoDoc License Go Report Card Codecov Coverage Go Quality Check Mentioned in Awesome Go

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

CapabilityHow
Zero-allocation hot pathsChunk-based SoA layout with direct pointer arithmetic — no GC pressure during iteration or component access
Predictable iteration speedLinear SoA memory access — cache-friendly, branch-free inner loops; sub-nanosecond per entity at scale
Predictable iteration costPer-entity overhead stays constant regardless of how much logic runs in the loop body
O(1) component lookupEntity-to-storage is a direct array index, not a hash map — constant time at any world size
Safe entity recycling64-bit generational IDs detect stale references after deletion, preventing ABA bugs
Cache-friendly storageContiguous SoA chunks; growth appends new chunks, removal uses swap-and-pop — no fragmentation
Batch entity creationFactory writes components directly into chunk-shaped batches — no per-entity allocation
Type-safe component APIFully generic — no reflection, no interface boxing, no runtime type assertions
Built-in schedulerDeclarative Plan wires systems into an execution graph — a full ECS runtime, not just a component store
Command BufferStructural 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 (NN).

📊 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.

CategoryOperationObserved Cost (2¹⁰–2²⁰ Entities)AllocsTechnical Mechanism
ThroughputIteration (Query.All)0.32 - 3.41 ns/ent0Linear SoA (0-10 components)
Subset QueryPick (per-entity, 1,024 only)3.6 - 11.2 ns/ent0Per-entity record lookup + pointer math
Direct AccessSeek (single entity, 1,024 only)2.9 - 10.7 ns/ent0Index lookup, independent of include/exclude mask
StructuralBatch Create3.9 - 9.0 ns/ent0-1Factory-based chunk writes
StructuralAdd Component32 - 90 ns/op0 at 2¹⁰; grows with N at 2²⁰Archetype migration (1 → 1+N components)
StructuralAdd Tag30 - 71 ns/op0 at 2¹⁰; grows with N at 2²⁰Archetype migration (zero-size component)
StructuralRemove Component83 - 110 ns/op0 at 2¹⁰; grows with N at 2²⁰Archetype migration (10 → 10-N components)
StructuralRemove Entity3.2 ns/op (population 100,000)0Swap-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:

PackageResponsibility
github.com/kjkrol/uid64-bit generational entity identifiers — safe index recycling, ABA prevention
iterLowest-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/compShared component primitives used across all internal packages — type registration, metadata, and blueprint definitions
internal/chunkCache-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/colstoreColumn-oriented storage for a single archetype — manages component columns over chunk.Pack chunks, resolves component IDs to memory locations in O(1)
internal/archArchetype identity, archetype graph, and SoA table storage — creates archetypes on demand and caches structural transitions as graph edges
internal/addrEntity 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/entEntity 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/queryQuery 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/orchPlan-based task orchestrator: sequential/parallel execution, deferred mutations via command buffers
internal/regTop-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 modifying MaskSize in internal/comp (e.g. MaskSize = 4 gives 256 component types) and recompiling GOKe — MaxComponents is derived automatically as 64 * 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.go files within the internal packages.