I'm surprised by the complexity of Go's generic constraints, given the language's focus on simplicity. Things like the difference between "implementing" and "satisfying" a constraint [0] and surprising exceptions around what a constraint can contain [1]:
> A union (with more than one term) cannot contain the predeclared identifier comparable or interfaces that specify methods, or embed comparable or interfaces that specify methods.
Is this level of complexity unavoidable when implementing generics (in any language)? If not, could it have been avoided if Go's design had included generics from the start?
i have been writing Go exclusively for 5+ years and to this day i use generics only in a dedicated library that works with arrays(slices in Go world) and provides basic functionality like pop, push, shift, reverse, filter and so on.
Other than that, generics have not really solved an actual problem for me in the real world. Nice to have, but too mush fuss about nothing relevant.
There’s an existing ecosystem that already works with the constraints of not having generics. If you can write all your code with that, then you won’t need generic much. That ecosystem was created with the sweat of library authors, dealing with not having generics and also with users learning to deal with the limitations and avoid panics.
Generics have been tremendously helpful for me and my team anytime we are not satisfied with the existing ecosystem and need to write our own library code. And as time goes on the libraries that everyone uses will be using generics more.
Just checked, in my current project, the only place where I use generics is in a custom cache implementation. From my experience in C#, generics are mostly useful for implementing custom containers. It's nice to have a clean interface which doesn't force users to cast types from any.
Containers are sort of the leading order use of generics: I put something in and want to statically get that type back (so no cast, still safe).
Second use I usually find is when I have some structs with some behavior and some associated but parameterizable helper. In my case, differential equations together with guess initializers for those differential equations. You can certainly do it without generics, but then the initial guess can be the wrong shape if you copy paste and don't change the bits accordingly. The differential equation solver can then take equations that are parameterized by a solution type (varying in dimension, discretisation and variables) together with an initializer that produces an initial guess of that shape.
Finally, when your language can do a bit of introspection on the type or the type may have static methods or you have type classes, you can use the generic to control the output.
Basically, they are useful (like the article implies) when you want to statically enforce constraints. Some people prefer implicitly enforcing the constraint (if the code works the constraint is satisfied) or with tests (if the tests pass the constraint is satisfied). Other people prefer to have the constraints impossible to not satisfy.
I sometimes wonder if they should have implemented generics. On the one hand you had a group of people using go as it was and presumably mostly happy with the lack of generics. On the other side you have people (like me) complaining about the lack of generics but who were unlikely to use the language once they were added.
It's very subjective but my gut feeling is they probably didn't expand their community much by adding generics to the language.
Generic containers are needed in some cases. Using generic containers with interface{} is very slow and memory-intensive. Not a problem for small containers, but for big containers it's just not feasible, so you would need to either copy&paste huge chunks of code or generate code. Compared to those approaches, generic support is superior in every way, so it's needed. But creating STL on top of them is not the indended use-case.
I think a lot of the people who wanted generics wanted them more to be like C++ templates, with compile time duck typing. Go maintainers were unwilling to go that route because of complexity. However, as a result, any time I think "oh this looks like it could be made generic" I fall into a rabbit hole regarding what Go generics do and dont allow you to do and usually end up copy pasting code instead.
Honestly so many things profit from generics, e.g. ORM code was very awkward before especially when returning slices of objects as everything was []any. Now you can say var users []User = orm.Get[User](…) as opposed to e.g var users []any = orm.Get(&User{}, …), that alone is incredibly useful and reduces boilerplate by a ton.
Programming is about building abstractions, abstractions are a way to reduce boilerplate.
Why do we need `func x(/* args / ) { / body */ }`, when you can just inline the function at each callsite and only have a single main function? Functions are simply a way to reduce boilerplate by deduplicating and naming code.
If 'reducing boilerplate is bad', then functions are bad, and practically any abstraction is bad.
In my opinion, "reducing boilerplate is bad in some scenarios where it leads to a worse abstraction than the boilerplate-ful code would lead to".
I think you have to evaluate those things on a case-by-case basis, and some ORMs make sense for some use-cases, where they provide a coherent abstraction that reduces boilerplate... and sometimes they reduce boilerplate, but lead to a poor abstraction which requires more code to fight around it.
I agree. The best language to handle data in a RDBMs is SQL, and in that case the best language to handle application logic is Go (or Kotlin, Python or whatever). So there must be some meeting point. Handling everything in Go is not optimal, and all in sql not always practical. So how to avoid redundant data description ? I often have structs in a model Go file that reflect queries I do, but that's not optimal since I tend to have to repeat what's in a query to the language and the query to struct gathering is often boilerplate. I also almost can reuse the info I need for a query for another query but leave some fields blank since they're not needed.. the approaches are not optimal. Maybe a codegen sql to result structs / gathering info ?
ORM is for object-relation mapping. Go is not object-oriented language and OOP-patterns are not idiomatic Go, so using ORM for Go cannot be idiomatic. That's generic answer. As for more concrete points:
1. Mapping SQL response to maps/structs or mapping maps/structs to SQL parameters might be useful, but that's rather trivial functionality and probably doesn't qualify as ORM. Things get harder when we're talking about complex joins and structs with relationships, but still manageable.
2. Introducing intermediate language which is converted to SQL is bad. Inevitably it will have less features. It will stay in the way for query optimisations. It'll make things much less obvious, as you would need to understand not only SQL, but also the process of translating intermediate language to SQL.
3. Automatic caching is bad. Database has its own caching and if that's not enough, application can implement custom caching where it makes sense.
In my opinion the only worthy database integration could be implemented with full language support. So far I only saw it with C# LINQ or with database-first languages (PL/SQL, etc). C# and Go are like on opposite spectrum of language design, so those who use Go probably should keep its approach by writing simple, verbose and obvious code.
I find libraries like sqlx more than enough. Instead of a full-blown ORM, they simply help hydrate Go structs from returned SQL data, reducing boilerplate. I prefer the repository pattern, where a repository is responsible for retrieving data from storage (using sqlx) using simple, clean code. Often, projects which use full-blown ORMs, tend to equate SQL table = business object (aka ActiveRecord) which leads to lots of problems. Business logic should be completely decoupled from underlying storage, which is an implementation detail. But more often than not, ORM idiosyncracies end up leaking inside business logic all over the place. As for complex joins and what not, CQRS can be an answer. For read queries, you can write complex raw SQL queries and simply hydrate the results into lightweight structs, without having to construct business objects at all (i.e. no need for object-relational mapping in the first place). Stuff like aggregated results, etc. Such structs can be ad hoc, for very specific use cases, and they are easy to maintain and are very fast (no N+1 problems, etc). With projects like sqlx, it's a matter of defining an additional struct and making a Select call.
I'm surprised by the complexity of Go's generic constraints, given the language's focus on simplicity. Things like the difference between "implementing" and "satisfying" a constraint [0] and surprising exceptions around what a constraint can contain [1]:
> A union (with more than one term) cannot contain the predeclared identifier comparable or interfaces that specify methods, or embed comparable or interfaces that specify methods.
Is this level of complexity unavoidable when implementing generics (in any language)? If not, could it have been avoided if Go's design had included generics from the start?
[0] https://stackoverflow.com/questions/77445861/whats-the-diffe...
[1] https://blog.merovius.de/posts/2024-01-05_constraining_compl...
i have been writing Go exclusively for 5+ years and to this day i use generics only in a dedicated library that works with arrays(slices in Go world) and provides basic functionality like pop, push, shift, reverse, filter and so on.
Other than that, generics have not really solved an actual problem for me in the real world. Nice to have, but too mush fuss about nothing relevant.
There’s an existing ecosystem that already works with the constraints of not having generics. If you can write all your code with that, then you won’t need generic much. That ecosystem was created with the sweat of library authors, dealing with not having generics and also with users learning to deal with the limitations and avoid panics.
Generics have been tremendously helpful for me and my team anytime we are not satisfied with the existing ecosystem and need to write our own library code. And as time goes on the libraries that everyone uses will be using generics more.
Just checked, in my current project, the only place where I use generics is in a custom cache implementation. From my experience in C#, generics are mostly useful for implementing custom containers. It's nice to have a clean interface which doesn't force users to cast types from any.
Containers are sort of the leading order use of generics: I put something in and want to statically get that type back (so no cast, still safe).
Second use I usually find is when I have some structs with some behavior and some associated but parameterizable helper. In my case, differential equations together with guess initializers for those differential equations. You can certainly do it without generics, but then the initial guess can be the wrong shape if you copy paste and don't change the bits accordingly. The differential equation solver can then take equations that are parameterized by a solution type (varying in dimension, discretisation and variables) together with an initializer that produces an initial guess of that shape.
Finally, when your language can do a bit of introspection on the type or the type may have static methods or you have type classes, you can use the generic to control the output.
Basically, they are useful (like the article implies) when you want to statically enforce constraints. Some people prefer implicitly enforcing the constraint (if the code works the constraint is satisfied) or with tests (if the tests pass the constraint is satisfied). Other people prefer to have the constraints impossible to not satisfy.
I sometimes wonder if they should have implemented generics. On the one hand you had a group of people using go as it was and presumably mostly happy with the lack of generics. On the other side you have people (like me) complaining about the lack of generics but who were unlikely to use the language once they were added.
It's very subjective but my gut feeling is they probably didn't expand their community much by adding generics to the language.
Generic containers are needed in some cases. Using generic containers with interface{} is very slow and memory-intensive. Not a problem for small containers, but for big containers it's just not feasible, so you would need to either copy&paste huge chunks of code or generate code. Compared to those approaches, generic support is superior in every way, so it's needed. But creating STL on top of them is not the indended use-case.
I think a lot of the people who wanted generics wanted them more to be like C++ templates, with compile time duck typing. Go maintainers were unwilling to go that route because of complexity. However, as a result, any time I think "oh this looks like it could be made generic" I fall into a rabbit hole regarding what Go generics do and dont allow you to do and usually end up copy pasting code instead.
Honestly so many things profit from generics, e.g. ORM code was very awkward before especially when returning slices of objects as everything was []any. Now you can say var users []User = orm.Get[User](…) as opposed to e.g var users []any = orm.Get(&User{}, …), that alone is incredibly useful and reduces boilerplate by a ton.
ORM is anti-pattern and reducing boilerplate is bad.
> reducing boilerplate is bad
Programming is about building abstractions, abstractions are a way to reduce boilerplate.
Why do we need `func x(/* args / ) { / body */ }`, when you can just inline the function at each callsite and only have a single main function? Functions are simply a way to reduce boilerplate by deduplicating and naming code.
If 'reducing boilerplate is bad', then functions are bad, and practically any abstraction is bad.
In my opinion, "reducing boilerplate is bad in some scenarios where it leads to a worse abstraction than the boilerplate-ful code would lead to".
I think you have to evaluate those things on a case-by-case basis, and some ORMs make sense for some use-cases, where they provide a coherent abstraction that reduces boilerplate... and sometimes they reduce boilerplate, but lead to a poor abstraction which requires more code to fight around it.
Not liking ORM I can understand, db table <-> object impedance mismatch is real, but "reducing boilerplate is bad" is an interesting take.
Can you elaborate and give some examples of why reducing boilerplate is generally "bad"?
I agree. The best language to handle data in a RDBMs is SQL, and in that case the best language to handle application logic is Go (or Kotlin, Python or whatever). So there must be some meeting point. Handling everything in Go is not optimal, and all in sql not always practical. So how to avoid redundant data description ? I often have structs in a model Go file that reflect queries I do, but that's not optimal since I tend to have to repeat what's in a query to the language and the query to struct gathering is often boilerplate. I also almost can reuse the info I need for a query for another query but leave some fields blank since they're not needed.. the approaches are not optimal. Maybe a codegen sql to result structs / gathering info ?
Could you expand on this?
I don't like ORM because in my experience you inevitably want full SQL features at some point but not sure if you have the same issues in mind or not
ORM is for object-relation mapping. Go is not object-oriented language and OOP-patterns are not idiomatic Go, so using ORM for Go cannot be idiomatic. That's generic answer. As for more concrete points:
1. Mapping SQL response to maps/structs or mapping maps/structs to SQL parameters might be useful, but that's rather trivial functionality and probably doesn't qualify as ORM. Things get harder when we're talking about complex joins and structs with relationships, but still manageable.
2. Introducing intermediate language which is converted to SQL is bad. Inevitably it will have less features. It will stay in the way for query optimisations. It'll make things much less obvious, as you would need to understand not only SQL, but also the process of translating intermediate language to SQL.
3. Automatic caching is bad. Database has its own caching and if that's not enough, application can implement custom caching where it makes sense.
In my opinion the only worthy database integration could be implemented with full language support. So far I only saw it with C# LINQ or with database-first languages (PL/SQL, etc). C# and Go are like on opposite spectrum of language design, so those who use Go probably should keep its approach by writing simple, verbose and obvious code.
I find libraries like sqlx more than enough. Instead of a full-blown ORM, they simply help hydrate Go structs from returned SQL data, reducing boilerplate. I prefer the repository pattern, where a repository is responsible for retrieving data from storage (using sqlx) using simple, clean code. Often, projects which use full-blown ORMs, tend to equate SQL table = business object (aka ActiveRecord) which leads to lots of problems. Business logic should be completely decoupled from underlying storage, which is an implementation detail. But more often than not, ORM idiosyncracies end up leaking inside business logic all over the place. As for complex joins and what not, CQRS can be an answer. For read queries, you can write complex raw SQL queries and simply hydrate the results into lightweight structs, without having to construct business objects at all (i.e. no need for object-relational mapping in the first place). Stuff like aggregated results, etc. Such structs can be ad hoc, for very specific use cases, and they are easy to maintain and are very fast (no N+1 problems, etc). With projects like sqlx, it's a matter of defining an additional struct and making a Select call.
> Go is not object-oriented language
That is most definitely not true. Go just uses composition instead of inheritance. Still OOP, just the data flow is reversed from bottom to the top.
understandable. thee are always valid uses cases. although ORM in Go is not something that is widely used.
Well, generics are mostly meant for library code. Just because you don't need it, doesn't mean that code you use doesn't need it.