Hacker News new | ask | show | jobs
by tschellenbach 1990 days ago
We have a very large Go codebase here at Stream and not having generics is just not really as big of an issue as you think it is. There are plenty of work arounds if you get used to not having generics in the language. The fast compile times of Go are amazing. I was doing some Kotlin a few weeks ago and the difference is crazy. Go: Install deps, compile everything done in 5s. Doing the same in Kotlin, laptop freezes, android studio freeze, time to get a coffee :)

That being said it would be really nice to have some reusable map type structures that handle GC better than the default maps. Fingers crossed.

14 comments

I remember thinking something similar about my Java 1.4 codebase back in the early 2000s. It's fine, right? What could I be missing?

The answer is: A lot.

People from the post-1.4 era of Java aren't aware how huge code generation was before generics showed up.

And then all that momentum, all the tools, the books, the conference presentations, the code ... all of it. Pop! It died. Because code generation sucks. It is the worst solution to any problem solvable by a type system.

Here is a good one, using Eclipse EMF for code generation having as input UML data files generated from Rational Rose.
My favorite example of generics wonkiness was when I needed a channel to wrap an untyped channel to avoid "infecting" every call site for a utility function with untyped pointers.

I thought it was madness, but bringing it up to a very large Golang group and get "nope channels are cheap! That's fine! There's repetition but it's easy to follow"

I've said before, my personal take is use Go, get a feel for the Go mentality, then take it with you to another language.

Go is just too stuck between low level and high level for me personally. I'd rather go under with Rust or over with Kotlin or C#

Go (without generics) is not Java 1.4.

Go's built-in collections are actually generic and Java's generic-less awkwardness was mostly about collections.

I know, right? In Java 1.4 you cast to Object, in Go you cast to interface{}. Totally different.
Most of the casting in Java 1.4 was from collections of Object. In Go the collections are typed, so the casting is confined to some very specialized pieces of code.
The only difference between Go and Java 1.4 in terms of collections is that Go has a generic map type, which Java didn't. Java 1.4 still had generic arrays, just like Go. In fact, it was a little easier to program with generic arrays in Java vs Go (but also less safe) because in Java a subtype[] can be passed to a function which takes a supertype[] (arrays are covariant).
Yeah, right because we never been there before.

    class IntList {
       private List objList;

    public add(int elem) {
       objList.add(Integer.of(elem));
    }

   }
Rest is left as exercise for the readers that never used Java generics, or C++ templates.
I miss Java 1.4. It was small and concise. Java 5 added so much that none knows all of it. Just look at the length of Java Generics FAQ. It's hilarious.
I've

- read that from start to end exactly zero times,

- picked up generics in a day or two,

- struggled with advanced types twice, half a day each time

- struggled with type erasure twice, also about half a day each time.

Meanwhile generics often saves me a number of minutes pr hour and makes everything cleaner and easier to read.

I used Java before generics but once it arrived I never looked back and neither did anyone i know.

Java before 5 wasn't a language, it was a library and number of jvm implementations. It wasn't until Java 5 that there was a memory model spec that defined how stuff was supposed to work.
Yeah that's ok until you have 20 million lines of generic ridden crapola pumped out by the lowest bidder. That's the hell I spent a good chunk of the last few years untangling on the C# front. Let's model this correctly! Oh no someone said fuck it, lets just use a bunch of generic data types!

Dictionary<List<Dictionary<string, object>>>, SortedSet<Dictionary<int, string>>>>

Several thousand out of bounds, missing keys, null reference exceptions, hash collisions and the hair starts to get thin on top. I'm not even sure I'm happy with it for abstract data types.

Please can we keep Go special.

> Several thousand out of bounds, missing keys, null reference exceptions, hash collisions and the hair starts to get thin on top.

What does any of that have to do with generics?

Well the generic programming model tends to favour using light weight abstract data structures instead of well defined types. Those abstractions are by nature leaky so many internal concerns leak out of abstraction boundaries into the caller and give them one hell of a bad time.
> Those abstractions are by nature leaky

Do you have any examples?

`Dictionary<List<Dictionary<string, object>>>, SortedSet<Dictionary<int, string>>>>`

This is not a problem with generics, but with C#'s lack of discriminated unions and/or tiny-types. Except what on earth are you doing with a dictionary whose keys are lists of dictionaries? I am quite sure someone has not modelled their domain correctly there. That's not something you can blame on the existence of generics - I shudder to imagine how much worse it could have been without generics!

> I shudder to imagine how much worse it could have been without generics!

It probably would have just become a very long String pretty early on. It would have xml in it, but not always, because nobody is that lucky.

I was exaggerating there I admit, mostly because I can't post some of the hell I've seen without breaking contracts. The worst example I've seen was a completely generic data type specified abstract syntax tree. I spent a couple of weeks rewriting that using concrete types and managed to find and fix tens of trivial bugs caused entirely by the design.

The point is really that it's hard to reason about such things and define if they are appropriate or not for a lot of people. It's a lot of rope to hang yourself with.

The sufficiently stupid-but-hard-working programmer can write crap code in any language. The actually-useful question is whether the language gives competent programmers enough rope to build whatever they're trying to build.
I think the answer for that for Go is "yes"
We shouldn’t build stuff for the 5%
Agreed. I'll be banning generics from any code I have control over unless there's a very good reason for it. I saw too much of this crap in C#, and ran from it screaming.
Say you need a binary heap to hold some customer records, and another binary heap to store some orders. How many times do you implement a binary heap?
Apparently their answer is “five times”.
Well, twice at least.

I don't know why you're all so scared of a little repetition ;)

I actually just need generic Sets.

Generic map/reduce on slices wouldn't hurt too.

OTOH, it's 2021 and look at what we are wishing. My love/hate relationship with golang is like the one I have with Apple.

isn't generic Sets easily implemented with map being already generic?
> isn't generic Sets easily implemented with map being already generic?

Since Go has neither generic functions nor generic typedefs you can't implement a Set with a generic key type on top of map, you have to reimplement all the set operations for each key type you use.

I think map[T]bool is already a pretty good set; the only things you can do with sets are insertion, deletion, iteration and checking for existence and they're all well-supported.

Of course, if you need a concurrent set you're right back in type system hell.

You can’t write intersection, union, difference, subset (contains all), or powerset as reusable functions for any element type. The idiomatic thing for now is to rewrite them as loops over and over, but that’s error prone, hard to read, and not a good use of time.
This was the exact issue I hit. There are set libraries for Go, but they only handle standard types. The for loops got old quick.
In addition to that, map[T]bool only works if T is one of the few types that Go can check for equality automatically. You can't define a custom equality (+hash) function for your type and use it with the built-in map.
I see what you mean there.
Not if you need to implement set operations (union, intersect, etc.) on your own each time.

Generic maps as sets are only ok for membership checks.

One challenge there is that identity is only supported for some built-in types — only primitives, and structs of primitives; no pointers, slices, maps.

If you want a set of some complex kind of value that contains non-map-indexable types like slices and pointers, then you have build an indirection around it.

A good set implementation needs to support a comparison operation. I really wish this existed for Go maps, too.

For some uses of a set, yeah. For intersections / unions etc. nope. Then it's back to for loops, so many for loops in Go.
Yes and that's probably why there is no set in the go std lib. You just can use struct{}{} as (empty) value in a map.
As an aside, this is exactly how HashSets are implemented in the Rust standard library.[0]

[0]: https://rust-lang.github.io/hashbrown/hashbrown/hash_set/ind...

I thought the idomatic approach was to represent sets with map[key]bool
The problem of doing that is twofold:

* each key now has 3 possible states (true, false, and unset) rather than two

* a bool takes 1 byte to store (which may get more problematic due to alignment, I've never checked what the memory layout of go's map is so I don't know how much of a concern it is there)

An empty struct fixes these issues: a key being present means the item is in the set, and an empty struct is zero-sized.

edit: apparently go maps are laid out as buckets of 8 entries with all the keys followed by all the values, so there's no waste due to padding at least.

As someone who finds "indicating intent in the code" an important thing, I must admit I find this concert slightly horrifying. A Map and a Set are two different things and which one you use conveys some intent as to what you mean by your code. I get that it works, but it would still make me unhappy to do.
Ideally, if the language/standard library provides maps but not sets, and you wanted to use the idiomatic set = map of type -> bool approach, you'd create a wrapper so that intent is preserved but users don't have to know about the backing mechanism. Of course, it's obnoxious if everyone has to do this themselves and the language lacks generics so you have to write this once for each potential type.
> A Map [with value type = void/unit/() [0]] and a Set are two different things

Not to defend Go or anything, but that's like saying:

| A Array [with element type = byte/char/u8] and a String are different things

It might be useful to call them different names (of course that would require Go to support generic typedefs for `type Set k = Map k Void`), but they're still fundamentally the same thing.

0: which, to be fair, is not the same thing as a map with bool values.

A map[T]bool has 3 states for every key; absent, true, and false. A map[t]struct{} has 2 states for every key; absent, and present.

People new to Go tend to pick map[T]bool or map[T]int because they're used to using bools and ints throughout their code, but struct{} is the correct value type for sets. (That is not to say that a counting set, map[T]int is useless, however. If you need that, use that!)

I usually use map[T]bool because

  if _, ok := wasTouched[thing]; !ok {
    touch(thing)
    wasTouched[thing] = struct{}{}
  }
is way uglier than

  if !wasTouched[thing] {
    touch(thing)
    wasTouched[thing] = true
  }
Hehe. It definitely was.. I think this is also still somewhere in "Efficient Go". However this seems to have changed in recent years. I was surprised by this too and personally I still prefer the bool even though it uses a bit more memory.

People argue there are 3 states but it is meaningless in my opinion because you can just ask exists := someMap[someKey] without checking for existence as you do with real maps. Here false is equivalent to not existent.

The bool is meaningless, so empty struct is more clear as it contains no state.
Eh, even C supports clean templated type safe containers:

https://www.github.com/glouw/ctl

Why Go is so far behind is behind me

There is a big difference here. The draft is about adding generics to the Go stdlib, not about if it's possible. Is it even possible? Yeah, there are some implementation (e.g. https://github.com/cheekybits/genny). So Go is not "far behind" C, which also does not have generics (or did I miss something in C11/C17?).
You missed, C17 introduced lightweight generics via _Generic and C2X plans to extend it further.
Tiny correction, _Generic was introduced in C11.
Thanks, that is what happens when I don't cross check my memories. :)
It seems to me like generics are extremely important for "library code", and not super important for "application code" (and in fact they can sometimes create more confusion than they're worth in the latter context). Go also seems like a language that thrives in smaller-scale, application-focused contexts (microservices being the obvious example).

So in this light, and with the basic generic data structures supplied by the standard library, it seems to make sense for "user-level" Go code to generally be better-off without generics

Of course the line between "library" and "application" code isn't well-defined (especially if you consider libraries outside of the standard one), which is probably where most of the pain-points are coming in

> It seems to me like generics are extremely important for "library code", and not super important for "application code"

I find that it really depends a lot on the language you're working in, and how well it does generics.

In Java, I don't use generics much beyond collections, streams, stuff like that. Whenever I try, I tend to trip over its relatively limited implementation of the concept.

In a language like F#, on the other hand, generics are the cornerstone of my business domain modeling. They provide a way to map everything out in a way that is much more concise, readable, type-safe, and maintainable than I find to be possible in many other languages.

I have yet to kick higher-kinded polymorphism's tires in a good context, but I can see where a good implementation of it would move things even further in that direction.

(edit: Disclaimer: This isn't meant to be a statement on Go or the advisability of this proposal. Go isn't really meant for the kinds of applications where I've seen real benefit from generics.)

Whether you find yourself using them and whether they're actually necessary are two different things :)

I've gotten use out of generics in "application code", but I've also been bewildered by overly-complex generics-within-generics-within-generics written by other people in application code. It's hard to be conclusive, but I wouldn't be surprised if they've done more harm than good across application contexts.

> overly-complex generics-within-generics-within-generics

To me, that's a shining example of the problems I've run myself into when trying to squeeze much power out of Java-style generics. I never seem to encounter similar problems in F#. Scala, it depends on how successful I am at not losing a boot in the mud.

Generic programming was born in a language whose other pioneering features were algebraic data types and an HM type system. I've never really seen a first-rate example of one that didn't come paired with at least passable examples of the others.

It's a real pain in the ass not having generics any time you're working with algorithms and data structures. Linked lists? Graphs? Trees? Go is generally quite nice to work with but it implementing these basic structures again and again with different underlying data types makes me feel like I'm writing Java. Which is ironic because, you know, Java has generics.
I think the idea is that these fundamentals could/should be supplied by the standard library

Ironically, despite all their differences, Rust actually has a similar situation: it's really hard to write the fundamental data-structures in Rust, so they've put a focus on having really good standard-library implementations and people are generally content using those (in Rust's case it's because the borrow-checker makes pointer twiddling hard, but the outcome is similar)

They kind of did this with maps and slices except that they baked them right into the language instead of the standard library. Like, map is a keyword. The standard library doesn't have many data structures at all because, well, without generics they're not very useful. There's a few things like a linked list and a thread-safe map that accept interface{} types but then you're basically throwing the type system out the window.
I wish they just did a bunch more data structures in the language and called it good.
> I think the idea is that these fundamentals could/should be supplied by the standard library

There is basically no limit to the number of data structures possible, nor to the possible implementation details of most of them, all of which can be relevant to the situation at hand.

The stdlib can hardly be expected to implement them all.

If they're very specific to the situation at hand, they're much less likely to need to be generic. The GP explicitly mentioned "Linked Lists" and "Trees". You don't need to be writing your own linked-list or (basic) tree from scratch.
> The GP explicitly mentioned "Linked Lists" and "Trees".

And graphs.

> You don't need to be writing your own linked-list or (basic) tree from scratch.

Trees are rarely useful in and of themselves, what's useful is the data structures you're building out of them. And that, in turns, informs a significant number of properties of the tree you'll be using as well as the operations to perform. The stdlib providing "a basic tree" and essentially telling users to get bent would be worse than useless, it would be actively insulting.

Even for the humble "linked list" there are half a dozen possibilities: singly linked? Immutable? Doubly-linked? Circular? Array-of-nodes?

> I think the idea is that these fundamentals could/should be supplied by the standard library

Data structures generally need to be parameterized on the contained types of you don't want to waste the effort of even having a static type system, which makes it impossible to do this right without generics

I generally agree but I note that any substantial project becomes largely composed of “library code” itself.
Right, and that's where the question gets muddy

Though again, Go as a whole seems ill-suited for scaling to larger projects because of lots of other limitations on its type system, reliance on conventions, implicit-defaults, etc. Which makes it well-suited to (and often used for) things like microservices, where each actual codebase is smallish. Codebases like these will tend towards having less "library-like" code anyway, which means they don't need generics as badly. There's synergy here in the language design.

So I guess what I'm saying is: leaving out generics seems like the more "Go-like" direction, will dovetail better with its overall philosophy, etc, and isn't without advantages. But it would also mean kneecapping the language when it comes to certain use-cases that it's never going to be great for anyway. It's the classic "opinionated" vs "everything for everybody" dichotomy

Funny you should say that, because go was specifically designed for large code bases. I work on a large go code base and it’s great.

https://talks.golang.org/2012/splash.article

Why do you say Go has trouble scaling to large code bases? Is that something you'd expect, or something borne out by the evidence? And if so, what is the evidence?
It's the subjective impression I've formed from, among other things, reading articles like this:

https://fasterthanli.me/articles/aiming-for-correctness-with...

Ctrl+F for "Let's start with Go" to jump to the relevant part

FWIW I would take fasterthanli.me with a grain of salt. The guy is a serial Go hater. His points stand on their own, but I don't think he appreciates Go's benefits. I think "A Philosophy of Software Design," Rob Pike's talks, or Russ Cox's blog posts are a good place to look if you want to understand what is valuable about Go and the reason to believe it would actually scale very well to large codebases.
When I worked at $big_company, we used a lot of code generation and reflection to work around the lack of Go generics in things like API interfaces to other services and test mocks. This was far from ideal, because it significantly increased compile times, and some things stopped being typesafe or had unfortunate type-related bugs.
If we're dealing in anecdata, mine is that "Go compiles fast!" is true right up until something in your dependency tree hauls in kubernetes repos, perhaps multiple times. Thanks the "first principles" design of go mod, that's becoming increasingly unavoidable.

Kotlin does compile much slower than I would like, but at least I only haul in one version of libraries and 0% of it is generated code. Java is basically instant for me on the same codebases.

Java, Go, OCaml, Typescript (>=3.9) are in their own league when it comes to compilation speed.
If anyone wants to check fast compile times try Delphi, RAD Studio has community edition which is amazing. It feels like it compiles while you type :)
I expect anything that has a little Pascal inside to be blazing fast.
And they have already added generics to Pascal

It really makes Go look like a joke

The features I like about Go do not overlap with the ones of Pascal. But yes, it makes Go look underdeveloped in that department.

  1. OCaml
  2. Go
  3. Java
  ...
  999. TypeScript (any version)
The TypeScript team should consider rewriting the type checker in Rust or Go IMO. It's free, they can do what they want, but the performance is really not good (compared to what it could be) and affects a lot of people.
you mean slow?
> Thanks the "first principles" design of go mod, that's becoming increasingly unavoidable.

That's interesting, could you explain this more?

Context: I used to use Go a lot, but mostly haven't since Go modules, and I'm curious to know the details of the problem and why it happens.

It doubles down on Go's assumption that git repository === a proper package/module system. It mixes up URLs and URNs.

If your git repositories aren't tagged just so, then go mod throws its hands up and simply invents a whacky snapshot version. Because it can't itself properly determine "earlier version" from "later version" on that snapshot, you often wind up with multiple snapshots from the same repo, not infrequently transmitted through other dependencies.

This is just jolly good fun when it turns out that your dependencies are pulling in incompatible versions of things. Since the official Kubernetes policy for downstream consumers is "we don't care about downstream consumers", it happens more quickly than one would expect.

As much as I have hated playing whack-a-mole with Maven or Bundler, I hate even more playing whack-an-adamantium-and-invisible-with-xray-eyes-mole against go mod.

> you often wind up with multiple snapshots from the same repo

Either I’m misunderstanding you or you’re mistaken. You can’t have multiple versions of the same module in a go build.

_Technically_ you're right. Go considers v0/v1 to be a distinct module from v2. Most people would consider this to be the same module, but Go doesn't. If you want to know more, you can go read the manifesto released by the maintainers about how this is "the best thing ever".

If you never tag v1, you'll never have to deal with it.

Huh, interesting, thanks for the response.

Does that mean Kubernetes is not following the version tagging policy in its repos? That seems...surprising!

The Go version tagging policy is a horrible idea for any multi-module git repo, especially one that releases both a product and some libraries.

It would mean that you need to create Go compatible tags for your library releases OR adjust your product versions to match Go's assumptions.

The whole implementation of Go modules is a shit show once you get into the weeds.

Kubernetes has its own tagging scheme, which makes perfect sense in its context.
I'm currently working on a project that depends on the k8s APIs and I haven't noticed ballooning compile times.

I'm only pulling in k8s.io/api, k8s.io/apimachinery, and k8s.io/go-client though.

My code before a transitive dependency pulled in the k/k universe took milliseconds to compile. Afterwards it took about 10 seconds to compile. Laboriously compiling thousands and thousands and thousands of lines of nearly-identical code turns out to be much slower. There are no clever shortcuts for a compiler that cannot deduce a higher intent.
I have the same experience. I maintain lots of software that directly depends on k8s.io/go-client and it feels the same as any other Go project.
Don’t do that then.
> We have a very large Go codebase here at Stream

That's exactly what generics are supposed to solve! ;)

No, but seriously, I'll be interested to see if they can pull off maintaining the compiler performance while adding support for this new feature. I've had to hand-write a lot of code that I'm excited about a generics solution automating, but automation can have a price, you're exactly right. I've worked on a C++ codebase before that couldn't physically compile on my machine because it blew stack on template instantiation recursions (issue never noticed because the original developer had a better machine).

In our large production project we have a very simple Go arch that enables 15+ microservices. Im not sure I see the value of generics either other than complicating a rather complicated distributed architecture with abstracted implementation.

It's beautiful that we still import 6yo packages that are clear, concise, and work as needed. The package landscape with generics doesnt seem great.

I don't think generics will suddenly make your, or anyone else's codebases more complicated. It might simplify code that's currently doing type assertions
I'm pretty convinced they could have added some additional structure libraries to the standard library and called it good. Some map stuff / set stuff etc.

For many its really not a big deal not having generics, and makes code SO much easier to follow (and compile / debug etc).

This is not the first time I argue this case, but I would go a step further and say that not having generics is feature of Go.

There are plenty of languages out there with Generics. I use several of them. I use Go when that suits me, and it's typically for cases where high readability trumps doing a lot of magic with generics. I think only once or twice have I thought to myself, "this would have been better with Generics".

Generics aren't magic, and they aren't Turing complete, like C++ templates. They're just a way to avoid copy-pasting code.

Having `Set<Foo>` and `Set<Bar>` is far more readable than `class FooSet` and `class BarSet`, where the code is exactly the same aside from a search-and-replace.

You also run into issues where someone finds a bug in `FooSet`, but doesn't know `BarSet` exists, and forgets to patch both. Now, you have two divergent copy-paste classes.

Generics solve a bunch of real-world problems, in a very simple manner.

Can you give a real world example that couldn't be solved with interfaces?

The only real cases I can see is creating new data structures, (for instance if you wanted to create your own map type).

>The only real cases I can see is creating new data structures

Half of programming is creating new data structures.

The other half is tranformations (e.g. map, filter, reduce, min, max, etc) which also benefit from being generic.

Sorry, when I said new data structures, I meant containers like maps and list, which I very rarely get to create day to day.

I can see it for your transformations, but I have seldom seen cases where generics would really help (usually we're talking about comparing complex structure types that will need custom code anyway).

"Theorems for free"! By which I mean, when I'm coding something and it could be universally quantified on the type, then it's better to do so. That way, it's impossible to phrase certain errors. For example, a function `Set<a> -> Set<a>` must produce a subset of the input set; that's guaranteed by the types. I can't accidentally include the value 0 in my set, because 0 isn't of that generic type. The generic forces you to think in terms of the structure you're manipulating, rather than in terms of its contents.
How about sets?
You can't use interfaces to prevent inserting values of the wrong types into a set. When you view the purpose of types as preventing bugs, that seems like a giant missing feature.
“Can you give a real world example that couldn't be solved with interfaces?”

Of course not. Go is Turing complete, and generics do not make it Turing-completer (whatever that may mean)

The question isn’t about what can or cannot be done, but about expressiveness, ease of understanding for humans versus language size (even if you have plenty of disk and RAM, That correlates with buggyness of the compiler) and compilation speed.

I think you're looking at it wrong. You can absolutely solve it with interfaces, the problem is those interface methods are identical, so it's duplicative.
> I think you're looking at it wrong. You can absolutely solve it with interfaces

There are lots of generics use case you either can't solve at all with interfaces, or you have to contort every which way and usually lose something in the process (type-safety, performances, readability, …).

> Can you give a real world example that couldn't be solved with interfaces?

There are none, you can always work around them, with the only downside is that you'll move some potential compile-time errors to runtime errors.

But that's the wrong conversation to be having; we could also say "why do we need floats? Everything can be solved with ints too", which is true, but also a lot of work and a poor trade-off. Similar arguments exist for many language features.

So it's a question of trade-offs: how much time will this save people? Will it reduce faults? And what are the costs of adding this? And how do they balance?

The introduction of generics would not change your workflow though. You could still happily "not use it" and keep matters readable. Others who wanted it, would use it.
Mostly concerned about having to read other people's code that uses it.
That ship already sailed with widespread misuse of interfaces. Consider:

    package foo

    type T interface { func Bar() }

    func New() T {
       return &someotherpackage.ImplPickedAtRuntime{...}
    }
Now whenever you see:

    x := foo.New()
    x.Bar()
You have no idea where to read the code for Bar. For maximum fun, ImplPickedAtRuntime should then contain members that were allocated in the same way. What should be a simple M-. then eats up your entire afternoon.
If others people code uses it, then those other people deemed it useful.

So the argument for not having them now becames either:

(a) they rather not have it available, because you personally don't find it useful

(b) those using generic don't know what they're doing, and only people not using generic are smart, so it's better to not have them to prevent the clueless from being able to use them

Like I said, I use (and like) several languages that have Generics, and when I need to do something where it makes sense, I can reach for those. For me there was an advantage in having a language where it wasn't an option. Your (b) scenario is quite a strawman. I have seen plenty of good code using Generics, but sure, there is subset of code written using Generics that is not good, and I think it becomes easier to obfuscate code and make it hard to read if you have Generics, that might just by my bias, and I think I'm tainted from C++ and hopefully it will never become as bad as what you can encounter there.

I'm not trying to make out that Generics have no place in Computer Science. I was trying to make the case for it being nice that there was a language that didn't have it, and was building on the grandparent saying that he didn't miss it that often, which mirrors my experience with Go.

This is a silly strawman. Developers often write hard to read code, and even if generics are useful to the writer, doesn't mean they are useful to the reader. Many developers do not consider the reader, or if they do, not very in-depth. You can also make the argument that generic code is uniformly harder to read than specific code.
I would argue that code using generics is often easier to read than code without it.

The fact that something is a type variable makes it clear the that type of that thing doesn't matter.

(c) Some people using generics don't know what they are doing and would be much better off not doing so.
> Doing the same in Kotlin, laptop freezes, android studio freeze, time to get a coffee

I work in Kotlin every day and my laptop never freezes.

I'm sure compile times are shorter in Go (after all, that's one of Go's main selling points). But compiling Kotlin code doesn't seem to be a huge bottleneck in my experience (and I have worked in some other languages that had truly atrocious compile times cough Swift cough). In fact, it usually takes longer for Spring to boot than for the Kotlin code to compile.

Measuring against Kotlin or other languages in this category like Scala is not the right thing to do. Also Kotlin is known for its slow compiler though JetBrains promised that the situation will improve. I would use Scala or Kotlin for everything data but when it comes to low-level networking/infrastructure I wouldn't even touch anything else but Go. Go is in the perfect sweet spot for this task. The Goldilocks Zone of network programming.
It's such a truly terrible comparison because they didn't compare Kotlin, they compared Android!

The insane amount of time of overhead an Android project has over a "plain Kotlin" from resource packing to dex stuff to desugaring to ProGuard, it made me question if they're speaking in good faith for the rest of the comment...

My ktor projects on the other hand compile incredibly quickly, and with hot reloading it's even more seamless to iterate

What is the workaround for having a nice ORM library?
Not using an ORM.
That's not a workaround, that's a solution.
Every workaround is a solution, but not every solution was a workaround. :D
This one is mostly excellent: https://github.com/go-pg/pg
The biggest challenge is avoiding the tricks from languages that allow you to use the language to paper over questionable design choices. Go requires that you get the design right up front and provides few escape hatches to save you if you mess up. Which is a good thing, but makes the language (not just the syntax) difficult to learn compared to others.
Can you give examples of specific language features of go that prevent you from making questionable design choices, that require you to "get the design right up front"?

When I hear that, the first things that come to mind are things like haskell's IO monad, which forces you to model IO better than go or most other languages, or haskell's other state monads which similarly force you to model state more explicitly.

I think of rust's lifetime and ownership system, which forces you to correctly model the ownership of types and prevents quite a few bad design patterns (which I see constantly in go btw; the number of times I've seen races due to multiple goroutines writing to a struct is large, the number of times I've seen incorrect synchronization that rust would have prevented is large).

I can't think of anything in go that pushes you towards designing your code well in go, especially when compared to languages with more complete type-systems.

> Can you give examples of specific language features of go that prevent you from making questionable design choices, that require you to "get the design right up front"?

No, obviously. I said that Go doesn't give you an escape hatch if you screw up. It does nothing to protect you from screwing up. I specifically said that the challenge was in learning how to not screw up as the language doesn't help you deal with or avoid design mistakes.

Aaah, I read "Go requires that you get the design right up front" totally wrong, as in "go prevents you from getting the design wrong", not "go lets you get the design wrong, and then doesn't help you".

I still don't get your contrast to other languages though. Are there specific language features other languages have that let you paper over crappy designs?

IMO Go’s escape hatch for screw ups is its superb refactor-ability.
Generics make working with reactive APIs much easier, which I think fits with a large chunk of Go's target audience (b/e networked services) quite closely.

Writing APIs to deal with futures, etc, is much easier when you can chain parameterised functions together.

OCaml has generics (and global inference!) and compiles very fast.