Why Go?
Why use Go for games programming?
A game engine needs more than a renderer. It needs simulation, asset processing, audio, networking, tools and a way to diagnose what went wrong on a player's machine. Go gives these parts a common language and toolchain. For Bunyip, its appeal is the combination of native compilation, straightforward code and tools that work across the whole project.
That makes Go a useful choice for developers who want to build their game in code, understand the engine underneath it and spend less time maintaining build infrastructure. Here is how that choice shapes Bunyip.
A shared language for the engine and core game code
Gameplay code uses the same structs, functions, interfaces and packages as the renderer and asset tools. You can follow a drawing call into the engine with your editor, change it, and rebuild with the same toolchain. Core game systems can call the engine's Go API directly and share packages with its tools.
Scripting for modding is a future goal. Bunyip does not have an embedded scripting language yet; its absence is not a decision to require every mod to be written in Go. A scripting layer would let mod authors extend a game while its engine and core systems retain this shared Go foundation.
Small interfaces let a subsystem accept the capabilities it needs. For
example, Bunyip's asset helpers accept fs.FS: the same loading code can
read embedded files in a shipped game and an in-memory filesystem in a
test. Closures keep short operations together, such as drawing through a
camera or building a UI panel. Generics keep component queries typed without
requiring a parallel hierarchy of game objects.
This is useful for an engine's users as well as its maintainers: ordinary Go techniques remain useful as a prototype grows. The API design guide shows the ownership and scope conventions in detail.
Native code with control over the data
Go compiles game logic to native machine code. Structs, arrays and slices let you represent a simulation directly and process related values together. Bunyip's ECS stores components in archetypes and exposes typed iteration:
type Position struct{ X, Y float64 }
type Velocity struct{ X, Y float64 }
// In Update, with a world containing these components:
world.Each2(func(e ecs.Entity, p *Position, v *Velocity) {
p.X += v.X * ctx.Delta
p.Y += v.Y * ctx.Delta
})
The update expresses the work directly, without type assertions in the game's loop. You can measure this work, change its data layout or algorithm, and keep the rest of the game intact. Go does not guarantee that every abstraction is free, or that a particular simulation will meet its frame budget. Its value here is that clear code and measurable performance can live in the same implementation.
Tools for the whole development cycle
Formatting, tests, benchmarks and static checks are part of the standard toolchain. A combat rule, map generator or save conversion can be a normal Go package with tests that do not open a window. Bunyip also supports offscreen rendering for visual checks; those checks still need a Vulkan implementation.
The same packages can support an asset converter, game server and desktop client. For example, a server and client can import a shared package of combat rules, while a command-line tool validates the same level format that the game loads. That reuse reduces duplicated logic; deterministic simulation still requires deliberate choices about ordering, randomness and numeric behavior.
Built-in profiling, connected to the running game
Performance problems are easier to fix when you can see where time and memory go. Go includes CPU and heap profiling and execution tracing in its standard tooling. These tools can inspect both your game code and the Go engine underneath it, helping you follow a slowdown across package boundaries.
Bunyip already exposes this as an option: set Config.Pprof to
"127.0.0.1:6060" to start its profiling server. Leave it empty to skip
starting the server. You can then capture a CPU profile while reproducing
a slow scene, inspect heap allocations, or record a trace to investigate
goroutine scheduling and blocking. The profiling walkthrough
shows the configuration and commands.
For example, a slow turn might come from repeated path searches; a frame spike might coincide with rebuilding temporary data. Profiles help locate those costs before you rewrite a subsystem. Bunyip's frame overlay and named profile scopes connect those investigations to individual game phases. CPU profiles do not measure GPU shader execution, so use the frame timings to decide whether the next investigation belongs on the CPU or GPU side. See the Go diagnostics guide for the tools and their interpretation.
Concurrency for work around the frame
Goroutines and channels make it practical to organize independent work: reading assets, decoding images, receiving network messages or preparing a save. Bunyip's asset loader already manages background workers, so game code can collect finished results instead of managing a worker pool itself.
Keep the boundary clear: workers prepare CPU data, and the game's callbacks create GPU resources and update the live game state. Sharing mutable state still needs synchronization. Starting a goroutine for every entity is not automatically faster, and concurrent execution does not establish a stable simulation order. Concurrency is most useful when the work and its result have a clear owner.
Managed memory with an explicit frame budget
Go manages the lifetime of ordinary Go memory. Component structs, decoded data and temporary application values do not each need a destructor. This reduces lifetime bookkeeping while building and changing a game.
Garbage collection still costs CPU time and memory, and it can affect frame timing. Allocation rate and the live object graph both matter. Reuse buffers in frequently executed code, avoid rebuilding large temporary structures every frame, and profile before adding pools or tuning the collector. Go's garbage collector guide explains these costs and the memory-versus-CPU tradeoff. Go provides no hard real-time guarantee.
GPU resources and open files have separate lifetimes. Bunyip's Graphics
owns its GPU resources, and Context.Cleanup handles registered cleanup
at the end of a context. Managed memory and explicit resource ownership
work together: the collector is not the mechanism for unloading a texture
or closing an asset pack.
A simpler build and distribution path
Bunyip builds with CGO_ENABLED=0. Its Go code calls native graphics,
windowing and audio APIs at run time, so building the engine does not
require a C compiler or platform SDK headers. The project cross-builds for
Linux, Windows and macOS on amd64 and arm64 using the Go toolchain.
The resulting game does still need the appropriate native libraries and graphics driver on the target machine. Cross-compilation also does not replace testing input, audio and graphics on that platform. The getting started guide lists runtime requirements and current hardware verification, and describes Bunyip's bundling tool.
Choosing Go means choosing its tradeoffs
Go suits Bunyip's approach: a code-driven engine with reusable packages, typed game state, built-in development tools and desktop native rendering. It is especially appealing when you want the game, its tools and its server to share a language and want to be able to modify the engine yourself.
Your required platforms and workflow still decide whether that is a good fit. Bunyip currently targets desktop systems; Go's support for other targets does not supply Bunyip with browser, mobile or console backends. If your project depends on a visual scene-authoring workflow, a particular middleware integration or strict control of every allocation and scheduling decision, evaluate those requirements before committing to an engine.
To see the approach in practice, open your first window or read how the API manages ownership and cleanup.