(a)RManos Blog

Golang developers should try Odin

The Hitchhiker’s Guide to Valhalla

  ·   14 min read

(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:

  1. No matter what the person with the programming socks told you, Odin does not have security vulnerabilities in its memory allocators.
  2. Memory leaks can occur in all programming languages, but what matters is how easily you can find them and fix them.
  3. 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