Why Odin & Thinking in Odin
Think of this page as the map you unfold before a long walk. You do not need to memorise it. You just need to know the shape of the territory: what Odin optimises for, what it deliberately leaves out, and which habits will make the rest of this track feel natural.
Meet Odin
Odin is a general-purpose, statically typed, compiled programming language. You write a .odin file, the compiler reads it, and you get a native executable — no virtual machine, no interpreter, no surprise garbage collector pausing your program in the middle of a frame.
What Odin Is
In practical terms, Odin is the language you reach for when you want C-level control without C-level pain:
- Fast native code. The Odin compiler produces machine code through LLVM, the same mature backend used by many production compilers.
- Small, regular syntax. The whole language is designed to fit in a person's head — the author's stated goal is that the entire specification should be memorisable by a mere mortal.
- Explicit over implicit. Memory, errors, and control flow are visible in the source instead of hidden behind machinery.
- Batteries included. A high-quality
core:library and a largevendor:collection of bindings ship with the compiler. - Friendly to C. You can call C libraries directly, which keeps decades of existing code available to you.
- Open source under the zlib licence — one of the most permissive licences in existence.
If you already know Python, C, Go, or C++, you will recognise a lot here, and that is on purpose. Odin borrows deliberately from Pascal, C, Go, Oberon-2, Newsqueak, and GLSL.
Who Made It, and Why
Odin was created by Bill Hall, known in the community as Ginger Bill. The project began one evening in late July 2016, after one too many frustrating hours spent programming in C++. He first tried to write a preprocessor that would add missing capabilities to C — and concluded that was a dead end. The alternative was to start again with a clean sheet, and that is exactly what happened.
The design idols behind the language are Niklaus Wirth (Pascal, Modula, Oberon) and Rob Pike (Go, Plan 9). If those names are familiar, the rest of Odin's taste will make sense immediately: a handful of small, orthogonal building blocks that compose well, rather than a wide surface of special cases.
The Guiding Principles
Every language is a pile of compromises. What makes a language feel coherent is that the compromises are made in a consistent direction. Odin's direction is written down openly by its author — five ideas that decide which features get in and which stay out.
Simplicity and Readability
Odin is minimal on purpose: "there ought to be one way to write something". When a language offers three ways to do the same thing, every reader has to understand all three, and every codebase slowly splits into dialects. Odin would rather give you one good way and stop there.
A related line from the design notes is worth remembering, because it explains a great deal of what follows:
Code is about expressing algorithms — not the type system.
That is why Odin has no operator overloading, no inheritance hierarchies, and no clever metaprogramming tricks in ordinary code. The type system is there to describe your data clearly, not to become the subject of the program.
Orthogonality — Small Pieces That Combine
An orthogonal design means the features do not interfere with each other. for works with ranges, arrays, slices, maps, and enumerations without needing a special loop for each. A procedure can return several values whether it is called from a loop, a when block, or another procedure. You learn one feature and it keeps working in new places.
This is also why Odin's syntax is so uniform: declarations put the name first and the type after, everywhere. Once you have read one declaration, you can read all of them.
The Joy of Programming
The fourth principle is refreshingly human: we got into programming because we love to solve problems, so our tools should bring us joy while doing it. In practice that means fast compiles, clear error messages, no build-system archaeology, and a standard library that is pleasant to call. Fun is not decoration here — it is a design requirement.
Nothing Important Is Hidden
Most modern languages hide useful machinery behind friendly syntax: a + that secretly calls a function, a property that secretly runs a query, an exception that secretly unwinds your stack. Odin's stance is the opposite — if a line of code does something, you should be able to see it.
No Methods, No Inheritance
Odin has no methods and no classes. Data lives in types, behaviour lives in procedures, and the two are joined by ordinary calls. There is no hidden table of functions attached to your struct, so a struct is exactly as big as its fields say it is — nothing more.
Here is the whole idea in a few lines. Notice how the procedure takes the data as a normal parameter:
import "core:math"
// A type is just data. No hidden function table, no constructor, no destructor.
Vector2 :: struct {
x, y: f32,
}
// Behaviour is a plain procedure that TAKES the data.
// Reading the call `length(v)` you can see nothing secret can happen.
length :: proc(v: Vector2) -> f32 {
return math.sqrt(v.x*v.x + v.y*v.y)
}
If you are used to object-oriented languages this may feel like a step backwards for a week, and like fresh air for the month after that. Every call site tells you exactly which procedure runs.
No Exceptions
Odin has no try, no catch, and no throwing. Failure is reported as an ordinary value in the result of a procedure, so a function call can never silently teleport you out of the code you are reading.
The simplest form is a second return value that says whether the operation worked:
import "core:fmt"
// The procedure reports failure in its RESULT, not by throwing.
// We ignore the real parsing for now — the shape of the signature is the lesson.
parse_age :: proc(text: string) -> (age: int, ok: bool) {
return 0, false
}
main :: proc() {
age, ok := parse_age("42")
// The check is mandatory and visible: nothing can skip this line for us.
if !ok {
fmt.eprintln("that is not an age")
return
}
fmt.println("age is", age)
}
Odin also has or_return to save you from writing those checks in a long chain of calls. We will meet it properly in the Errors, defer & or_return lesson — for now just remember: errors are values you can hold, inspect, and pass along.
Allocation Is Visible
There is no garbage collector in Odin. Memory comes from an allocator — a strategy you choose — and the current one lives in a small structure called the context. Built-ins such as make and new take memory from that context unless you hand them an allocator yourself.
The point is not that you must write more code. The point is that nothing allocates behind your back, and you always have a place to look:
import "core:fmt"
main :: proc() {
// make() uses the CONTEXT's allocator. Nothing is secret:
// you could read it right here as `context.allocator`.
numbers := make([dynamic]int, 0, 8)
defer delete(numbers) // hand the memory back when this scope ends
// append may grow the buffer — using that very same allocator.
append(&numbers, 10, 20, 30)
fmt.println(numbers[:], len(numbers), cap(numbers))
}
Built for Data on Modern Hardware
Here is where Odin stops being "a nicer C" and develops its own opinion. Modern CPUs spend most of their time waiting for memory, not computing. A program that lays its data out well can be several times faster than an identical algorithm laid out badly. Odin treats that as a language-level concern.
Struct-of-Arrays, On Request
Normally, an array of records stores each record whole — field after field, one record after another. This is called AOS, array-of-structs, and it is what almost every language gives you.
SOA, struct-of-arrays, flips the layout: every field becomes its own contiguous array. A loop that only reads x then touches nothing but x values packed tightly together — which is exactly what the cache wants. Odin has built-in support for declaring a collection in SOA form, so you do not have to hand-split your data.
Array Programming
Odin lets arithmetic work on whole arrays of numbers, element by element. The loop still happens — but it is not cluttering your source, and the compiler is free to vectorise it.
import "core:fmt"
main :: proc() {
// Array programming: the same operator works on whole arrays of floats.
// `c := a * b` multiplies the two arrays ELEMENT BY ELEMENT — no loop in sight.
a: [4]f32 = {1, 2, 3, 4}
b: [4]f32 = {10, 20, 30, 40}
c := a * b
fmt.println(c) // [10, 40, 90, 160]
}
The same idea extends to swizzles and SIMD types, which is how Odin reaches the performance of hand-written graphics and scientific code without leaving the language.
Batteries Included
A language is only as productive as its library. Odin ships two: one you use every day, and one that connects you to the outside world.
The core: Library
Anything imported as core:something is part of the standard library that comes with the compiler — no downloads, no package manager, no version conflicts. It covers the ground you would expect: formatting, strings, UTF-8, slices, dynamic arrays, maps, math, time, files, processes, threads, and reflection.
// Every core package is imported by name, with the `core:` prefix.
import "core:fmt" // printing and formatting
import "core:strings" // string building and searching
import "core:os" // files, environment, exit codes
The vendor: Library
Imports beginning with vendor: are ready-made, officially maintained bindings to popular third-party libraries. Odin provides its own bindings for the major graphics APIs — OpenGL, Vulkan, Direct3D 11 and 12, Metal, wgpu, and WebGL — and for widely used libraries such as SDL2, GLFW, raylib, miniaudio, and microui.
// vendor: gives you bindings maintained by the Odin team itself,
// so the headers, types, and calling conventions already match.
import "vendor:raylib" // games, graphics, audio
import "vendor:glfw" // windows, input, OpenGL contexts
And when a library is not covered, Odin's foreign interface lets you call C directly. That is the subject of Foreign Interface (C Interop) later in the track.
Odin Next to C, C++, and Rust
You do not need to know any of these languages to learn Odin, so feel free to skim this section and come back to it later. It is here to answer one honest question: why would anyone build another systems language?
A Feature Comparison
| Concern | C | C++ | Rust | Odin |
|---|---|---|---|---|
| Memory | malloc / free | Manual plus RAII destructors | Ownership checked by the compiler | Manual, through explicit allocators and a context |
| Failure | Return codes, easy to ignore | Exceptions, sometimes expected | Result with the ? operator | Errors as values, ok and or_return |
| Reuse model | Functions only | Classes and inheritance | Traits and generics | Procedures, structs, parametric polymorphism |
| Metaprogramming | Preprocessor macros | Templates and macros | Macros and generics | Compile-time when blocks |
| Build | Make, CMake | Make, CMake, MSBuild | Cargo | Built into the compiler: odin build |
| C interop | Native | extern "C" | Through FFI bindings | Direct, with zero-cost bindings |
| Feel | Small but unforgiving | Enormous surface area | Strict but safe by default | Small, regular, and explicit |
The pattern is deliberate. Odin keeps C's performance and closeness to the machine, borrows C++'s control and Go's simplicity, and avoids the complexity that makes large systems hard to reason about.
Thinking in Odin
Philosophy is only useful if it changes your hands. Here are the five habits that will make this track — and every Odin program you write afterwards — feel natural.
Five Habits to Carry Forward
- Read the signature before the body. Odin declares everything in the same order — name first, then type — so
name :: proc(args) -> resultsalready tells you what memory is used and which failures are possible. - Ask "where does the memory come from?" Before using anything that grows, find its allocator and decide who frees it. If you cannot answer, the design is not finished.
- Treat failure as a value. Nothing throws. Check the second return value, or propagate it with
or_return, and your program stays readable top to bottom. - Move work to compile time when you can. If a value or a branch is fixed while the compiler runs,
whenand parametric polymorphism will remove it from the final binary entirely. - Design the data, then the code. Choose a layout that matches how you actually loop — contiguous, cache-friendly, and SOA where it pays off. In Odin this is a choice you make, not one the language makes for you.
Continue with Toolchain & Environment →