Golang developers should try Odin

Lobsters Hottest Tools

Summary

This article argues that Golang developers should explore Odin, a new programming language that addresses some of Go's limitations with features like memory management and array programming.

<p><a href="https://lobste.rs/s/ytc27d/golang_developers_should_try_odin">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 09/13/26, 04:51 PM

# Golang developers should try Odin Source: [https://rm4n0s.github.io/posts/2-go-devs-should-learn-odin/](https://rm4n0s.github.io/posts/2-go-devs-should-learn-odin/) \(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](https://github.com/odin-lang/Odin/blob/master/examples/demo/demo.odin), its got almost everything you need to get started\. Yes, it does not have documentation \(no, the[overview](https://odin-lang.org/docs/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](https://github.com/rm4n0s/trace) ``` // 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](https://github.com/odin-lang/Odin/blob/master/examples/demo/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](https://github.com/odin-lang/Odin/blob/f047f804f67bd57713d058a0df6d83c49e0f3848/core/sort/sort.odin#L11)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](https://github.com/odin-lang/Odin/blob/master/examples/demo/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](https://odin-lang.org/docs/overview)and[how to bind to C](https://odin-lang.org/news/binding-to-c/)

Similar Articles

Understanding the Odin Programming Language

Hacker News Top

A book teaching the Odin programming language, covering manual memory management, parametric polymorphism, and data-oriented design. Version 1.10 has been released with updates.

Just Fucking Use Go

Lobsters Hottest

An opinionated developer essay advocates for the Go programming language, emphasizing its straightforward syntax, robust standard library, efficient concurrency model, and single-binary deployment as practical alternatives to overly complex modern technology stacks.

Odin's New Inline Assembly Templates

Lobsters Hottest

The article details new inline assembly templates for the Odin programming language, featuring a universal syntax inspired by Go's assembler and supporting amd64 targets.

Odin dev-2026-06 Released

Lobsters Hottest

Odin dev-2026-06 has been released. Odin is a data-oriented programming language designed for high-performance systems development.