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.