site

personal website, served at mordaunt.dev/site
Log | Files | Refs

why-people-hate-go.md (10802B)


      1 +++ 
      2 draft = false
      3 date = 2025-07-23T14:16:55-03:00
      4 title = "Why People Love to Hate Go"
      5 description = "Go has good ideas but never gives them to you completely."
      6 slug = ""
      7 authors = ["Jack Mordaunt"]
      8 tags = ["Go", "Odin"]
      9 categories = ["development", "opinion"]
     10 externalLink = ""
     11 series = []
     12 +++
     13 
     14 > Go is very idiosyncratic, approximating good ideas and relying heavily on data transposition.
     15 
     16 In this opinion article I will higlight why I think people love to hate Go.
     17 
     18 I'll do so by contrasting Go to Odin. Odin serves as an excellent comparison because the
     19 syntax is similar in essence (both heavily inspired by Pascal and C).
     20 
     21 Unlike Go, Odin actually gives you the good ideas in their completeness -
     22 without a complex type system or giving up any control, and without needing to
     23 fight the ideas for a decade only to finally concede.
     24 
     25 This opinion is not a knock against Go authors, or community of which I respect and have
     26 been a part for a long time.
     27 
     28 Modern versions of Go have addressed most of the examples highlighted here.
     29 This serves to reinforce the underlying of the article: that these things
     30 are valuable, proven by the fact that Go eventually conceded on them.
     31 
     32 - Go now has generics
     33 - Go now has iterators
     34 - Go now has an `any` alias that is more clear naming than `interface{}`
     35 
     36 What follows is not an exhaustive list, but rather just highlights to frame the point being made.
     37 
     38 ## Generics for Me but Not for Thee
     39 
     40 > Now, time to beat the horse some more.
     41 
     42 Generics: when the relationship _between_ data is the thing you are trying to
     43 encode, not the shape of the data itself.
     44 
     45 ### Go
     46 
     47 For the longest time Go had no user-level generics.
     48 
     49 It did have generics, but only for the runtime!
     50 
     51 This is ironic, because it proves the utility of generics while at the very same
     52 time denying it to the user.
     53 
     54 Go has relied heavily on the transpositional nature of data, using one form to represent another.
     55 It graces you with the most important structures, which alleviates the problem:
     56 
     57 - array
     58 - pointer
     59 - map
     60 - slice            (dynamic array)
     61 - channel          (thread-safe queue)
     62 - closure          (anonymous function)
     63 - multiple-returns (pseudo tuple)
     64 
     65 In many cases you can rely on what I dub "lexical generics", function closures that capture
     66 lexical values implicitly capture their types as well - making closures highly type-generic
     67 in a fascinating way. You will see this be {ab,}used in many Go codebases.
     68 
     69 Often the friction of custom data structures is high enough that the desired data structure
     70 will be transposed onto the runtime data structures.
     71 
     72 Do you want a queue or stack? Use a slice.
     73 Do you want an iterator? Use a channel.
     74 Do you want a set or graph? Use a map.
     75 Do you want an enum? Use an integer.
     76 Do you want a tagged union? Use an integer kind and a fat struct.
     77 
     78 As you can imagine these choices can come with significant penalties. Using a channel as
     79 an iterator, simply because it is both type-generic and integrates with the looping syntax, adds
     80 performance overhead due to its thread safety properties. If you don't need threadsafety, you're
     81 needlessly paying for it anyway, just to coerce the language into being ergonomic to you.
     82 
     83 It's not a virtue to rely on the transpositional nature of data structures to avoid generics,
     84 but this was the underlying argument for "why do you even need generics?".
     85 The true formulation is more like "we have given you enough generic structures,
     86 just transpose onto them".
     87 
     88 Go never really told you that generics was not needed, only that it had provided enough already.
     89 
     90 ### Odin
     91 
     92 Odin gives you parametric polymorphism: a simple form of generics that allows for the building of
     93 type-safe, generic data structures. It is "simple" because it doesn't come with a lot of features
     94 or baggage. There are no traits, no method sets, and no "complex type system".
     95 
     96 Parametric: of parameters.
     97 Polymorphism: of many shapes.
     98 
     99 Put together it means "parameter that can take on many shapes". The Odin
    100 community shortens this to "parapoly" for brevity.
    101 
    102 When people say they want generics, parapoly is usually what they want.
    103 
    104 The reality: types are a compile-time-known datum. The idea that you cannot
    105 parameterise structures and functions by a simple shape known at compile time
    106 is silly. The problem domain: not very complex. This will rhyme when we talk about
    107 dynamic types.
    108 
    109 The punchline: Odin gives you what you actually wanted, parametric polymorphism.
    110 It doesn't expect you to transpose yourself into oblivion.
    111 
    112 ```odin
    113 // A simple structure that cares about the relationship between T's,
    114 // not the shape of T itself. Tell me, where is the complex type-system?
    115 Node :: struct($T: typeid) {
    116   parent: ^T,
    117   child:  ^T,
    118 }
    119 ```
    120 
    121 ## No True Dynamic Type
    122 
    123 > Noob: How do I do dynamic typing in Go?
    124 >
    125 > Go: Here, use this: `interface{}` - it's called the "empty interface".
    126 >
    127 > Noob: ...
    128 >
    129 > Noob: Uh, what? 
    130 >
    131 > Go: I wasn't really meant for dynamic typing, use that or structure your program differently.
    132 >
    133 > Noob: Ok fine, I'll use the "empty interface" then...
    134 >
    135 
    136 Dynamic programming is important and useful, it's also not difficult to support first-class.
    137 
    138 Dynamic types reduce to a fat pointer: a structure that contains two pointers,
    139 one pointing to the data and one pointing to the type information. This allows
    140 the program to handle arbitrary values at runtime by writing logic against
    141 dynamic types.
    142 
    143 However Go is an OOP language. It encodes the very essence of OOP: behavioural polymorpshim
    144 based on v-tables. The term used in Go for this concept is the `interface`.
    145 
    146 Like a true dynamic type, the interface is indeed a fat pointer:
    147 
    148 ```go
    149 type ITab struct {
    150 	Inter *InterfaceType
    151 	Type  *Type
    152   // --snip--
    153 }
    154 ```
    155 The revealing part is the separate definition for the empty interface:
    156 
    157 ```go
    158 type EmptyInterface struct {
    159 	Type *Type
    160 	Data unsafe.Pointer
    161 }
    162 ```
    163 
    164 Why is this bothersome? Instead of just giving you an "any" type,
    165 Go forces you to talk in terms of "interfaces that have no methods".
    166 
    167 Linguistically and conceptually this makes no sense. 
    168 
    169 This is philosphically impure.
    170 
    171 Look at these beauties:
    172 - `[]interface{}`
    173 - `map[interface{}]interface{}`
    174 - `func(string, ...interface{})`
    175 - `struct { value interface{} }`
    176 
    177 A better approach is to do what Odin does: just give you the `any` type.
    178 Defined as simple as the concept is, a fat pointer.
    179 
    180 ```odin
    181 // Raw_Any points at some data, and associates type data with it.
    182 // This type is named `any` at the user-level.
    183 Raw_Any :: struct {
    184 	data: rawptr,
    185 	id:   typeid,
    186 }
    187 ```
    188 
    189 Once you understand fat pointers, dynamic programming becomes easy.
    190 
    191 Now, if you want an interface in Odin, you simply define a struct that contains
    192 a pointer to the data and a pointer to the implementation procedure (rather than
    193 a typeid).
    194 
    195 Consider the common allocator interface:
    196 
    197 ```odin
    198 Allocator :: struct {
    199 	procedure: Allocator_Proc,
    200 	data:      rawptr,
    201 }
    202 ```
    203 
    204 It's the same underlying idea (with a v-table of 1 to keep it ergonomic).
    205 
    206 Any specialised allocator just needs to map itself onto this generic Allocator
    207 structure and voilà, you have achieved runtime behavioural polymorphism!
    208 
    209 Go has rectified the language-side of this in later releases, including the
    210 `any` alias (equivalent to `interface{}` in all ways), but you can see how
    211 Go's design doesn't reason up from first principles, but rather down from empiricism.
    212 
    213 > We think objects are good idea.
    214 >
    215 > We want to do them at runtime.
    216 >
    217 > Aha, the interface type. It's a fat pointer to a vtable and some data.
    218 >
    219 > Look, if you want to do dynamic programming you can just use an empty interface!
    220 >
    221 
    222 ## Declaration and Assignment Syntax
    223 
    224 Go and Odin both use this Pascal inspired declaration-assignment syntax `:=`.
    225 
    226 ```go
    227 // Go
    228 
    229 var variable string = "variable"
    230 var variable = "variable"
    231 
    232 const constant string = "constant"
    233 const constant  = "constant"
    234 
    235 x := "foobar"
    236 ```
    237 
    238 ```odin
    239 // Odin
    240 
    241 variable := "variable"
    242 variable: string = "variable"
    243 
    244 constant :: "constant"
    245 constant: string : "constant"
    246 
    247 x := "foobar"
    248 ```
    249 
    250 What's the problem? Well, the thing is that Go defines `:=` as a keyword.
    251 You cannot actually place a type between `:` and `=`.
    252 
    253 Once again Go fails to give you the complete idea, the grammar: `<symbol> : <type> = <expression>`.
    254 Where the type can be elided to result in `<symbol> := <expression>`.
    255 Where constants are the same, but with a second `:` instead of `=`.
    256 
    257 Go has chosen an inconsistent approach. When the underlying grammar is elegant,
    258 Go decides to take the shortcut and ignore the point. Odin just gives you the
    259 elegant, consistent grammar.
    260 
    261 Consider type and proc definitions:
    262 
    263 ```go
    264 // Go
    265 
    266 type Node struct {
    267 	// --snip--
    268 }
    269 
    270 func Visit(n Node) {
    271 	// --snip
    272 }
    273 ```
    274 
    275 In the Go syntax we have introduced more keywords to declare types and
    276 functions. Why do we need extra syntax here? After all, struct and function
    277 definitions are just regular constants!
    278 
    279 In Odin, this fact is evident.
    280 
    281 ```odin
    282 // Odin
    283 
    284 node :: struct {
    285 	// --snip--
    286 }
    287 
    288 visit :: proc(n: node) {
    289 	// --snip--
    290 }
    291 ```
    292 
    293 The syntax is consistent:
    294 - consistent with other constants
    295 - consistently read left-to-right, with symbol first, type second, and finally the binding third
    296 
    297 After all, a struct defintion is a compile-time known value.
    298 Thus, the `node` type is just regular constant set to a typeid.
    299 
    300 Don't believe me? Check this out:
    301 
    302 ```odin
    303 // Odin
    304 
    305 node: struct {} : struct {
    306 	// --snip--
    307 }
    308 
    309 visit: proc(n: node) : proc(n: node) {
    310 	// --snip--
    311 }
    312 ```
    313 
    314 You _can_ put the type in the normal type position, it's just elided by convention!
    315 
    316 And if you want them to be variables instead:
    317 
    318 ```odin
    319 // Odin
    320 
    321 node: struct {} = struct {
    322 	// --snip--
    323 }
    324 
    325 visit: proc(n: node) = proc(n: node) {
    326 	// --snip--
    327 }
    328 ```
    329 
    330 Odin takes the underlying idea of elegant, minimal and consistent grammar and
    331 actually just hands it to you. Type definitions are _just_ constants!
    332 
    333 Go decides to keep the shorthand `:=` but never actually fully embraces the
    334 underlying idea. You can't put a type between `:` and `=`, and you are expected
    335 to use bespoke keywords for type and function declarations.
    336 
    337 Feel the inconsistency.
    338 
    339 ```go
    340 // Go
    341 const c int = 42
    342 const c = 42
    343 var v int = 42
    344 var v = 42
    345 v := 42
    346 ```
    347 
    348 ```odin
    349 // Odin
    350 c: int : 42
    351 c :: 42
    352 v: int
    353 v: int = 42
    354 v := 42
    355 ```
    356 
    357 ## Conclusion
    358 Go develops features empirically, adopting what works practically. Odin constructs from first principles, focusing on conceptual elegance.
    359 
    360 Go’s incremental evolution led to ad-hoc syntax and features, making it feel less coherent to users accustomed to functional or fully featured OOP languages.
    361 
    362 Conversely, Odin feels purposeful and coherent, designed from foundational ideas rather than incremental adaptations.
    363 
    364 Despite these criticisms, Go remains practical and solid, which explains its continued popularity—and why many developers both love and hate it.