Zero to Hello World in Odin
Odin’s syntax is small, readable, and almost certainly familiar. If you have written Go or TypeScript, you will recognise the shape of it within minutes. There are no hidden conversions, no implicit type coercions, and no build configuration ceremony just to connect multiple files together.
The stereotype of systems programming as inherently hostile does not hold here.
In this article, you will:
- compile and run your first Odin program,
- declare typed variables and constants,
- use explicit type conversions,
- and understand the basic structure of an Odin program.
We are going to start with the simplest possible program and build up from there.
Your First Odin Program
Create a directory named learn_odin. Inside that directory, create a file named starter.odin and paste in the following code. It demonstrates the four things you will learn in this part: type inference, explicit typing, compile-time constants, and explicit type conversion.
package main
import "core:fmt"
main :: proc() {
// Variable declaration with type inference
age := 20
// Explicit type declaration
target_distance: f64 = 42.195
// Compile-time constant
MAX_VELOCITY :: 300
// Explicit type conversion
age_as_float := f64(age)
fmt.printf(
"Age: %v, Distance: %v, Constant: %v, Cast: %v\n",
age,
target_distance,
MAX_VELOCITY,
age_as_float,
)
}
To run this code, open your terminal, navigate to the learn_odin directory, and execute:
odin run .
This command builds the executable and immediately runs it. If you only want to compile without running:
odin build .
That produces an executable you can run separately.
Anatomy of an Odin Program
You are already reading and running Odin code. Let’s break down the code into its structure and meaning.
Odin organises code by directory. Every .odin file in a directory belongs to the same package, and each file must begin with the same package declaration. In starter.odin, that declaration is package main.
When you run odin run ., the compiler takes every .odin file in the current directory and compiles them together as a single unit/collection. The directory is the project (or package) boundary. You do not manually list source files or maintain a build configuration just to build programs with multiple files and packages.
To use code from another package, you import it:
import "core:fmt"
The core: prefix indicates that fmt belongs to the core library collection — in essence, files that begin with package fmt. Without a prefix, the compiler looks relative to the current file.
You can also alias the import. For example, import bar "core:fmt" lets you call bar.printf instead.
The Entry Point
The entry point of an Odin program is a procedure named main, declared as:
main :: proc() {
Odin calls functions procedures. There is no distinction between functions and methods — Odin has no concept of a method at all, which may feel unusual if you are coming from an object-oriented background. Everything that does work is a procedure.
Procedures are values in Odin, and they follow the same declaration style as everything else. The general pattern is:
name : type = value
This collapses to name :: value when the type can be inferred and the value is a constant. So main :: proc() is shorthand for assigning the procedure value to the name main at compile time.
The choice to call them procedures rather than functions is deliberate. See the Odin FAQ for the reasoning — in short, the term comes from mathematics and Wirth-era languages, where a procedure is a named sequence of steps, without the implication of “returning a computed value” that the word function carries.
Variables and Type Inference
The pattern is name : type with an optional value assignment.
When you wrote target_distance: f64 = 42.195, you declared a variable named target_distance of type f64 and assigned it a value. You can omit the type and let the compiler infer it:
age := 20
Here age becomes an int. The := syntax means: declare this name, infer its type, assign this value.
Variables declared with := can be reassigned later. Constants cannot — they are determined at compile time and use :: instead. Here’s MAX_VELOCITY declared as a constant:
MAX_VELOCITY :: 300
The full form for a typed constant is name : type : value. With inference, name :: value is sufficient. Constants in Odin are used for fixed values, procedure declarations, and type aliases.
Explicit Type Conversion
Odin does not mix types silently. Even types that are logically compatible will not convert automatically:
age := 20
float_val: f64 = age // This will not compile
The conversion must be written explicitly:
float_val: f64 = f64(age)
You can also write this as (f64)(age), or write it using the cast keyword: cast(f64)age. All three forms have the same meaning. There is also the transmute operator, used for bit-cast conversion between two types of the same size.
This strictness is not over-the-top. Every place where precision or representation changes is now visible in the code. An entire category of invisible bugs — the kind that silently truncate a 64-bit float into a 32-bit integer — simply cannot occur implicitly.
Mini Recap
You have written and run your first Odin program. To recap:
- Use
odin run .to compile and run the program in the current directory. - Use
odin build .to compile without running. - Use
:=for type-inferred variable declarations. - Use
: typefor explicit type declarations. - Use
::for compile-time constants and procedure bindings. - Use
Type(value)for explicit type conversions.
The Next Step
Now that the syntax feels familiar, we can tackle the concern that turns most developers away from systems programming before they even start: memory management.
In the next part, you will meet two concepts that make manual memory management not just manageable, but simple.