(BEWARE: the article may cause seizures)
Introduction
I’ve been using Golang for 10 years, and even though I tried other programming languages, I always went back to it because of its simplicity.
It is so simple that reading the source code and playing around helped me pick up the majority of the syntax when I first got into Golang.
However, it pains me to say that Golang has problems because of its garbage collector and runtime.
Some of the problem are listed here:
- CGO
- It runs and compiles slowly
- You have to know and write C in comments!!
- It can not compile to WASM
- The language is slow compared to Rust
- It is easy to have memory leaks and they are hard to find.
- The error type is not good
But even with these problems, I still prefer Golang to other programming languages, until I met Odin a week ago, and it was love at first sight.
This article will show you the beauty of Odin.
What is Odin and who is it for
Odin is a new programming language that is
- 60% Golang,
- 20% features that you wished Golang had
- 10% array programming
- 10% low level, FFI, memory management
People think that it is only for game development just because it includes 3D libraries with the compiler, but I disagree. Odin is also for backend development because it has a core library similar to Go’s standard library. Which means that it has all the building blocks for us to copy the rest of Go’s libraries like Chinese manufacturers.
I know that some of you play around with Rust, Zig, C3 or Hare, but these do not have array programming and struct tags in their languages.
Just ask yourself:
- how will you serialize data between DBs and markup languages without struct tags?
- how will you write a server side anticheat without array programming? Don’t you want to make linux gamers happy?
It even has quaternions! Do you know what that is? Me neither! But I am excited to learn.
Odin is ready
The language will not change and it will stay stable until you retire. However, the compiler, toolchain, and core library are still in development and always improved.
Also, it is used in production from JangaFX and ChiAha™
The characteristics of Odin
The language has 31 keywords, but each keyword may have a #directive or @(attribute). It may seem too much for someone that cannot remember more than 26 keywords like me, but all the keywords, directives, and attributes connect so seamlessly together like reading English.
For example, the switch-case that works like in Golang requires from the user to specify every case, unless you put the #partial directive in front of it.
It will really make sense to you how easy it is for you to understand the code without knowing the language. Take 10 minutes to read demo.odin, its got almost everything you need to get started.
Yes, it does not have documentation (no, the overview is not a real documentation), but that is the beauty of it. You learn to code in Odin by reading other’s people code. Most of the standard library does not have comments or examples, but you can easily understand how to use it through its source code.
No, it does not have macros, or comptime, or constexpr or decorators and it is better that way because every language that has them makes developers to waste 90% of their time googling for every library that uses them.
Trust me, stick with Odin’s pre-defined directives and attributes.
Yes, it has generics, and they are more well-designed than Golang’s generics.
No, it does not have a package manager, or go.mod file or anything else. The package is just a directory that you just copy to your project, and you call it relative to the path of the source code file.
For example,
// git clone https://github.com/laytan/odin-http
// vim main.odin
package main
import http "odin-http"
Yes, compiler is as simple as Golang’s compiler.
cd myproject
odin build .
odin run main.odin -file
Yes, it has VSCode plugin with autocompletion.
The trade offs
Odin has manual memory management, which is why it does not have Closures, Composition, Goroutines, and Selectors. However, I will demonstrate how you can live without them.
Secondly, it lacks libraries, which is why I’m trying to get you to learn it so you can copy your libraries from Go to Odin.
Lastly, it does not have documentation or books, but if you have years of experience in Go, then Odin will feel like home.
Living without a garbage collector
Error handling
Living without a garbage collector means that you don’t have errors.New() or fmt.Errorf(). There is no error type in Odin. You only have Enums, Unions and Structs, and that is better than Go’s errors.
Go’s errors suck
Golang’s errors makes you push server-side error messages to users, and you cannot do anything to stop it. That is not a good thing because users don’t need to know what is SQL or that the bank_account is nil.
Furthermore, another problem is that Go’s errors do not have stack traces.
Now look at the superpower of Odin
For each function you create, also include an enum or a union or a struct to represent the error for that specific function.
Payment_Error :: enum {
None,
Bank_Account_Is_Empty,
}
Pay_Half_Debt_Error :: union #shared_nil {
Payment_Error,
json.Marshal_Error,
}
Save_House_Error :: union #shared_nil {
Pay_Half_Debt_Error,
Is_Even_Worth_Saving_Error,
}
// etc
When a function returns an error that it received from another function, and so forth, you will receive an error value similar to this.
err := Extend_Deadline_Error(
Save_House_Error(
Pay_Half_Debt_Error(
Payment_Error.Bank_Account_Is_Empty
)
),
)
From there you can create trees of switch statements that return proper user messages.
#partial switch save_house in err {
case Save_House_Error:
pay_half := save_house.(Pay_Half_Debt_Error)
#partial switch pay in pay_half {
case Payment_Error:
#partial switch pay {
case .Bank_Account_Is_Empty:
fmt.println("You have 24 hours to leave the house.")
}
}
}
And also you can print it like a stack trace using my library
// this condition works thanks to #shared_nil directive in union
if err != nil {
tr := trace.trace(err)
defer delete(tr) // defer works in scopes
fmt.println(tr)
}
// prints: Extend_Deadline_Error -> Save_House_Error ->
//Pay_Half_Debt_Error -> Payment_Error.Bank_Account_Is_Empty
Even by looking the type of errors you can guess what the functions do.
No other programming language can do that.
If Odin can’t help the economy, then nothing can.
Memory management
Lets be clear
Before we talk about memory management let me clarify three things:
- No matter what the person with the programming socks told you, Odin does not have security vulnerabilities in its memory allocators.
- Memory leaks can occur in all programming languages, but what matters is how easily you can find them and fix them.
- If you don’t handle memory yourself, then Odin will never accept you in Valhalla.
A mini introduction
If you read demo.odin then you already know how pointers work, but if you haven’t already then just remember:
n : ^int // this is how you assign pointer type
n = new(int) // this is how you create a new pointer
assert(n^ == 0) // remember that all pointers have default values
n^ = 2 // this is how you assign a value to a pointer
fmt.println(n^) //this is how you read the value from a pointer
free(n) //this is how you free a pointer
// make()/delete() is for strings, array and maps.
// For the rest of the types use new()/free()
arr := make([dynamic]int)
delete(arr)
// make(),delete(),new() and free() accept allocators
// if you don't add an allocator then they use the default.
// context.allocator is the default allocator
my_allocator := context.allocator
m := new(int, my_allocator)
// you can also change the default allocator
context.allocator = my_allocator
// import "core:mem" has many other types of allocators
// like Tracking_Allocator to track leaks
// and Panic_Allocator useful for spaceship software
The gotchas
If you double delete() or double free() a pointer, then the you will get “Segmentation fault”
arr := make([dynamic]int)
delete(arr)
delete(arr) // causes "Segmentation fault"
If you read pointer after free() or delete(), you will get random value
n = new(int)
n^ = 2
free(n)
fmt.println(n^) // it will not print 2 but something random
If you make() an array of new() pointers then you have to free() the pointers inside the array before deleting the array, or else it will cause a memory leak.
arr := make([dynamic]^int)
for i in 0 ..< 10 {
n := new(int)
n^ = i
append(&arr, n)
}
// if you don't free each item
// for n in arr {
// free(n)
// }
// then this will leak
delete(arr)
My rules to avoid gotchas
For your global pointers, assign nil after you deallocate them and check for nil before you deallocate, read, or assign the pointer.
global := new(int)
if global != nil {
free(global)
// after deallocation assign nil to the pointer
global = nil
}
// so other procedures can:
// - check before assign
if global != nil {
global^ = 2
}
// - check before they read
if global != nil {
fmt.println(global^)
}
// - check for nils and not double free
if global != nil {
free(global)
global = nil
}
For your local pointers, use defer to deallocate the pointer. Always put defer under the allocation so it is easy to see the deallocation.
{
n := new(int)
defer free(n)
n^ = 2
fmt.println(n^)
// defer will run the free() here
}
If a procedure contains the parameter “allocator := context.allocator” then 99% of the time the results need to be deallocated. This means that you have to call delete()/free() or call another procedure from the same library that will deallocate the pointer for you.
// when a procedure creates a pointer with a deallocator then it has name that ends with _init
multi_reader_init :: proc(
mr: ^Multi_Reader,
readers: ..Reader,
// always look at the end of parameter lists for "allocator"
allocator := context.allocator) -> (r: Reader)
// its deallocator has a name that ends with _destroy
multi_reader_destroy :: proc(mr: ^Multi_Reader)
Use Arena_Allocator to allocate an array of pointers and then just destroy the arena.
arr_arena: virtual.Arena
// create arena allocator
arena_allocator := virtual.arena_allocator(&arr_arena)
// create the array using the arena allocator
arr := make([dynamic]^int, allocator = arena_allocator)
for i in 0 ..< 10 {
// include the items in the arena
n := new(int, allocator = arena_allocator)
n^ = i
append(&arr, n)
}
// delete everything in a single sweep
virtual.arena_destroy(&arr_arena)
Interfaces
Odin does not have interfaces, which is why I say that they work like quantum entities. When you look at them, they act like interfaces, but when you don’t look at them, they are just pointers.
There are two ways to write interfaces in Odin.
Do it like in Go
This example emulates composition.
Stringer_Interface :: struct {
data: rawptr,
sprint: proc(
si: Stringer_Interface,
allocator := context.allocator) -> string,
}
Book :: struct {
name: string,
}
new_stringer_book :: proc(b: ^Book) -> Stringer_Interface {
return Stringer_Interface {
data = rawptr(b),
sprint = proc(si: Stringer_Interface,
allocator := context.allocator) -> string {
context.allocator = allocator
data := cast(^Book)si.data
return fmt.aprint("The name of the book is", data.name)
},
}
}
print :: proc(si: Stringer_Interface) {
str := si->sprint()
defer delete(str)
fmt.println(str)
}
main :: proc() {
odin_book := Book {
name = "Odin book",
}
stringer_book := new_stringer_book(&odin_book)
print(stringer_book)
}
Also, this is how interfaces used in Odin’s standard library.
For example, sort.Interface in Odin is copied from Go using this type of interface structure.
Do it like in Java
Here is an example that looks like Java’s abstract class.
Shape_Abstract :: struct {
width: int,
height: int,
get_area: proc(shape: ^Shape_Abstract) -> int,
}
Penis :: struct {
using _: Shape_Abstract,
name: string,
}
new_penis :: proc(name: string, width, height: int) -> Penis {
return Penis {
name = name,
width = width,
height = height,
get_area = proc(shape: ^Shape_Abstract) -> int {
data := cast(^Penis)shape
return data.width * data.height
},
}
}
print_area :: proc(shape: ^Shape_Abstract) {
fmt.println("Area:", shape->get_area())
}
main :: proc() {
my_penis := new_penis("mini me", 5, 23)
print_area(&my_penis)
}
Also, you can’t add more than one interface to a struct or you’ll start getting weird shit or segmentation faults. So in the end, it also works like in java.
Threads
Odin does not have goroutines or selectors because they need garbage collectors to work.
However, I have written a complete example just for you on how to emulate goroutines and selectors in Odin.
This example demonstrates the feeding process for baby birds from their parents.
(You know the process. Mama bird throws up on the kids mouth to feed them. Isn’t nature beautiful?)
package main
import "core:fmt"
import "core:mem"
import "core:strconv"
import "core:sync"
import "core:sync/chan"
import "core:thread"
import "core:time"
Parent_Enum :: enum {
Father,
Mother,
}
Food_From_Father :: struct {
papa_index: int,
}
Food_From_Mother :: struct {
mama_index: int,
}
Food :: union {
Food_From_Father,
Food_From_Mother,
}
KidData :: struct {
kids_wait_group: ^sync.Wait_Group,
mouth: ^chan.Chan(Food, chan.Direction.Recv),
}
ParentData :: struct {
parent_type: Parent_Enum,
num_foods: int,
parents_wait_group: ^sync.Wait_Group,
mouth: ^chan.Chan(Food, chan.Direction.Send),
mouth_mutex: ^sync.Mutex,
}
parent_task :: proc(t: ^thread.Thread) {
data := (cast(^ParentData)t.data)
fmt.println(data.parent_type, "starts feeding")
for i in 1 ..= data.num_foods {
food: Food
if data.parent_type == .Mother {
food = Food_From_Mother {
mama_index = i,
}
}
if data.parent_type == .Father {
food = Food_From_Father {
papa_index = i,
}
}
// if you don't add mutex, then at least once,
// a parent will send the same food index to two kids
// while the other parent does not send any food
// for the same food index (which I don't understand why)
sync.mutex_lock(data.mouth_mutex)
chan.send(data.mouth^, food)
sync.mutex_unlock(data.mouth_mutex)
// wait to throw up new food
time.sleep(500 * time.Millisecond)
}
sync.wait_group_done(data.parents_wait_group)
fmt.printfln("%v's feeding stopped", data.parent_type)
}
kid_task :: proc(t: thread.Task) {
data := (cast(^KidData)t.data)
fmt.println("kid", t.user_index, "opens mouth")
for {
msg, ok := chan.recv(data.mouth^)
if !ok {
fmt.println("mouth closed for kid", t.user_index)
break
}
switch food in msg {
case Food_From_Father:
fmt.println("kid", t.user_index,
"received food", food.papa_index,
"from father")
case Food_From_Mother:
fmt.println("kid", t.user_index,
"received food", food.mama_index,
"from mother")
}
// wait to chew their food
time.sleep(time.Second)
}
fmt.println("kid", t.user_index, "finished eating")
sync.wait_group_done(data.kids_wait_group)
fmt.printfln("kid %d went to bed", t.user_index)
}
main :: proc() {
parents_wg: sync.Wait_Group
kids_wg: sync.Wait_Group
num_kids := 5
// create feeding pipe
mouth, err := chan.create(chan.Chan(Food), context.allocator)
defer chan.destroy(mouth)
mouth_mutex := sync.Mutex{}
// create mama bird
mama_mouth := chan.as_send(mouth)
mama_thread := thread.create(parent_task)
defer thread.destroy(mama_thread)
mama_thread.init_context = context
mama_thread.user_index = 1
mama_thread.data = &ParentData {
parent_type = .Mother,
num_foods = 8, // with more food
parents_wait_group = &parents_wg,
mouth = &mama_mouth,
mouth_mutex = &mouth_mutex,
}
// create lazy father bird
papa_mouth := chan.as_send(mouth)
papa_thread := thread.create(parent_task)
defer thread.destroy(papa_thread)
papa_thread.init_context = context
papa_thread.user_index = 2
papa_thread.data = &ParentData {
parent_type = .Father,
num_foods = 6, // with less food
parents_wait_group = &parents_wg,
mouth = &papa_mouth,
mouth_mutex = &mouth_mutex,
}
sync.wait_group_add(&parents_wg, 2)
thread.start(mama_thread)
thread.start(papa_thread)
// create a nest for kids
nest: thread.Pool
thread.pool_init(&nest,
allocator = context.allocator,
thread_count = num_kids)
defer thread.pool_destroy(&nest)
sync.wait_group_add(&kids_wg, num_kids)
for i in 1 ..= num_kids {
kid_mouth := chan.as_recv(mouth)
data := &KidData{
kids_wait_group = &kids_wg,
mouth = &kid_mouth
}
// add kid to the nest
thread.pool_add_task(
&nest,
allocator = context.allocator,
procedure = kid_task,
data = rawptr(data),
user_index = i,
)
}
thread.pool_start(&nest)
// first we wait for parents to stop feeding kids
sync.wait_group_wait(&parents_wg)
fmt.println("all parents stopped feeding them")
// everybody closes their mouths
chan.close(mouth)
fmt.println("kids close their mouths")
// we wait for all kids to sleep
sync.wait_group_wait(&kids_wg)
fmt.println("all kids slept")
// run this or else the program will never close
thread.pool_finish(&nest)
}
If you haven’t figured out what is the selector and the goroutines in this example, then look at the union and threadpool.
One channel that accepts the union type message is equivalent to using two channels and a selector in Golang because each individual type in the union is equivalent to a channel in a selector. The union emulates the selector.
The threadpool is equivalent to Golang’s runtime because it runs the threads in a head of time that will be used later on to run the goroutines.
Just remember your university’s Java course on concurrency, use threads for long-running tasks, threadpools for small tasks and paracetamol for deadlocks.
In conclusion
With this article, I hope now you understand Odin’s potential for the future and as a Golang’s replacement for backend development.
By the way, if you read demo.odin and my explanations without skipping anything, then from now on you are a Senior Odin Developer. (ooops!)
Congratulations!
To be a Grandmaster Odin Developer, just scroll overview and how to bind to C