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 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?
Generics are a powerful mechanism, and there is a spectrum. The act of retrofitting generics on go without generics certainly meant that some points in the design space were not available.
On the other hand, when making a language change as adding generics, one wants to be careful that it pulls its own weight: it would be be sad if generics had been added and then many useful patterns could not be typed.
The design choices revolve around expressivity (what patterns can be typed) and inference (what annotations are required). Combining generics with subtyping and inference is difficult as undecidability looms. In a language with subtyping it cannot be avoided (or the resulting language would be very bland).
So I think the answer is no, this part of the complexity could not have been avoided. I think they did a great job at retrofitting and leaving the basic style of the language intact - even if I'd personally prefer a language design with a different style but more expressive typing.
tapirl 4 days ago [-]
The difference between types.Implements and types.Satisfies is mainly caused by a history reason. It is just a tradeoff between keeping backward compatibility and theory perfection.
It is pity that Go didn't support the "comparable" interface from the beginning. If it has been supported since Go 1.0, then this tradeoff can be avoided.
I recommend people to read Go Generics 101 (https://go101.org/generics/101.html, author here) for a thoroughly understanding the status quo of Go custom generics.
jerf 4 days ago [-]
In practice, none of this impacts your program. The standard advice I give to people messing around with this stuff is, never use the pipe operator. The standard library already implements all the sensible uses of it.
In particular, people tend to read it as the "sum type" operator, which it is not. I kind of wish the syntax has used & instead of |, what it is doing is closer to an "and" then an "or".
By the time you know enough to know you can ignore that advice, you will. But you'll also likely find it never comes up, because, again, the standard library has already implemented all the sensible variants of this, not because the standard library is magic but because there's really only a limited number of useful cases anyhow. I haven't gone too crazy with generics, but I have used them nontrivially, even done s could tricks [1], and the pipe operator is not that generally useful.
When the generic constraint is an interface with methods is the case that can actually come up, but that makes sense, if generics make sense to you at all.
It probably is a good demonstration of the sort of things that come up on generic implementations, though. Despite the rhetoric people often deployed prior to Go having them, no, they are never easy, never without corner cases, never without a lot of complications and tradeoffs under the hood. Even languages designed with them from the beginning have them, just better stuffed under the rug and with less obvious conflict with other features. They're obviously not impossible, and can be worthwhile when deployed, certainly, but it's always because of a lot of work done by the language designers and implementations, it's never just "hey let's use generics, ok, that one sentence finishes the design I guess let's go implement them in a could of hours".
As defined, the set of the type Ordered is exactly the sum of all elements of int, uint and string. The intersection of int and string would be empty. The or symbol makes sense because an element of Ordered is either a uint or an int or a string. An element of Ordered is not a uint and an int and a string.
It feels to me that static typed languages tend to give you intersection bounds and not union bounds. Rust has intersections, java has intersections. Meanwhile, if you have duck typing then you end up with a bunch of union types (see python -> mypy, javascript -> typescript). There are of course the general union types (not generic bounds) in C/C++/rust which kind of behaves in a similar fashion.
jerf 3 days ago [-]
No, you have created "a type that can be one of int, uint, or string, or anything that backs directly to them". They all can >, and since that's the only thing you used in Max, everything works fine. You don't have a sum type; you have "a type that is either an int, a uint, a string, or something that backs to them", specifically. It doesn't come in as any sort of sum type, it is specifically one of those types directly.
For one thing, as you've specified it, you don't even have a closed set of types. Off in another package I can declare a "type MyInt int" and use your Max on it, so if you tried to type switch in your Max function, you would not know about my type, and it is arguably the defining characteristic of a sum type that you can know all the branches it has.
You can fix that by knocking off the tilde, but then you still have the problem that it is not legal to use "switch val := a.(type)", which is basically the level of deconstruction of a type that Go permits, because when the Max function is running, it is not running on a value of type "Ordered"; it literally has a value of the type you passed in. That's the whole point of generics, to have values of the concrete type that was passed in, and not a sort of "sum type".
Note you don't get "Ordered". You get the specific types. That's not any sort of "generic weirdness", that's the real situation. That's why you can only use the intersection of operations they all support.
If you want a sum type in Go, use closed interfaces: https://github.com/BurntSushi/go-sumtype If you're willing to accept what you've written as a sum type, you should be even more willing to accept this method, which actually produces a reasonable approximation of one.
Groxx 2 days ago [-]
I broadly agree with you, so this isn't to disprove you or anything, but in case you hadn't seen it before: you can do type checks inside generic functions. You just have to trick the compiler / do pointless boxing because the compiler is overly simple.
It's basically because generics are generated code for specific types with little more than text replacement, and type assertion only works on interfaces, and it can't rule out non-interfaces. But if you box it in an `any`, it's fine, just like it's fine to `((any)(5)).(int)` anywhere else (or any other equivalent construct).
tapirl 4 days ago [-]
> In particular, people tend to read it as the "sum type" operator, which it is not. I kind of wish the syntax has used & instead of |, what it is doing is closer to an "and" then an "or".
I don't understand here. In my understanding, the pipe operator is indeed closer to "or" and "sum type" operator. Interpreting it as "and" is weird to me.
Groxx 4 days ago [-]
I think they're reading it as "a bitwise-and of the functionality of the types passed", which is accurate (since you're getting the lowest common denominator of all |'d types).
I'm... not sure which way I lean tbh, now that I've seen that idea. Both have merit, it's more of a problem for educational material than anything. If you present it as "these types", | makes sense. If you instead use "these behaviors", & makes sense. | is slightly easier to type for me though, and & has more meanings already (address-of), so maybe I'd still favor |.
tapirl 4 days ago [-]
Okay, it is some reasonable if the operator is viewed as a behavior operator. But it is not, it is a type set operator.
jerf 4 days ago [-]
And the real point I'm making here is that "the type set operator" is not "a sum type". A sum type with, say, three branches is either the first, or the second, or the third, and to do anything with any of them, you have to deconstruct it, at which point you have full access to the deconstructed branch you are in. The | operator in a Go generic is more a declaration of "I want to operate on all of these at once", so, you can put multiple numeric types into it because you can do a + or a - on any of them, but while the syntax permits you to put three struct types into it, and it'll compile, it does not produce a "sum type". Instead you get "I can operate on this value with the intersection of all the operations they can do", which is more or less "nothing". ("Methods" aren't "operations"; methods you can already declare in interfaces.) Some people particularly fool themselves because you can still take that type, cast it into an "any", and then type switch on it, but it turns out you can always do that, the | operator isn't helping you in any particular way, and if you want to have a closed set of types, a closed interface is a much better way to do it, on many levels.
It also doesn't currently do anything else people may want it to do, like, accept three structs that each have a field "A" of type "int" and allow the generic to operate on at least that field because they all share it. There's a proposal I've seen to enable that, as the current syntax would at least support that, but I don't know what its status is.
But I doubt sum types will be supported perfectly in Go. The current poor-men's sum type mechanism (type-switch syntax) might be still useful in future Go custom generic age.
jerf 3 days ago [-]
I've pondered the utility of just proposing some syntax sugar around the current methodology, mostly for the reason of getting it rejected for being an unnecessary redundancy to what we already have, so we can point at the rejection.
rendaw 4 days ago [-]
There are tons of random limitations not present in other languages too, like no generic methods.
bigdubs 4 days ago [-]
That's not a random limitation, there are very specific reasons[1] you cannot easily add generic methods as struct receiver functions.
For someone not well-versed in language implementation details, it may very well feel random.
I've been using Go as my primary language for a decade, and the lack of generics on methods was surprising to me the first time I ran into it, and the reasoning not obvious.
rendaw 3 days ago [-]
Yeah. I'm not claiming they didn't back themselves into a corner here.
There's no theoretical reason not to have it, the reason is because of a random intersection of other design decisions... unless you're saying they made those choices fully expecting to have these restrictions on generics later?
the_gipsy 3 days ago [-]
> Or, we could decide that parameterized methods do not, in fact, implement interfaces, but then it's much less clear why we need methods at all. If we disregard interfaces, any parameterized method can be implemented as a parameterized function.
What? Methods are not needed if not for implementing an interface?
Anyway, functions could also be implementing interfaces, some languages allow that.
I swear the go docs read like a cult.
int_19h 1 days ago [-]
Functions in Go can be generic, just not methods.
And unless you're also using interfaces, methods are no different from functions aside from call syntax.
the_gipsy 19 hours ago [-]
But "methods are only needed because of interfaces" is simply not true. Not true in all other OOP languages that I know of, not true in go, and not true in go's stdlib (that is, in practice).
Methods bind state with a function.
That an object can satisfy an interface is secondary here. In different languages, an interface could be satisfied with a combination of methods, fields, or nominality.
If the statement "we could decide that parameterized methods do not, in fact, implement interfaces, but then it's much less clear why we need methods at all" was true, then there should not be a single struct in go (stdlib nor elsewhere) that does not implement some interface (and it must be used via that interface to make sense). This is obviously not the case.
foldr 3 days ago [-]
Rust has a similar restriction on trait objects, for similar reasons.
I wish type constraints had a different Golang type than actual interfaces. "Is one of" and "Implements" seem like different enough concepts to warrant divergence there.
indulona 4 days ago [-]
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.
kgeist 4 days ago [-]
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.
BlackFly 4 days ago [-]
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.
neonsunset 4 days ago [-]
C# generics are way more powerful than that when it comes to writing high-performance or just very, err, generic code. Generic constraints and static interface members are immensely useful - you can have a constraint that lets you write ‘T.Parse(text[2..8])’.
They are far closer to Rust in some areas (definitely not in type inference sadly, but F# is a different story) than it seems.
Of course if one declares that they are an expert in a dozen of languages, most of which have poorly expressive type systems, the final product will end up not taking advantage of having proper generics.
aljarry 4 days ago [-]
> From my experience in C#, generics are mostly useful for implementing custom containers.
That's my experience as well in C# - most of other usages of generics are painful to maintain in the long run. I've had most problems with code that joins generics with inheritance.
Groxx 4 days ago [-]
That's kinda the point. Generics are mostly a library concern, improving end-user experience and performance. End-user creation of generic types is relatively rare, and you can use them in very simple ways and that's almost always good enough because you don't need them to be maximally correct, only good enough.
For libraries (that adopt generics): yes they can be complicated. But using them is mostly zero-effort and gets rid of a ton of reflection.
slimsag 4 days ago [-]
Unfortunately not everyone shares that opinion of their restricted use-cases.
I've seen ~100 line HTTP handler methods that are implemented using generics and then a bunch of type-specific parameters inevitably get added when the codepaths start to diverge and now you've got a giant spaghetti ball of generics to untangle, for what was originally just trying to deduplicate a few hundred lines of code.
Groxx 3 days ago [-]
tbh I'll still take it over a similar Gordian knot with interfaces. At least you can tell the restrictions are met at compile time, rather than silently failing at runtime because you (and/or someone else in the past) didn't notice one edge case lodged somewhere surprising.
gregwebs 4 days ago [-]
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.
marcus_holmes 3 days ago [-]
The libraries will, yes. But folks just using the libraries still won't need generics.
If you know your concrete types, they're just not that useful.
Even in home-grown libraries, I find generics to be a convenience rather than a necessity. It's useful to not have my library code so tightly coupled to my non-library code. But it does also come with a cost: every so often I have to check what the library actually does because being loosely coupled meant that iterations in the rest of the system didn't automatically have to involve the library, so the library code can get left behind.
whateveracct 4 days ago [-]
this is wild because i use parametric polymorphism by writing `forall` in basically every Haskell PR i do for work ever
i think Go having a pretty bad implementation of parametric polymorphism (a programming concept from the 70s) is probably the root cause here
tonyedgecombe 4 days ago [-]
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.
sbrother 4 days ago [-]
Having recently had to work on a Go project for the first time, I think I agree with you here. I'd tried Go a little bit when it came out, had zero interest in what it offered, and then when I was asked to work on this project a couple months ago I thought it would be fun to try it out again since I had read the language had improved.
No, it still feels like programming with a blindfold on and one hand tied behind my back. I truly don't get it. I've worked with a lot of languages and paradigms, am not a zealot by any means. Other than fast compiles and easy binary distribution, I don't see any value here, and I see even experienced Go programmers constantly wasting time writing unreadable boilerplate to work around the bad language design. I know I must be missing something because some people much smarter than me like this language, but... what is it?
rwiggins 3 days ago [-]
I felt the same way initially, but the language has grown on me. The turning point was writing a lot of Go, like full-time project work for a few months.
But as I've gotten older, I've started striving more and more for simplicity above all else, especially in systems design (disclaimer: I'm an SRE). Go is pretty good at being simple.
There are some things that still annoy me a whole bunch, though. Like - just one example - `fmt.Errorf` not being a first-class syntactic construct (or the difference between `%v` and `%w` in `fmt.Errorf`).
bobbylarrybobby 1 days ago [-]
I frequently hear that Go is simple, and I generally take that to mean that it doesn't have a whole lot of features. But doesn't this then force the complexity on the programmer? There is a set amount of complexity in the world, and either your language models it or you model it, but something has to model it for your program to be a faithful representation of the problem in question. It seems that Go has shunted much of that complexity onto the programmer. This article does a good job of summarizing (and indeed helped to shape) my thoughts on the matter: https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-...
majormajor 3 days ago [-]
> Other than fast compiles and easy binary distribution, I don't see any value here, and I see even experienced Go programmers constantly wasting time writing unreadable boilerplate to work around the bad language design. I know I must be missing something because some people much smarter than me like this language, but... what is it?
If you "other than" two huge-for-many-use-cases good things, sure, it might look bad. ;)
But I would add good overall performance and in particular straightforward flexible concurrency support to the list of good things.
And IMO once you're in the set of "things with good perf" there's generally a lot of "boilerplate" of one sort or another anyway.
zbentley 3 days ago [-]
I'm in a similar boat to sbrother (GP) and some of the other sibling commenters: like GP, I don't love the language (honestly, "patronizing" is the word that feels most accurate to describe working in Go for me, if a programming language can even be patronizing); like others, I have been impressed with how well it works when programming in its niche (high-performance network proxies).
I'd add what I think is perhaps its most significant benefit to the list: Go fully solved the function coloring I/O problem in a way few other languages (Erlang/Elixir and ... Bend? Others?) have.
That's adjacent to the concurrency benefits in the parent comment, but a little different: allowing procedural, non-colored code to be efficiently concurrent over I/O without introducing function coloring or requiring people to code specifically to an event loop/IO multiplexer in some other way requires good concurrency support in the language, to be sure. However, getting rid of function coloring while providing efficient concurrent IO also requires: a solid stdlib of IO capabilities; a very very good runtime that can coalesce goroutine-concurrent IO into multiplexing OS primitives in the same way a function-colored event loop would (while pre-empting/scheduling in a reasonable way that mitigates unexpected blocking); a strong "critical mass" of libraries to talk to common IO-ful systems; a strong community convention of "we will generally prefer reimplementing IO drivers in Go rather than binding/Cgo-ing in foreign code".
It's when you combine all of those that Go shines as a platform for concurrent (usually network) IO.
sbrother 3 days ago [-]
Yeah that's fair. In terms of "things with good perf" I'd rather be writing C++ or Rust, but there are significant issues with using either of those on a large team.
I'm more comparing it against languages like Kotlin and Swift, or even Scala.
LinXitoW 3 days ago [-]
It might be nit picking, but that's more the ecosystem or tooling that's great. The language is mediocre, but it's what everyone gushes about.
I still remember people gaslighting everyone that any feature Go had was ESSENTIAL, and every feature Go didn't have was USELESS or too complicated for mere mortals "delivering value".
And the fast compiles at least are in big parts because the language is so horrendously basic. Can't get hung up on checking type constraints if you barely have any.
int_19h 1 days ago [-]
> I still remember people gaslighting everyone that any feature Go had was ESSENTIAL, and every feature Go didn't have was USELESS or too complicated for mere mortals "delivering value".
I'd say this is still the norm in discourse around Go, it's just that the goalposts have moved somewhat since it has more features now.
4 days ago [-]
indulona 4 days ago [-]
> I see even experienced Go programmers constantly wasting time writing unreadable boilerplate
if it is unreadable, in Go, probably the most readable language used today, i would question the aforementioned experience.
LinXitoW 3 days ago [-]
I don't think a language where for every 1 line of functionality, you need 3 lines of error handling boilerplate gets to be called readable.
Heck, Go went out of it's way to "subvert expectations" more than the last season of Game of Thrones.
99% of decent C-ish languages either do "String thing" or "thing: String", but Go is so fancy and quirky, it does "thing String" for no freaking reason. Don't get me started on the nightmare that is map types.
kweingar 3 days ago [-]
I like Go's error handling. The reader knows exactly which lines of code can encounter an error, and exactly how the error is handled.
I find that exception-based code is much harder to read. The happy path is clearer, but exceptional code paths are often completely obscured. It's harder to reason about what state the program is in when the exception is handled.
the_gipsy 3 days ago [-]
Exceptions have been a mistake, the alternative are Result types, a concept which predates go.
int_19h 1 days ago [-]
Go doesn't force you to either handle or propagate the error.
And the alternatives are not exceptions, of course, but ADTs.
DangitBobby 3 days ago [-]
What you're doing right now is called "coping".
quinnirill 4 days ago [-]
Mad LoCs, dude, gotta make alotta lines, that’s what productivity is!
brokencode 3 days ago [-]
Most of the improvements made to any language don’t expand the community much. And they don’t have to, because that’s not the point. The point is to improve the language and ecosystem to help make better software.
Generics support is a ubiquitous feature in static programming languages. If it was included on day one in Go, nobody would have blinked an eye. This is only such a controversial topic in Go because the language maintainers made it one.
vbezhenar 4 days ago [-]
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.
cherryteastain 4 days ago [-]
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.
wyufro 4 days ago [-]
I think "oh this looks like it could be made generic" is the wrong time to convert to generics.
You should convert when you reach the point "I wish I had that code but with this other type". Even then, sometimes interfaces are the right answer, rather than generics.
4 days ago [-]
cherryteastain 3 days ago [-]
I mostly agree, hence the
> end up copy pasting code instead
bit of my original comment
marcus_holmes 3 days ago [-]
Library authors were the main target group, I think.
Without generics, your library has to define interfaces that your users have to implement and it all gets a bit strange and unintuitive.
With generics you can write library code that is easier to use.
The thing I was worried about with this (adding generics) is that we'd start moving more towards the NPM Hell of everyone just writing plumbing code for imported packages. But thankfully that hasn't happened and idiomatic Go still tends to just use the standard lib and very few external packages.
peterldowns 4 days ago [-]
My most common use of generics is when testing — check out my library for typesafe test comparisons. I find it really useful because I like having readable helpers for asserting in tests, but I also want compiler errors if I refactor things.
The most frequent use case I and my coworkers run into where we use them is when we want type covariance on a slice.
I.e., when you want to write a function that take some slice of any type T that implements interface I, such that []T is a valid input instead of just explicitly []I.
throwaway63467 4 days ago [-]
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.
the_gipsy 3 days ago [-]
gorm just takes a pointer of your type and does reflection magic. It's worse than generica, I agree, but you don't get []any.
indulona 4 days ago [-]
understandable. thee are always valid uses cases. although ORM in Go is not something that is widely used.
vbezhenar 4 days ago [-]
ORM is anti-pattern and reducing boilerplate is bad.
TheDong 4 days ago [-]
> 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.
bobnamob 4 days ago [-]
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"?
vbezhenar 4 days ago [-]
What I mean is reducing boilerplate is not something one should strive to achieve. It is not bad in the sense that one should introduce more boilerplate for the sake of it. But reducing boilerplate for the sake of it is not good thing either.
If you need to make code more complex just to reduce boilerplate, it's a bad thing. If you managed go make code simpler and reduced boilerplate at the same time, it's a good thing.
And boilerplate might be a good thing when you need to type something twice and if you would make error once, the whole thing wouldn't work, so basically you'll reduce the possibility of typo. It might look counter intuitive. Just unrelated example: recently I wrote C code where I need to type the same signature in the header file and in the source file. I made mistake in the source file, but I didn't make the same mistake in the header file and the whole program didn't link. I figured out the mistake and corrected it. Without this boilerplate it's possible that I wouldn't notice the mistake and "helpful" autocomplete would keep the mistake forever. That's how HTTP Referer header made it into standards, I guess.
rad_gruchalski 4 days ago [-]
Not the person you’re replying to. The orm sucks because as soon as you go out of the beaten path of your average select/insert/update/delete, you are inevitably going to end up writing raw sql strings. Two cases in point: postgres cte and jsonb queries, there are no facilities in gorm for those, you will be just shoving raw sql into gorm. You might as well stop pretending. There’s a difference between having something writing the sql and mapping results into structs. The latter one can be done with the stdlib sql package and doesn’t require an „orm”.
There are two things an sql lib must do to be very useful: prepared statements and mapping results. That’s enough.
metaltyphoon 4 days ago [-]
Perhaps you have to yet use a good ORM? I could probably count on my fingers the times I had to drop to raw SQL in EFCore. Even when you do that you can still have mapped results, which reduces boilerplate.
rad_gruchalski 4 days ago [-]
I’m doing this job for 25 years and I haven’t seen a good orm. Sorry. Look, linq is nice. But linq is not enough of a productivity gain to switch the whole stack from go to .net. I used linq 15 years ago extensively and it feel like magic. But then again, how would you model jsonb select for a variable set of of properties and include nested or and and conditions using its notation? Maybe you could but how much longer is it going to take you rather than templating a string?
marcus_holmes 3 days ago [-]
I've been doing this job for over 30 years and I agree.
Entity Framework was the thing that made me spit the dummy with C#, uninstall Windows, install Linux and discover Go in the first place.
Knowing how to write good SQL is a superpower as a developer, and every time I've worked with an ORM fan I get this reinforced. "The database is too slow!" No, your SQL just sucks.
rob74 3 days ago [-]
Maybe people who have only ever used ORMs are happy with them, because they don't know how much more flexible and faster "raw" SQL queries can be?
bobnamob 4 days ago [-]
You haven’t answered my question at all.
The parent comment made two claims: ORM not great (I agree) and “boilerplate reduction bad” which still needs some elaboration
rad_gruchalski 4 days ago [-]
I answered it, you just don’t see it. One ends up with the boilerplate anyway as soon as one attempts to step out of the usual crud path. There’s no gain, there’s no difference in templating an sql string vs fighting an orm api.
LinXitoW 3 days ago [-]
Any ORM worth it's salt has an escape hatch that allows you to do all those fancy raw SQL queries.
But the amount of queries that aren't fancy, and that an ORM is perfectly capable of abstracting away is (imho) 90% of all queries run.
Why make 90% or queries more tedious and error prone, just to make 10% slightly easier?
rad_gruchalski 3 days ago [-]
I don’t even comprehend your argument. Look: https://gorm.io/docs/, it’s full of strings everywhere. That’s as error prone as anything else and it puts an additional layer of abstraction between you and your sql.
makapuf 4 days ago [-]
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 ?
bluesnews 4 days ago [-]
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
vbezhenar 4 days ago [-]
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.
kgeist 4 days ago [-]
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.
indulona 4 days ago [-]
> 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.
frou_dh 16 hours ago [-]
It goes like this:
1. Software development is a fashion industry.
2. OOP is currently uncool.
3. People who identify as Go programmers don’t want it to be thought of as connected to OOP at all, because then it is uncool by association.
4 days ago [-]
nordsieck 4 days ago [-]
>> Go is not object-oriented language
> That is most definitely not true.
I think at best, you could say that Go is a multi-paradigm language.
It's possible to write Go in an object oriented style.
It's also possible to write programs with no methods at all (although you'd probably have to call methods from the standard library).
That's in contrast to a language like Java or Ruby where it's actually impossible to avoid creating objects.
pjmlp 4 days ago [-]
Unless you happen to want to warm up the CPU, there is very little Go code that is possible to write that does anything useful without OOP concepts, like interfaces, methods and dynamic dispatch.
Creating objects on the heap isn't the only defining feature how a language does OOP or not.
randomdata 4 days ago [-]
Go has objects, but objects alone does not imply orientation. For that, you need message passing.
3 days ago [-]
kaba0 4 days ago [-]
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.
eweise 4 days ago [-]
here you go.
func Ptr[T any](v T) *T {
return &v
}
guilhas 4 days ago [-]
I like in Go how the code looks like a execution graph, by avoiding smarts and just copying code, when you have an error in the log you can generally just follow it through the code as there is only one path to get there. In C# I would have mostly to debug to understand where did it came from
Not just because of the language, but of the simplify culture. Let's see how generics will change that
Rendered at 07:12:57 GMT+0000 (Coordinated Universal Time) with Vercel.
> 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...
It is pity that Go didn't support the "comparable" interface from the beginning. If it has been supported since Go 1.0, then this tradeoff can be avoided.
There are more limitations in current Go custom generics, much of them could be removed when this proposal (https://github.com/golang/go/issues/70128) is done.
I recommend people to read Go Generics 101 (https://go101.org/generics/101.html, author here) for a thoroughly understanding the status quo of Go custom generics.
In particular, people tend to read it as the "sum type" operator, which it is not. I kind of wish the syntax has used & instead of |, what it is doing is closer to an "and" then an "or".
By the time you know enough to know you can ignore that advice, you will. But you'll also likely find it never comes up, because, again, the standard library has already implemented all the sensible variants of this, not because the standard library is magic but because there's really only a limited number of useful cases anyhow. I haven't gone too crazy with generics, but I have used them nontrivially, even done s could tricks [1], and the pipe operator is not that generally useful.
When the generic constraint is an interface with methods is the case that can actually come up, but that makes sense, if generics make sense to you at all.
It probably is a good demonstration of the sort of things that come up on generic implementations, though. Despite the rhetoric people often deployed prior to Go having them, no, they are never easy, never without corner cases, never without a lot of complications and tradeoffs under the hood. Even languages designed with them from the beginning have them, just better stuffed under the rug and with less obvious conflict with other features. They're obviously not impossible, and can be worthwhile when deployed, certainly, but it's always because of a lot of work done by the language designers and implementations, it's never just "hey let's use generics, ok, that one sentence finishes the design I guess let's go implement them in a could of hours".
[1]: Just about the edge of the "tricky" I'd advise: https://github.com/thejerf/mtmap
As defined, the set of the type Ordered is exactly the sum of all elements of int, uint and string. The intersection of int and string would be empty. The or symbol makes sense because an element of Ordered is either a uint or an int or a string. An element of Ordered is not a uint and an int and a string.
It feels to me that static typed languages tend to give you intersection bounds and not union bounds. Rust has intersections, java has intersections. Meanwhile, if you have duck typing then you end up with a bunch of union types (see python -> mypy, javascript -> typescript). There are of course the general union types (not generic bounds) in C/C++/rust which kind of behaves in a similar fashion.
For one thing, as you've specified it, you don't even have a closed set of types. Off in another package I can declare a "type MyInt int" and use your Max on it, so if you tried to type switch in your Max function, you would not know about my type, and it is arguably the defining characteristic of a sum type that you can know all the branches it has.
You can fix that by knocking off the tilde, but then you still have the problem that it is not legal to use "switch val := a.(type)", which is basically the level of deconstruction of a type that Go permits, because when the Max function is running, it is not running on a value of type "Ordered"; it literally has a value of the type you passed in. That's the whole point of generics, to have values of the concrete type that was passed in, and not a sort of "sum type".
https://go.dev/play/p/MGhRjwvpdTh
Note you don't get "Ordered". You get the specific types. That's not any sort of "generic weirdness", that's the real situation. That's why you can only use the intersection of operations they all support.
If you want a sum type in Go, use closed interfaces: https://github.com/BurntSushi/go-sumtype If you're willing to accept what you've written as a sum type, you should be even more willing to accept this method, which actually produces a reasonable approximation of one.
This fails: https://go.dev/play/p/3J4urjOU6lc
But this works: https://go.dev/play/p/Zb_fnAMaqZb It's basically because generics are generated code for specific types with little more than text replacement, and type assertion only works on interfaces, and it can't rule out non-interfaces. But if you box it in an `any`, it's fine, just like it's fine to `((any)(5)).(int)` anywhere else (or any other equivalent construct).I don't understand here. In my understanding, the pipe operator is indeed closer to "or" and "sum type" operator. Interpreting it as "and" is weird to me.
I'm... not sure which way I lean tbh, now that I've seen that idea. Both have merit, it's more of a problem for educational material than anything. If you present it as "these types", | makes sense. If you instead use "these behaviors", & makes sense. | is slightly easier to type for me though, and & has more meanings already (address-of), so maybe I'd still favor |.
It also doesn't currently do anything else people may want it to do, like, accept three structs that each have a field "A" of type "int" and allow the generic to operate on at least that field because they all share it. There's a proposal I've seen to enable that, as the current syntax would at least support that, but I don't know what its status is.
But I doubt sum types will be supported perfectly in Go. The current poor-men's sum type mechanism (type-switch syntax) might be still useful in future Go custom generic age.
[1] https://go.googlesource.com/proposal/+/refs/heads/master/des...
I've been using Go as my primary language for a decade, and the lack of generics on methods was surprising to me the first time I ran into it, and the reasoning not obvious.
There's no theoretical reason not to have it, the reason is because of a random intersection of other design decisions... unless you're saying they made those choices fully expecting to have these restrictions on generics later?
What? Methods are not needed if not for implementing an interface?
Anyway, functions could also be implementing interfaces, some languages allow that.
I swear the go docs read like a cult.
And unless you're also using interfaces, methods are no different from functions aside from call syntax.
Methods bind state with a function.
That an object can satisfy an interface is secondary here. In different languages, an interface could be satisfied with a combination of methods, fields, or nominality.
If the statement "we could decide that parameterized methods do not, in fact, implement interfaces, but then it's much less clear why we need methods at all" was true, then there should not be a single struct in go (stdlib nor elsewhere) that does not implement some interface (and it must be used via that interface to make sense). This is obviously not the case.
https://doc.rust-lang.org/reference/items/traits.html#object...
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.
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.
They are far closer to Rust in some areas (definitely not in type inference sadly, but F# is a different story) than it seems.
Of course if one declares that they are an expert in a dozen of languages, most of which have poorly expressive type systems, the final product will end up not taking advantage of having proper generics.
That's my experience as well in C# - most of other usages of generics are painful to maintain in the long run. I've had most problems with code that joins generics with inheritance.
For libraries (that adopt generics): yes they can be complicated. But using them is mostly zero-effort and gets rid of a ton of reflection.
I've seen ~100 line HTTP handler methods that are implemented using generics and then a bunch of type-specific parameters inevitably get added when the codepaths start to diverge and now you've got a giant spaghetti ball of generics to untangle, for what was originally just trying to deduplicate a few hundred lines of code.
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.
If you know your concrete types, they're just not that useful.
Even in home-grown libraries, I find generics to be a convenience rather than a necessity. It's useful to not have my library code so tightly coupled to my non-library code. But it does also come with a cost: every so often I have to check what the library actually does because being loosely coupled meant that iterations in the rest of the system didn't automatically have to involve the library, so the library code can get left behind.
i think Go having a pretty bad implementation of parametric polymorphism (a programming concept from the 70s) is probably the root cause here
It's very subjective but my gut feeling is they probably didn't expand their community much by adding generics to the language.
No, it still feels like programming with a blindfold on and one hand tied behind my back. I truly don't get it. I've worked with a lot of languages and paradigms, am not a zealot by any means. Other than fast compiles and easy binary distribution, I don't see any value here, and I see even experienced Go programmers constantly wasting time writing unreadable boilerplate to work around the bad language design. I know I must be missing something because some people much smarter than me like this language, but... what is it?
But as I've gotten older, I've started striving more and more for simplicity above all else, especially in systems design (disclaimer: I'm an SRE). Go is pretty good at being simple.
There are some things that still annoy me a whole bunch, though. Like - just one example - `fmt.Errorf` not being a first-class syntactic construct (or the difference between `%v` and `%w` in `fmt.Errorf`).
If you "other than" two huge-for-many-use-cases good things, sure, it might look bad. ;)
But I would add good overall performance and in particular straightforward flexible concurrency support to the list of good things.
And IMO once you're in the set of "things with good perf" there's generally a lot of "boilerplate" of one sort or another anyway.
I'd add what I think is perhaps its most significant benefit to the list: Go fully solved the function coloring I/O problem in a way few other languages (Erlang/Elixir and ... Bend? Others?) have.
That's adjacent to the concurrency benefits in the parent comment, but a little different: allowing procedural, non-colored code to be efficiently concurrent over I/O without introducing function coloring or requiring people to code specifically to an event loop/IO multiplexer in some other way requires good concurrency support in the language, to be sure. However, getting rid of function coloring while providing efficient concurrent IO also requires: a solid stdlib of IO capabilities; a very very good runtime that can coalesce goroutine-concurrent IO into multiplexing OS primitives in the same way a function-colored event loop would (while pre-empting/scheduling in a reasonable way that mitigates unexpected blocking); a strong "critical mass" of libraries to talk to common IO-ful systems; a strong community convention of "we will generally prefer reimplementing IO drivers in Go rather than binding/Cgo-ing in foreign code".
It's when you combine all of those that Go shines as a platform for concurrent (usually network) IO.
I'm more comparing it against languages like Kotlin and Swift, or even Scala.
I still remember people gaslighting everyone that any feature Go had was ESSENTIAL, and every feature Go didn't have was USELESS or too complicated for mere mortals "delivering value".
And the fast compiles at least are in big parts because the language is so horrendously basic. Can't get hung up on checking type constraints if you barely have any.
I'd say this is still the norm in discourse around Go, it's just that the goalposts have moved somewhat since it has more features now.
if it is unreadable, in Go, probably the most readable language used today, i would question the aforementioned experience.
Heck, Go went out of it's way to "subvert expectations" more than the last season of Game of Thrones.
99% of decent C-ish languages either do "String thing" or "thing: String", but Go is so fancy and quirky, it does "thing String" for no freaking reason. Don't get me started on the nightmare that is map types.
I find that exception-based code is much harder to read. The happy path is clearer, but exceptional code paths are often completely obscured. It's harder to reason about what state the program is in when the exception is handled.
And the alternatives are not exceptions, of course, but ADTs.
Generics support is a ubiquitous feature in static programming languages. If it was included on day one in Go, nobody would have blinked an eye. This is only such a controversial topic in Go because the language maintainers made it one.
You should convert when you reach the point "I wish I had that code but with this other type". Even then, sometimes interfaces are the right answer, rather than generics.
> end up copy pasting code instead
bit of my original comment
Without generics, your library has to define interfaces that your users have to implement and it all gets a bit strange and unintuitive.
With generics you can write library code that is easier to use.
The thing I was worried about with this (adding generics) is that we'd start moving more towards the NPM Hell of everyone just writing plumbing code for imported packages. But thankfully that hasn't happened and idiomatic Go still tends to just use the standard lib and very few external packages.
https://github.com/peterldowns/testy
I.e., when you want to write a function that take some slice of any type T that implements interface I, such that []T is a valid input instead of just explicitly []I.
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.
Can you elaborate and give some examples of why reducing boilerplate is generally "bad"?
If you need to make code more complex just to reduce boilerplate, it's a bad thing. If you managed go make code simpler and reduced boilerplate at the same time, it's a good thing.
And boilerplate might be a good thing when you need to type something twice and if you would make error once, the whole thing wouldn't work, so basically you'll reduce the possibility of typo. It might look counter intuitive. Just unrelated example: recently I wrote C code where I need to type the same signature in the header file and in the source file. I made mistake in the source file, but I didn't make the same mistake in the header file and the whole program didn't link. I figured out the mistake and corrected it. Without this boilerplate it's possible that I wouldn't notice the mistake and "helpful" autocomplete would keep the mistake forever. That's how HTTP Referer header made it into standards, I guess.
There are two things an sql lib must do to be very useful: prepared statements and mapping results. That’s enough.
Entity Framework was the thing that made me spit the dummy with C#, uninstall Windows, install Linux and discover Go in the first place.
Knowing how to write good SQL is a superpower as a developer, and every time I've worked with an ORM fan I get this reinforced. "The database is too slow!" No, your SQL just sucks.
The parent comment made two claims: ORM not great (I agree) and “boilerplate reduction bad” which still needs some elaboration
But the amount of queries that aren't fancy, and that an ORM is perfectly capable of abstracting away is (imho) 90% of all queries run.
Why make 90% or queries more tedious and error prone, just to make 10% slightly easier?
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
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.
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.
1. Software development is a fashion industry.
2. OOP is currently uncool.
3. People who identify as Go programmers don’t want it to be thought of as connected to OOP at all, because then it is uncool by association.
> That is most definitely not true.
I think at best, you could say that Go is a multi-paradigm language.
It's possible to write Go in an object oriented style.
It's also possible to write programs with no methods at all (although you'd probably have to call methods from the standard library).
That's in contrast to a language like Java or Ruby where it's actually impossible to avoid creating objects.
Creating objects on the heap isn't the only defining feature how a language does OOP or not.
func Ptr[T any](v T) *T { return &v }
Not just because of the language, but of the simplify culture. Let's see how generics will change that