Trying Out Generics in Go
markphelps.meOne annoying bit about Go's generics is that you can use type parameters in functions, but not in methods.
So for example, maybe you'd want to write a Map function for the Optional type in this article, which returns None if the option is None, or calls a given function with the value of the Optional otherwise.
You'd probably write it like this:
func (o Option[T]) Map[U any](f func(a T) U) Option[U] { ... }
But that doesn't work: "option/option.go:73:25: methods cannot have type parameters"The type inference is also a bit limited, e.g: let's say you have a None method:
func None[T any]() Option[T] { ... }
And you call it somewhere like: func someFunction() option.Option[int] {
if (!xyz) {
return option.None()
}
// ...
}
it isn't able to infer the type, so you have to instead (in this case) write option.None[int]().Generics is a super cool addition anyway though.
Edit: I just found https://go.googlesource.com/proposal/+/refs/heads/master/des... which has some details on why method type parameters aren't possible.
To hoist this comment a bit higher: https://news.ycombinator.com/item?id=29583557 and this one which as the "a hah" moment for me: https://news.ycombinator.com/item?id=29583866
While you can't do this:
You can do this:type Option[T any] struct{} func (o Option[T]) Map[U any](f func(a T) U) Option[U] { ... }
So this is not quite as restricting as it seems. Though it is still likely to be annoying. Runnable example: https://gotipplay.golang.org/p/2w2y1KEjXVEtype Option[T,U any] struct{} func (o Option[T,U]) Map(f func(a T) U) Option[U] { ... }> func (o Option[T,U]) Map(f func(a T) U) Option[U] { ... }
I love GO and it's simplicity and yes I do want generics, but is it just me or is this reading much much harder now ? It reminds me of those ugly voidfuncptr signatures of C and C++ :(
Maybe my eyes should just get used to it but I do feel a little my simple Go now reads not as easy. YMMV
Personally, I think that Rust's Option and Result are not well suited to Go. Option may serve a purpose, but the language was not built around these constructs. It's best (still IMO) to use generics to avoid code repetition, not to build semi-mathematical abstractions for their own sake right now. With time, we'll know how to use generics properly.
Without generics this is
tbh I don't see it as much different. The func argument is the most complex part of the whole thing. And much of the readability comes from not using single-char type names, but you do learn to see past that with time.func (o IntToStringOption) Map(f func(a int) string) IntToStringOption { ... } // ... but that has to be repeated dozens of times
You should also be able to do functions instead of methods, just downgrading the receiver to a normal parameter, if I understand correctly. Other than losing the method call syntax, this is probably the most reasonable workaround, due to the interface problem discussed in the generics proposal.
> func (o Option[T]) Map[U any](f func(a T) U) Option[U] { ... }
This is totally opinionated, but I better not see code like this on a review. Is there a way to make it a bit more readable and a bit less like Perl?
It only looks messy. It’s not particularly difficult to read, and with what it’s expressing there’s really no way of expressing it in shorter terms. “A method on an Option[T] named Map, generic over type U, taking one argument, a function from a T to a U, and returning an Option[U].”
Mind you, I wouldn’t mind colons and arrows as separators which I think make it easier to read; here’s what it’d look like in a somewhat more Rust-like syntax:
I would also note that signatures like these are mostly found in foundational types; they take a bit more effort and practice to write, but you don’t often have to do so; and have the outcome that the API is more pleasant to use—no more interface objects and downcasting everywhere, in Go terms.fn Map<U: any>(self: Option<T>, f: fn(a: T) -> U) -> Option<U> { ... }I got your point, and it's worth mentioning that there's that rare moment when Rust looks more readable than Go.
Concerning resulting API: isn't code generation solves a problem with interface objects and downcasting?
I’m not sure what you’re asking. Generics are code generation (look into the term monomorphisation), just automatic and managed by the compiler for better convenience than manual code generation by other means, and type safe for better correctness and efficiency than interface objects and downcasting.
Thank you, i wasn't able to decipher the syntax until i read your comment “A method on an Option[T] named Map, generic over type U, taking one argument, a function from a T to a U, and returning an Option[U].”
I’d argue a lot of the ugliness of this is a product of the basic syntax of genetics in go.
The syntax in other languages with generics (C#, Swift, Java, and even c++) for this construct is easy to read. And obviously there’s always Haskell where you often don’t need explicit type annotations at all :D
I guess in Java, it'd look something like this:
Kotlin might be a closer match in semantics if I use an extension function:public class JavaOption<T> { public <U> JavaOption<U> map(Function<T, U> func) { //todo } }fun <T, U> Optional<T>.map(func: (T) -> U): Optional<U> { // todo }What's un-readable about this ? I haven't even read the article but I can understand this. Probably because of prior experience in C++, Java. But it seems sweet and succint.
At the max, one can make a couple of type-aliases for a bit more legibility, but that's all one can squeeze.
I want to mention that Go doesn't have a ternary operator, because it is "used too often to create impenetrably complex expressions"[1]
There is another way, you can declare option as
I wish there was type inference for function arguments, so that you could write func(x) { return double(x) }. Maybe in a couple of years the Go team could be convinced.type Option[T any] *T nil is None opt == nil instead of IsNone() func Some[T any](t T) Option[T] { return &t } *opt instead of opt.Get() option.Map(opt, func(x int) double { return double(x) }) for the monadic behavioryou are programming go why do you expect consistency
>One annoying bit about Go's generics is that you can't use type parameters in methods.
i haven't written go in a long time (generics would/could get me to go back to it) but are you saying that functions can't be generic? or is members here vernacular for class (struct?) associated functions? i thought those were called "receivers", which you mention further down. so it looks to me like you're saying that functions can't be generic. to which i ask: wtf is the point of generics when functions can't be generic...?
You can use type parameters in functions, but not methods. Methods are functions which have a receiver, so:
In the second case, "Foo" is a method which has a "bar" instance as a receiver.// This is a function, you can use type parameters here: func Foo[T any](g T) { ... } type bar struct {} // This is a method, you can't use type parameters here: func (b bar) Foo[T any](g T) { ... }I've edited my original post to make it a bit clearer.
Doesn't C++ have a similar restriction, in that virtual methods cannot be templates? And in Go, with its dependence on interfaces everywhere, every method is the equivalent of /comparable to a C++ virtual method.
Yes, and you are right, Go interfaces are similar to pure virtual base classes in C++ (to the extent of virtual dispatch being discussed).
so does this mean there are no generic structs in generic go?
You can do
just fine. What you can't do istype X[T any] struct{} func (x *X[T]) Method() {}func (x *X[T]) Method[T2 any]() {}Not saying mixing OO and generics could never have any merit, but.. Isn't a method just a function having an object as first parameter. Does Go change this beyond "syntactic sugar" somehow? Been a while from coding Go, so interested to hear.
The rationale seems to me that generics be functions first (ok, procedural), and not complecting it with objects and OO too much, whatever that mix could mean..
> Isn't a method just a function having an object as first parameter.
Differences:
- Methods must be defined in the same package as the receiver.
- Methods can be used to implement interfaces.
- Methods can be discovered dynamically by inspecting the type of the receiver (either through reflection or with a dynamic cast).
In other words, methods cannot introduce generic types.
Honest question:
Do you think Generics will be an overall win for the Go language, or will they be overused / end up making code harder to read / harder reason about?
I kind of hate looking at Go code containing generics. What previously was elegant and easy on the eyes is now annoying and muddled by the additional layer of abstraction. I'm saying this with sadness, as someone who fell in love with Go back in 2012 and still writes it at least weekly.
Is it a better bet to move on and go full Rust, rather than bother with wherever the goggle golang train is headed?
p.s. Even though code generation is [also] annoying and perhaps not ideal, I've rarely needed to use it and kind of liked that it was inconvenient - forcing me to think about problems differently. Certainly some problems will benefit from the addition of generics, but is it really enough to justify the added complexity? I wonder if this is a case of tragedy due to vocal minority.
p.p.s. Generics in other languages like Java or Scala seem fine, as they are "kitchen sink"-style "all things to all people" languages. Such behemoths are nearly always clunkier and less easy to read than pre-generics Golang.
> Is it a better bet to move on and go full Rust, rather than bother with wherever the goggle golang train is headed?
Ecosystem aside, with Rust you'll get far better language design (generics fully integrated with everything (stdlib, consts, all libraries), sane error handling, sane dependency management, no "any" type, editions, and more. More complexity too though.
> Certainly some problems will benefit from the addition of generics, but is it really enough to justify the added complexity?
Coming from languages that have them, it's just hard to take Golang seriously, where every library either ditches type safety (more runtime errors I wouldn't have with other language), or forces you to copy-paste code just because you need support for some new type (more boilerplate to maintain == more errors). Or reinvents generics with code generation and aboriginal characters.
Once you start using generics, they really aren't complex.
> Coming from languages that have them, it's just hard to take Golang seriously
Some people judge the language on their ability to get work done with it.
I get generics, but they really don't come up in daily use with the tasks where I use Go. Yes, it would be nice for writing some libraries but you're going through a laundry list of Rust features which don't hamper my ability to get work done at all.
I even like Rust, but if I'm going to write a worker that will read from a queue, do a transformation and write to a few more queues/services upon completion, Go just works and the turn around time is far better than Rust. It's like was Perl was for Unix, but for the cloud instead.
Agreed.
I wrote a bunch of Rust, Scala, Haskell, but I still greatly prefer Go, even without generics.
I am very happy with the generic container libraries I'll get with generics, but I hope people won't try to be too clever (as they usually do). So far, Go is a language that just rules out a lot of bikeshedding, which I very much appreciate. We'll see how that evolves.
I do think premature or wrong abstractions are a much bigger and widespread problem than a lack of abstraction.
Also, I really like Go's error handling, it results in error messages in Go projects usually being top-notch (because of explicit handling and wrapping which includes relevant context and human-readable messages).
I'm with you. Go has its problems, and I am aware of them, but it is just so compatible with the way my brain works, it is amazing.
I remember listening to a podcast about C++, and the guest explained how after working with C++ for about five years, they still encountered aspects of the language that surprised them on a regular basis (to be fair, though, that was before C++11). To me, Go just clicks in way few languages did.
I think language preference is very much about how people’s brains work (and they all work differently!). My brain struggles with Go, but clicks with Rust.
This must be it and ditto for me on Rust. It feels like they made the language just for me.
Interesting - I've also written a ton of code in those languages (not as much in Haskell), and Go would be my last choice by a large margin (and Rust my favorite by a wide margin). I went through a love/hate relationship for a while (it lets me create static executable, compiles pretty fast, it is easy to remember, etc. but if I have to type "if err == nil" one more time!!!).
While I applaud the focus on simplicity, I found it simply transfers that burden to the programmer (I have to loop over a map to clear it...really?). Every single "lack of" feature in Go (has nil, no sum type errors, no pattern matching, etc.) is in Rust which gives me endless freedom to express safe, correct programs. I suppose language choice is highly individual, but it still perplexes me as to what people see in Go over Rust.
I'm sure everyone's experiences differ, but in my travels Go has shown to carry the lowest rate of "who wrote this shit?" when you come back to a project five years later. I agree that it puts the burden onto the programmer upfront, but reduces it down the road. Tradeoffs...
I have a lot more fun writing code in other languages. I enjoy not having that burden on me while writing code. In an earlier life that would have been important to me. Now that I'm old and curmudgeony I've started to value other things.
I work in a team, where not all are unicorn Rust specialists. The work we do doesn't require millisecond-level response times, nor is it something that needs to be ultra-safe in relation to memory safety.
I can teach any mook with basic Java/C# programming knowledge how to be productive in Go in less than a week. At this point they can read pretty much any Go code pretty fluently and can be trusted not to commit anything stupid.
Can you say the same about Rust?
> I work in a team, where not all are unicorn Rust specialists
> I can teach any mook with basic Java/C# programming knowledge how to be productive in Go in less than a week.
This is fair, and probably the reason why Go continues to be popular I guess
> The work we do doesn't require millisecond-level response times
Rust is a high level language, and it is a bit of a misnomer that it is only good for low level things. Most of my stuff doesn't require this level of speed either (the previous major version of my project was written in _Python_). I use Rust for the safety and data structure benefits, not speed.
> can be trusted not to commit anything stupid
As a Rust coder, and a fan of functional programming as well, I personally find any "null pointer" error quite "stupid" and unnecessary as is the occasional "err == nil" instead of "err != nil", or forgetting to check it at all. We will probably have to disagree on what constitutes stupidity, and that is fine.
Absolutely. You can write some Rust by following some hello world tutorial and be productive in 15 minutes. That doesn't mean you'd be proficient in writing procedural macros or understanding all details of memory management, but I've seen Linux magazines presenting Rust omitting all those "advanced" features and surprising amount of Rust users is new to programming.
The learning curve is usually different because you will need to understand more before program will compile and because most materials aims to cover 100% of the language from day one, but if you want to approach it differently, productivity wouldn't be a problem.
Just so we're clear, you're saying that Rust only takes 15 minutes to learn and be productive in, for the average person?
Basic Rust, sure. For some definition of "basic", and average person that knows some other programming language. What I mean is something along this line:
https://www.linuxjournal.com/content/getting-started-rust-wo...
It leaves many concepts without full explanation, but that's not necessary to do something useful.
Premature or wrong anything is clearly bad. But is parameterisation a premature or wrong abstraction? I'd say that parameterisation - even in the type language - is the original proven abstraction.
I think this is why I always have trouble understanding arguments against generics (in the general case at least). Parameters are added to functions/methods in order to make them more generic/flexible, as a rule. To move something that may have been hardcoded into something that can now be configured:
When the type either does not matter, but the concrete instance records it, or the type makes sense to be configurable, you want generics. As a silly example (but short enough to fit into a comment block):get_data() { filename=default ... } =becomes=> get_data(filename=default) { ... }
Now you have to make a new nth for every single sequence type, even though it has no bearings on the actual operation. Or you make it generic:fn nth(seq: [int], n: int) -> int
That's a trivia case, yes. But if anyone has ever worked on a complex code base there are plenty of situations like this that turn up, at least in my experience. Sure, I almost always start off with concrete instances with a fixed type, but as soon as it becomes apparent that the type itself is irrelevant and I have a couple use-cases with different types, why not make it generic and be done with it? Like, would you really have more than one version of that get_data function running around, one for every conceivable filename? That would be obscene. Why would you do the same with types?fn nth<T>(seq: [T], n: int) -> TYes, in my opinion oftentimes it is.
There is a reason why even in mathematics people like to operate on concrete examples to get an intuition. For many, concrete is much easier to understand than abstract.
That's the less important point. The more important point is that making your code generic often involves more trickery which makes the code more complex, even if you only use the code once or twice - so that's just effort wasted.
The fact that parameterization is a proven abstraction doesn't mean it's good everywhere. Same as I don't agree with the "Clean Code" way of creating a myriad of 4 line functions.
Yes, good programmers won't make these mistakes, you can totally handle them. But when arriving at legacy code or open-source projects I greatly prefer to find under-abstraction rather than over-abstraction.
To be clear, I'm not against generics, I just agree with the parent of my original message. I'm worried people will overuse them and I don't want a whole laundry list of Rust features in Go. I'm very happy about libraries with type-safe generic B-Trees.
I get what you mean, and I personally rarely write generics, but when I need them they're great. I think that a good rule of thumb that would be easy to implement and review would be "no generics outside of libraries". This way, you get your type-safe containers, your application code doesn't get much more complex than before, and if you want to introduce generics, you have to really think about it.
> I get generics, but they really don't come up in daily use with the tasks where I use Go.
I sort of understand this argument, but I can't really imagine defining queue as something else then <T> wait_for_item() -> T. I've been writing Python for to long to know that wait_for_item() -> any will backfire in production eventually and I don't want that. At some scale (both in code size and amount and scope of dependencies) those problems just become too serious and too common to not have language that deals with that. And Go is way too popular for people to limit its use only for the cases where it currently works.
Define queue using generics, sure. It will be huge for people waiting packages.
A specific queue is typically well defined and has a struct in/out.
There are times when I've wanted arbitrarily nested JSON that doesn't map into structs very well, but it is uncommon enough.
I guess a better response to this is that I don't use generics a lot when writing Go, but I probably use a lot of packages where they would have been incredibly useful.
> if I'm going to write a worker that will read from a queue, do a transformation and write to a few more queues/services upon completion, Go just works and the turn around time is far better than Rust.
Since the inception of async/await in Rust, it is incredibly quick to whip something like that up. The slowest part might even be the time it takes to compile. Maybe that's what you were referring to?
The way Go does it is easier to reason with, channels and goroutines are really easy to explain to anyone.
That does sound a lot easier than Rust's channels and tasks...
> Some people judge the language on their ability to get work done with it.
Really? Safety and correctness aren't relevant for you? Then why even bother with go instead of Python or a Lisp?
For me it's crucial that a programming language contains tools that allow me to definitely rule out as many errors as possible. A powerful (and sound) typesystem does just that.
How are you finding Idris? or are you more in the Agda camp?
If you want to definitely rule out as many errors as possible, dependently typed languages are the state of the art, allowing you to write a sort function that will fail to compile if it returns a list that isn't sorted (eg,https://dafoster.net/articles/2015/02/27/proof-terms-in-idri..., or https://www.twanvl.nl/blog/agda/sorting).
After all, if you can't even prove basic properties about your code from your language, like array accesses being within bounds, are you really using all possible tools to rule out errors at your disposal?
To be fair, I never had a chance to use Idris nor Agda. I don't know how capable I would be to encode the proofs in libraries nor client code. If it's usable, I am all for it.
Otoh, I do know that languages like OCaml, Haskell, or Rust take the burden of trivial errors from my shoulders for neglible cost.
Yeah, that was an insincere gotcha question, and it's a shame that it could potentially undermine other readers' potential value of a higher degree of confidence versus a hypothetically perfect confidence.
Does Rust also have a feature that instills a burning desire to proselytise?
I like Rust mostly for its user-friendly tools. I dislike the compilation model. The type system I already liked even before Rust existed. So, not every String opinion on PL is down to Rust.
No, I just find discussions like these absolutely hilarious. A Go developer with ten years of experience convinces himself that Go is the gospel and generics are a useless toy and distraction from "real work™", then tries generics and they move from a feature you don't really need to a feature you couldn't possibly live without.
Same with every other thing that goes into this language. A thing that's been available elsewhere for literally 3-4 decades.
We have some examples in this very thread.
Your description doesn’t match what I see in this thread at all.
Odd that you’re so upset by other people’s choices.
Yes, all Rust devs get infected with the PROSELTIZE virus. Other languages are desperately trying to develop vaccines.
> > Some people judge the language on their ability to get work done with it.
> Really? Safety and correctness aren't relevant for you? Then why even bother with go instead of Python or a Lisp?
A charitable reading of the GP would be that "safety and correctness" would be included in "getting work done", in amounts that are appropriate to the work in question. Your interpretation is... less than charitable.
Exactly. Go is safe enough. Correctness for most programs is more about making sure it solves the problem correctly, not merely that it guarantees no errors if it runs. That feels like a modern version of "It compiles, I'm done."
Maybe they don't want a target directory using 3gb from a clean build.
It's already self-evident that Go manages that just fine. That's just weird dogma.
> It's like [what] Perl was for Unix, but for the cloud instead.
Perl and Go both have the kind of long-term language stability that I value above all.
But Go offers excellent concurrency, networking baked-in, and now even fuzzing.
I use Go every day and yeah you can your work done with it but not quickly due to typing all the boilerplate.
> Some people judge the language on their ability to get work done with it.
I value maintenability (type safety helps prevent unintended consequences) and readability (Haskell and Clojure are out).
I haven't even written that much go, but even with the little I have written, I have felt hindered in my "ability to get work done" by the lack of generics.
That comparison is a disowner for Perl, given its language capabilities.
It's a disowner for Go given that Perl's philosophy is "more is more" and Go's philosophy is the Wirthian "less is more".
There is a market for a language like Rust but with garbage collection and reflection. There is OCaml, but it's not for the modern developer. Go with generics is the closest thing to that which is getting some use.
This more|less also describes Nim. The recent automatic memory management ARC/ORC alternative is not really even what most people think of as "garbage collection". Its macros give you full AST accept & re-emit powers and it's had generics since before Go existed. I realize it probably does not score highly on "gets used", but it deserves more attention.
I think so too but I think the languages that fit the bill are Kotlin and Swift. Both modern syntax, great generics w/GC and ARC respectively.
Go w/generics falls very short IMO, expressibility, type safety and poor null handling all rule it out as a reasonable stand-in for Rust.
> There is a market for a language like Rust but with garbage collection and reflection.
That's why languages like JS/TS, Haskell, Elixir, OCaml (which is way more modern than Go), ... exist and are used.
That language is Swift. It's got a lot of similarities with rust, just with everything being ref counted.
If only its ecosystem was more platform agnostic.
Swift is pretty much that language. It doesn't have the imitable crate ecosystem or tooling, though.
Why not Ocaml?
Ocaml's ecosystem is a total disaster. Do you use the lousy included standard library? Do you use Jane Street Base? Jane Street Core? Async? Lwt? Batteries? Containers? Iter? Esy? Opam? Ocamlbuild? Dune? Have you seen Dune's documentation? And Facebook fractured the ecosystem even more with Reason, obnoxiously.
Slight nit: Rust does have an `Any` type[0] (and has since at least 1.0). Unlike Go's `interface{}`/`Any` type though, it's actually type-safe; the only way to get a value from it is to try to downcast into a concrete type, which returns an Option and will always be `None` if it doesn't match the type.
That's generally how interface{} works in Go too. In local idioms, but the same effect. Only valid operations are permitted.
If you continue to disagree with me, please be specific about what operation Go permits on interface{} values that you consider type-unsafe.
The trailing suffix cast (not sure the actual name, but `x.(string)` and the like) are not type safe. Yes, it's not required to be able to try to deal with an empty interface type, but it's there, and it will always compile fine and allow code afterwards to freely assume the cast succeeded without any errors.
I imagine the rebuttal to this is that you could always just manually `unwrap` the `Option` that is returned by the downcast methods in Rust, but I pretty strongly feel that adding explicit syntax for this type of unsafe cast normalizes it to an extent that having an `unwrap` method on a generic Option type doesn't remotely approach. It would be quite a stretch to argue that having the `unwrap` method on Option is explicitly a endorsement on unwrapping on the downcast methods given that Option is used for far more than just that (and especially given the huge amount of stigma that using `unwrap` gets in the community, which is mostly fair but sometimes goes a little overboard). On the other hand, having specific syntax that is used for unsafe casts and nothing else is a pretty explicit argument that it should be done sometimes, or else it wouldn't be in the language at all. Go could pretty easily have gone the route they did with map lookups and had the unsafe casts return two values, the latter of which is a boolean indicating success or failure (and in the cast of `false` being returned, the former value would just be the zero value for the output type), and the fact that this wasn't done means that ergonomics was prioritized over safety.
"Yes, it's not required to be able to try to deal with an empty interface type, but it's there, and it will always compile fine and allow code afterwards to freely assume the cast succeeded without any errors."
That is incorrect. It issues a runtime panic, which is the same as the syntax for Rust that will do the same thing. Or you can use the "x, isX = y.(SomeType)" syntax and it will tell you whether it matches or not.
It's the exact same functionality just spelled differently, but there is no scenario where you have an int but you call it a string and the code simply proceeds along and does whatever.
"unsafe casts return two values"
It does do that! It's done it since the beginning. It's not a cast, though. It's a "type assertion". It can't convert. Go only has casting for safe conversions... well, things most programmers consider safe. I don't consider int -> byte "safe" but I am in the minority on that.
You need to stop talking about Go. You don't know it. There's nothing wrong with not knowing it, but you shouldn't combine that with trying to explain it to people. It isn't as crazy as you think. It is definitely type-safe. The "type safe" that it is is less rich and complex than Rust or Haskell, but it is type safe within its type system, subject to the usual "unsafe" caveat. If it weren't, it would never had needed generics... it would be a dynamic language and they build generics in so deep they aren't even "generics", they're just how the language works at all. The whole reason Go needs generics is precisely that it isn't type-unsafe.
> That is incorrect. It issues a runtime panic, which is the same as the syntax for Rust that will do the same thing.
As I said before, having explicit syntax for it is a very different thing than having a method for it on a generic type that isn't specific to it
> It does do that! It's done it since the beginning
That's good! Still isn't required though, and having a safe way to do something doesn't mean that the unsafe way doesn't exist
> It's not a cast, though. It's a "type assertion". It can't convert.
Okay? It still lets you get errors due to the type system not preventing them
> It is definitely type-safe. The "type safe" that it is is less rich and complex than Rust or Haskell, but it is type safe within its type system
I agree that type safety is a spectrum, and very few languiages are fully type safe. I probably should have been more clear that I wasn't saying that Go wasn't 100% unsafe, but I thought it would be obvious I wasn't saying that. Clearly that's not the case.
> If it weren't, it would never had needed generics... it would be a dynamic language and they build generics in so deep they aren't even "generics", they're just how the language works at all. The whole reason Go needs generics is precisely that it isn't type-unsafe.
I'm not sure what this means; pretty much every statically typed language gets benefits from generics, and they're clearly not all equally type safe, so I don't know what this is supposed to convince me of.
> You need to stop talking about Go. You don't know it.
I don't know everything about Go, that's true. Go doesn't have a special definition of type safety though, and recognizing places where it isn't type safe doesn't require complete knowledge of the entire language.
They are called type assertions, I believe they are type safe, albeit at runtime. It will panic if the assertion is incorrect.
Is Rust really a suitable replacement for Go? I mean I know they are both system languages but I feel they have different use cases completely.
I'm a Rust "fanboy" (if one wants to say so), and I still think that the use cases for Rust are just a minority in the landscape of modern languages (that is, where there is a choice).
Rust has a mind-boggling overhead, for many reasons (and I'm not talking about the borrow checker, which I think one gets used to after some time), even if the language itself is consistent and ergonomic (within the intended constraints).
To me, they have very different use cases - one will definitely know when Rust is required or it's an appropriate pick. For the rest, Go is fine. I think that those who put them on the same basket, haven't really used one of them.
Regarding systems programming, my opinion is that they require the lack of a runtime (not just because of the performance, but also, for the framework(s) written with low-level primitives in mind), but this is arguable (in particular, there's no clearcut definition of what systems programming is).
Also a Rust "fanboy" and I would disagree. I'm amazed how productive I am in Rust (about 6 months in). I've written tons of Go as well, and I write Rust faster (What I was expecting was to write better code, but have it take longer, but turned out not to be true). I also thought the language would be too "low level" for the type code I write (I wrote in Go and Python mostly before, though I've written in many languages in the past), but I found it scales very well up and down to low and high level challenges (Serde _rocks_). At this point, I use Rust for everything except a couple hundred line script (Python for that).
I'm not saying everyone would fit in that camp and it is a harder language to get started in for sure, but I think the borrow checker scares away too many people. It is a learning curve, but when it clicks, you will realize that every language has ownership and borrowing... you just didn't realize it because the GC allowed you to be sloppy about it. Once you do, it makes you a better programmer (just like coding in Haskell does).
> I've written tons of Go as well, and I write Rust faster
The overhead in Rust, when compared to comparable languages, is very concrete. On top of my head:
- using hash maps is more convoluted (in some cases, arrays also need some boilerplate as well)
- bidirectional/circual references need to be handled (and are also ugly); algorithms programming in Rust is typically more complex because of this
- lifetimes (which also spread virally when introduced)
- explicit allocation types
- (smart) pointer types in general
- interior mutability; which may also required additional design (including: will the mutexes destroy performance?)
Some of them intersect with each other and pile up (allocation types; pointer types; interior mutability).
There is certainly overhead in Golang (I think it's not very ergonomic for a functional style, for example), but it's nothing comparable.
Overhead takes time; unless one has a time machine, it makes a programming language concretely "slower".
The above is just the concrete overhead. The abstract overhead (=things to keep in mind) is another story (e.g. proliferation of types because of the ownership, traits...). I understand, say, that path types are a necessary evil, but they're surely ugly to handle.
> you just didn't realize it because the GC allowed you to be sloppy about it
It's not sloppy where it's not needed. A significant part of the Rust overhead is due to the rigorous philosophy of the language, which enforces constraints also when they're not required. This is absolutely fine, but it's not realistic to think that it has no cost.
I thought this as well before I had used the language for major projects, but in practice, I found it not the case (at least for me). Your list of concerns I do not find to be anything I think about day to day as I'm coding. I just write code normally for the most part. Yes, you do have to think about your data structures and how you will use them, but in other languages I found myself redesigning these later because I had come up with the wrong paradigm - in Rust I find myself getting these correct the first time, so perhaps the restrictions I find helpful here (I suspect the earlier poster commenting how certain languages fit your thinking patterns may be on to something).
My Rust code typically 'just works' the first time or close to it (something I haven't experienced since writing Haskell and Ocaml), but in other languages I would not experience this, and I'd spend more time debugging. I have gotten stuck a few times in Rust as part of my learning journey, but overall, I'm still proceeding at least as fast if not faster than I wrote in Go and other languages.
Rust is definitely not perfect, and I see some of the warts, but I don't want to write a major project in anything else at this point.
Ironically I've been using a lot of hashmaps in some code recently...and the rust implementation is pretty damn ergonomic.
Certainly better than C++ or C# IMHO, and even my Python colleagues were amazed how easy it was to work with.
I have only touched Rust & Go but I would only consider Rust where I would previously have used C or C++. The stuff that needs to be fast, low memory, and what not. I see Go as more of a Java. Good for backend systems. Also CLIs.
> they are both system languages
Due to ability to work without stdlib, Rust is system language in a sense Go never will be, to the point Go authors withdrawn this definition.
> Is Rust really a suitable replacement for Go?
It depends on your requirements for the ecosystem. If you need compatibility with Go or Go libraries, than it's not a replacement.
Libraries and support aside, any program written in Go can be written in Rust (and going back to nostdlib, many programs written in Rust cannot be written in Go). If you have required libraries, I'd say it can be written comparably quickly and easy. For example you can have web service returning hello world in 10 lines of code or so in either.
They are popular in different circles, but that is mostly not related to technical abilities. For 99% of of applications, you could pick either one.
Rust is overused where Haskell or OCaml is more appropriate, simply because it people prefer the general quality of it over more mainstream languages even at the cost of putting up with memory management minutiae. Simply put, one rarely every need to use Rust in a better world.
This is a good direction for Go, which will either lead to Go having a better ecosystem, or remove ideological barriers from people keeping on using Go.
> Rust is overused where Haskell or OCaml is more appropriate
Except then I'd have to learn Haskell or OCaml :) As a curly-bracket language programmer, Rust was much easier for me to get into and feels more like home.
I found this closed-mindedness hard to understand -- I don't spend very much conscious thought on the syntax when programming at all -- but for people like you facebook made Reason ML https://reasonml.github.io/
Someone should port OCaml to the Go runtime with a good high-level FFI. It could really give the community a boost.
The actual need for generics is, I think, overstated. If you are writing a very abstract library, maybe, but most problems that I have encountered don't even begin to approach needing them.
> Or reinvents generics with code generation and aboriginal characters.
Tell me you’ve never used Go without telling me you’ve never used Go.
I believe OP is referring to this https://github.com/vasilevp/aboriginal
Yes, I know, but that was a transparent joke.
> Do you think Generics will be an overall win for the Go language, or will they be overused / end up making code harder to read / harder reason about?
Here's my $0.02 from a java background. There will be cases where someone overuses generics. That happen pretty much every time a new language feature lands in any language.
However, my expectation is that like java, you will see Go generics in general practice only come up when dealing with things like collections. Once the community settles and stops trying to use generics as a meta-programming language, they will become pretty boring (which is what you want).
From a readability standpoint, IMO, generics in Java don't really have a negative impact on readability. Sure, you'll get the random `Map<Map<String, Object>, Object>`... but most people see that as the anti-pattern it is.
In short, I'm guessing they'll be a win.
yeah - generics can get fairly complex for library authors that want to be maximally flexible (which is optional!), but the end result generally tends to be a library that users can use correctly by accident, without needing to understand generics in depth.
It's that "correct by accident" part that's hard to have both safe and performant without generics.
I have never needed generics in Go, and I've probably been using it since 2017. I've never even once had to resort to any interface{} trickery to express what I want, and I've written Go programs for Fortune 50 companies, as well as complex personal projects such as AST parsers/code generators.
I'm pretty disappointed to see generics introduced into the language and every example I've seen feels completely unreadable to me compared to pre-generics implementations.
To be clear, it has never been the case that the Golang authors were 100% against generics. It has always been their position that the implementation needed to be good enough to make the trade-offs worthwhile. I just don't think they chose the right trade-offs.
> I've never even once had to resort to any interface{} trickery to express what I want, and I've written Go programs for Fortune 50 companies, as well as complex personal projects such as AST parsers/code generators.
That's pretty surprising to me. Have you never had to implement marshalers for unknown types and such? I have had to implement things like json.Marshal and json.Unmarshal for different encodings dozens of times in my Go tenure. I have had to use reflection a lot. I have had to deserialize into map[string]interface{} to handle ambiguous situations at runtime a lot. Have you never even had to wrap or build your own Printf equivalents that accept interface{}? No loggers? No custom containers? None of that which operates on unknown types?
I see use of interface{} all over the vast majority of Go projects. I think your experience may be atypical.
It's entirely possible that, through some strange quirks of circumstance, I've managed to avoid every problem space that would make me wish for generics.
In spite of that, it's unlikely that I've written implementations where using interface{} would be easier to read and reason about than not using interface{}. And the experience of the author whose blog post we're commenting on tracks with mine: "In my 5+ years working in Go, I can probably count on one hand the number of times that I felt like I really needed generics." I can too, just without using any fingers :-)
I feel the similar way, though I wouldn't be so brave to say I didn't ever use interface{}. I think we all work around slices and maps being the only generic containers and don't know we do. I think everyone will find that while they didn't need generics, they will help them when using utility libraries. Java 1.4 people thought the same.
I expect well-curated libraries to come about that will really simplify some otherwise difficult problems for people (e.g. task/object pooling). I'm even toying with a futures impl at https://github.com/cretz/fut, but I wouldn't use it in place of channels in most cases.
Yes. It would be short-sighted to dismiss them out of hand, and it's possible that generics will provide a level of expressiveness and readability I haven't anticipated yet. I'm fairly bearish on it for now.
> I have had to deserialize into map[string]interface{} to handle ambiguous situations at runtime a lot.
Something I worry about is if I'm getting too jaded. You really think things are different elsewhere, but then you see that we all eat the same shit sandwhich.
That code would never have to be written if someone just used their brain before switching their integer IDs to string GUIDs. Bless your soul but I wish we didn't have to resort to such things. Some things code can't fix.
His experience coincides with my own having solved many business level problems using golang for several years.
At most I have used non-empty interfaces to solve very few number of issues (countable on one hand). I have never needed interface{}.
> as well as complex personal projects such as AST parsers/code generators.
Funny you say that because for me that's the one use case I have for generics.
(edit: why the hell am I getting downvoted for posting a fact? I wasn't offensive, argumentative, etc. Just citing one example I've run into where generics would help me personally)
There are areas where they will help, and be pretty transparent to the user, lots of places in the stdlib, one trivial example: math.Max(a,b) could take any numeric instead of just float64, sort could be neater etc.
Perhaps a Go 2 if they ever get there could be a generic std lib rewrite, largely transparent to the end user, with some minor incompatibilities allowed and lots of stuff rewritten behind the scenes. They could remove a few ugly corners in the stdlib naming specific types by using generics, add a few more container types perhaps, deprecate some old stuff and move it out.
I'm hoping Go 2 replaces the file struct with an interface too. That's been a particular pain point for me.
You mean like https://pkg.go.dev/io/fs#File?
Sorry I should have been more specific. I mean the os file struct https://pkg.go.dev/os#File
The os package makes use of a *File struct rather than an interface. The authors acknowledged this was a mistake but it's some of the oldest code in Go and Go's backwards compatibility guarantee has meant that they cannot fix that.
Since I author a $SHELL written in Go, being able to add in my own *File methods would have allowed me to add in some cool features. But I've found workarounds in most cases. It's just not as clean code as it could have been.
What I am saying is, there is already an interface, which os.File should be implementing.
And what I'm saying is that interface was created after os.File was created and after go 1.0 was released thus that change now cannot happen without breaking the backward compatibility guarantee. Hence my point about go v2.0
never needed min/max ?
Would you use generics to write the same implementation of min/max for integers and floats?
Absolutely, yes. It's downright trivial: https://gotipplay.golang.org/p/XF6wM3JF2QM
From https://itnext.io/generics-in-golang-1-18-no-need-to-write-m...
This is a joke, right? Your trivial implementation isn't even correct.
https://gotipplay.golang.org/p/N2v8aB1tUtN
Is this one better? It does run and doesn't rely on casting (not sure why GP's source took that route, it was lossy and kind of dumb). I suspect the compilation problem was because of a change in the syntax between when that was created and today.
yeah, it's just old. the `~int | ~float32 | etc` syntax is relatively recent.
Why not? You later seem to think that this would require reflection - but that makes it apparent you don't understand how generics work in a language. They're used at compile time - to avoid runtime checking.
You certainly might use them to write a facade which made life easier for consumers of the math pkg and accept floats,ints or uints even if behind the scenes it splits into different implementations.
I would not be very happy if every time I called math.Min, I was also, under the hood, calling reflect.TypeOf.
The whole point of generics is to avoid this - why do you think you would have to use reflection?
The fact that you would not need a separate one for each of the: uint8 , uint16 , uint32 , uint64 , int8 , int16 , int32, int64 is not enough ?
As someone who's been writing Go full-time for only about 8 months now, I've repeatedly been frustrated with the lack of generics while building web APIs. For example, the Go GraphQL ecosystem is a bit of a disaster full of type unsafe code and use of reflection or code generation to support simple things like "a resolver that returns FooResult" vs. "a resolver that returns BarResult."
Here's a fun one I stumbled on: How do you implement a PUT endpoint where a missing JSON value is treated different than a null JSON value? This ends up being very difficult and requires a boilerplate wrapper type for every single type you might accept. It's even worse when you start accepting slices or maps, or slices of maps...
These are areas where generics will help me a lot.
This is because JSON and GraphQL are hot garbage. You may as well have written:
> How do I disable static typing for this statically typed language?
I have sympathy for your struggles -- I've been there. But fundamentally this always ends up being a problem of putting a square peg into a round hole.
I don't expect most folks to agree with this take, but I have the utmost faith it'll age well.
If you don't believe me now, set a reminder for ten years and see how we feel about JSON and GraphQL.
You're missing the point: even if JSON and GraphQL were better, untrusted data is always "untyped". Something has to parse the raw representation until proper data types the type system can understand.
Ideally this is all transparent, and programmers can stop wasting their lives reimplementing this stuff again and again and again, but even if programmers don't waste their time reimplementing it, computers will spend a decent amount of time running it, at least where there is more than one process / machine / whatever in question and therefore untrusted boundaries.
I don't follow. Writing a parser doesn't require generics. I've written hundreds of parsers. You can even use a generator like protoc to provide you read/write code that returns/uses concrete, static types.
What on earth does dealing with untrusted inputs have to do with anything?
I wasn't primary talking about generics, I would responding to
> You may as well have written:
> > How do I disable static typing for this statically typed language?
and so just talking about parsing and static type checking.
As it turns out, generics do help immensely if one wants to use so-called "parser combinators".
GraphQL is typed.
> How do you implement a PUT endpoint where a missing JSON value is treated different than a null JSON value?
Tbf that’s a pain in the ass everywhere unless you’re reifying it as a map (so manipulating a json dom).
Iirc in rust the “complete” way to do this for a struct (as opposed to a map) with serde require two options and a bespoke deserializer.
It's much easier to do in dynamically typed languages or by using maps - agreed. Unfortunately we have other design decisions that force us into using structs for deserialization on this endpoint (part of our validation strategy.)
The Go answer is a struct that contains an IsDefined boolean (i.e. your first option), and a pointer-to-value (i.e. your second option.)
This is fine if you need it, but having to write this same logic over and over again for every type gets old... especially if your validators are tied to your types (i.e. a type per field.)
> Unfortunately we have other design decisions that force us into using structs for deserialization on this endpoint (part of our validation strategy.)
Oh I’m not blaming you, sorry if it came across that way. I’m aware of the issue because i’ve been hit by the exact same (hence having been made aware of the rust workaround), can’t say I was a happy camper.
> Is it a better bet to move on and go full Rust
If you don't like generics you shouldn't use Rust. You can't escape them in Rust. The designers repeated the mistake of C++ and made it a language feature kitchen sink. It's an unholy mess. You'll find yourself constantly fighting the borrow checker. Type signatures are littered with lifetime annotations. The type system is Turing complete because they didn't analyze it before implementing it. Go's generics were formally validated [1]. Rust's compile time is slow, and the 'async' story is sad. Async functions are colored and infect everything.
>You'll find yourself constantly fighting the borrow checker.
In the beginning, yes, this is true. But most people learn within a month or two which design patterns lead to problems with the borrow checker and which work smoothly, and often this knowledge translates to good design in languages like C and C++ as well.
If you're fighting the borrow checker in Rust, you'd probably have been fighting segfaults and use-after-free in C / C++. I'd rather spend 30 minutes fighting the borrow checker than spend 4 hours digging around in Valgrind.
> Type signatures are littered with lifetime annotations.
You cannot avoid the concept of lifetimes, without a garbage collector. If you don't want garbage collection, you have to deal with them.
Having explicit lifetime annotations in the code is _vastly_ better than trying to track the lifetimes in your head from scratch every time.
> If you're fighting the borrow checker in Rust, you'd probably have been fighting segfaults and use-after-free in C / C++.
That is in my ( admittedly limited) experience just not true. There's plenty of things that are perfectly safe that the borrow checker just doesn't understand.
The borrow checker can prove that a subset of things is safe. But the borrow checker being unable to prove something doesn't mean it's not safe.
This, one thousands times.
The borrow checker forces you to write in the very narrow subset of code paradigms it can understand. When it fails to compile, it doesn't mean it's wrong: it means that it can't prove that it's correct, which is a completely different statement.
Ah yeah, I am not sure everybody know me what “colored” means but I remember when comparing a C# solution using async with a Go solution using channels and Go routines I finally understood why people keep raving about concurrency in Go. It composed very nicely while any language following the popular async/await approach turns into a total mess. I guess async/await looked good years ago because we compared it with managing POSIX threads manually. That sucked.
It's worth noting that Rust's Async functionality starts with very different priorities from Go's goroutines. Rust, as a systems language, made lack of overhead (allocations, etc) the highest priority. It's part of Rust's overall zero-cost abstractions ethos. Goroutines are just never going to be zero cost. Go chose to prioritize the interface to the programmer, which is much more a part of Go's ethos around being simple.
There's nothing wrong with either approach, they just have different trade-offs because their goals are different. Rust's approach will sometimes push complexity onto the programmer to handle. But it can be made to perform better and more predictably than the Go equivalent. This might not matter if you're not pushing the performance envelope, but if you are, Rust makes that possible in a way that Go simply doesn't. You'd never want to write code using goroutines for an embedded device with limited CPU/memory, but Rust's async is already proving useful for these sorts of projects.
However if you can tolerate the performance overhead that Go imposes, giving the programmer a simpler mental model can easily be worthwhile. Technology is all about trade-offs and you have to choose the right tool for the job.
Async/await was intentionally chosen by a lot of languages well after Go had become popular. Rust once had Go-style concurrency and abandoned it in favor of its current model.
Paraphrasing Stroustrup, there are only two types of languages: those (that end up) with a Turing complete type system and those that nobody use.
A type system being turing complete really isn't a problem. You bound the recursion depth in practice, and the chance of a real world programming hitting that limit is minuscule. Lot's of other languages have turing complete type systems, subtyping and typeclasses lead towards it.
Sure, if you regularly want to increase your #![recursion_limit] or #![type_length_limit] for the next generation of type bloat.
Hmmm I guess it is all in the eye of the beholder. If you are the kind of person who thinks C++ went wrong with its template system, then you might find issue with any language emulating C++ failures.
If you think C++ is a beautiful well deigned language, then I am sure you will not have issues with Rust either.
Java & Haskell both have turing complete type systems as well. I'm not a particular fan of C++'s type system, doesn't mean have metaprogramming features is bad.
C++'s templates go too far — way further than Rust's generics go. Rust's type system may technically be Turning-complete, but nobody's actually doing serious* metaprogramming in it, unlike in C++, where template metaprogramming is a whole discipline unto itself.
* not just toy examples
Right, because Rust provides a real macro system for metaprogramming.
I'm concerned about two things (that I hope I'm wrong about): (1) developers will overuse generics and use poor types or `any` where not necessary, and (2) this marks the beginning of Go's convergence to yet another language with a million choices for abstractions and a million ways to misuse its features.
I think they did this really quite elegantly by extending the interfaces abstraction.
Personally I would discourage overuse of generics in an application codebase as I’d discourage overuse of interfaces, concurrency or channels - they have their place in certain areas (for generics e.g. collections, orms - mostly in library code) but most of the time simply aren’t required.
Not a Go programmer but I can say that Generics were important for me to consider adoption of Go. Now that they are present, it's a legitimate language. Whether they make code messy really depends on how they are being used.
It's been a legit language for years. Frankly I've enjoyed the lack of generics acting as a filter function for astronaut engineering.
I don't want to read through a heavily templated, generic code base when concrete, simple types will do. Go is so easy to read and reason about that I'm genuinely afraid of any change which could affect that.
The best advertisement for avoiding generics in Go despite their availability is the fact that they won't appear in the standard library for some time, and that the language maintainers believe it will take years to understand how to use them appropriately.
I'd wait until we see how the updated standard library looks like. In Java they had to invent Streams. Scala/Kotlin didn't repeat that mistake and so their collection APIs are more natural to work with. Historically golang has repeated most Java mistakes so I don't hold my breadth.
On the positive side, now Either/Try-like composable types are possible. So even if they insist on not having exceptions they could clean up error processing.
> Now that they are present, it's a legitimate language.
I second that sentiment, but I would have to look into the specifics. Generics ala Java aren't really that attractive when Rust mainstreams ML-style polymorphism with Haskell-style overloading.
What about `if err return err` everywhere?
I like them personally. It is easy to ignore them, and if you want them, they are there.
I share your worries, but I think it will be just fine. There are two reasons I have for that:
First, the Go community mainly comprises people who love simplicity and got accustomed to it. I imagine most people who want to go overboard with generics will stay with languages that let them go way more overboard.
Second, and more importantly, there's no method parameterization, which saves us from monadland.
In my opinion, there are plenty of good alternatives to Go such as Nim, or Python if performance allows, and even languages like Zig, V, CommonLisp, and D depending on the use case. I don't get why people keep mentioning Rust in threads about Go. It's perhaps a replacement for C, C++ and Ada - though for the latter only if you're okay with switching from self-documenting code to unreadable gibberish. Rust's philosophy is pretty much the opposite of that of Go, and it is neither designed nor suitable as a high productivity, easy to use language.
IMHO Go generics are simple and useful, particularly for container libraries. They are fairly readable, unlike template programming and macros in other languages. Together with the any type alias for interface{} they will make code more readable.
I do hope that Go stays at version 1, though, or that it at least takes a long time to add new substantial features and get version 2. Slow change is one of the many advantages of Go and I'd rather see them improve the compiler in hidden ways.
I'm not so sure it will be an overall win.
Will it be nice with generics for carefully crafted and cherry-picked use-cases? Absolutely.
Will it make the average large codebase less readable after a dozen coders have been doing their own cleverness with generics 3-5 years down the line? In my experience, most definitely.
Of those two, I'm much more afraid of working with the latter than missing out on the former.
It sometimes feel like we as devs have a habit of optimizing for the individual dev's convenience (not to mention what's fun) rather than for the collective effort's best interest - like the long-term maintainability/coherency of the code base. To some extent I assume that's just being human, but in other professions I think it would be seen as a bit lazy and thus more stigmatized.
having played with generics over the past few weeks: my guess is that there will be a period of widespread misuse, the first six months or so after 1.18 is released. So... between February and August of next year I'm expecting a lot of the Go discourse to be characterized by a very poor signal to noise ratio. I think there's virtually no chance that Go 1.18 will come out and people will use generics well; people will use generics poorly before they use them well.
A bunch of people will write libraries that utilize generics in some way that's not very orthogonal to the rest of the Go ecosystem. They'll put these libraries out as quickly as possible, because they want a first-mover advantage in picking up adoption. A handful of people will take their time and learn how to utilize generics in a way that is native to Go. Eventually those people come out of the woodwork, and it turns out that utilizing generics in Go looks different than it looked in some other language, so a lot of the early assumptions about how to use them were wrong. A bunch of those early libraries turned out to be badly designed once actually deployed into the real world, so they fade out of use as their problem domains get written by newer libraries that utilize generics in a manner more orthogonal to the rest of the Go ecosystem. A few of those libraries picked up significant market share, and maybe a startup has shipped stuff that generates revenue with those libraries, so they fund those projects, which continue to exist as a result of pure inertia. So now you have some big libraries written in like ... March of 2022, which are just bad, but people keep promoting them because they have a vested interest in doing so, but the ecosystem at large moves on, and by late 2022, generics will fit in very nicely with Go and not complicate things in an unnatural way. I'm wary of what's coming in the short term, but optimistic of what's coming in the long term as a result of this change.
I'm certainly not a fan of Go, but I suspect generics will result in the language and ecosystem getting better. I'm not personally a fan of the go generics syntax because I find braces and parentheses to be visually similar, but I'm sure that would go away if I used it more.
I'd go for Rust tbh. I think it's a much more coherent language.
Wrote Go code daily for quite some time, also tried Rust.
I like Go because of the tooling and simplicity of the language, it's easy to learn and explain.
Rust has the borrow checker has a corner stone. The concept mutability and references is easier to understand if you are coming from the c/c++ side of things
I use Go because it compiles quickly, the GC is reasonable, the ecosystem is good enough, and it's not rocket science (Scala/Rust/etc).
While generics moves Go in the direction of rocket science, it feels like this is solving a problem I always have.
> Is it a better bet to move on and go full Rust, rather than bother with wherever the goggle golang train is headed?
If you want to unnecessarily encumber your mind with trivial but tedious memory management puzzles, then choose Rust.
code might be harder to read but easier to interact with, because the type is more correct. That's my experience with typescript. Generics in typescript is used way too much comparing to other mainstream languages because it's way too powerful comparing to them and I doubt golang will ever go there. However, I think people focus too much on readability often in the sense that's code is a static thing. In reality, we interact with code and poke around and use type inference to discover them.
I hope that this quote from the article is true:
> I’m not sure that most Go developers will be using generics daily, but it’s nice to know that they exist if we need them.
Most people won't need generics in Go, and I hope people don't force them into their code where an interface would do just fine. I'm a big fan of good type systems like Rust, but Go doesn't need all that power all over the place. I think generics are a good feature for Go, but I really hope they don't get overused in places that would currently use an interface.
My hunch is that Go packages will still work fairly well for isolating gory details. The guts of a package can get more complicated with generics but there is friction for consuming anything more clever than, like, ‘lists.NewLinkedList[myType]’ outside; further, the path towards cleverer and gorier cross-package APIs mostly goes through interfaces. In this way, underneath some new syntax, the concepts employed when using polymorphic type parameters largely resemble the status quo for dynamic dispatch.
Every language feature gets misused in some way, but if it is overwhelmingly used to clarify and reduce code, I think it is a win. I believe generics will overwhelmingly be used to do this, so I think they are a win.
I doubt more complex features than this will be added, considering how long it took to get generics.
Rust is a very different language from Go, though it can do similar things. If you have people willing to learn it, you can certainly try, though you might find the ecosystem lacking.
Hum, I've never seen generics making code harder to reason about in any language, except, of course for C++, where they are hacked over text.
If they will make code harder to read, that's up to syntax. I don't know how all that will look up on the end, but it should be reasonably easy to just write an example.
> Hum, I've never seen generics making code harder to reason about in any language, except, of course for C++, where they are hacked over text.
do you mean templates?
i was under the impression that c++ generics == templates, but after a google search found out that c++ has both (at least according to microsoft), not surprised
https://docs.microsoft.com/en-us/cpp/extensions/generics-and...
C++/CLI is C++ compiled to NET CLR IL, so it has all the features of the regular C++ compiler (templates) AND all the features of the CLR (generics).
It is an extremely niche language, extremely rarely used even in the .NET ecosystem, except sometimes as glue code.
Normal C++, including MSVC C++, has only templates.
It is definitely nicer than trying to correctly get P/Invoke declarations or debug COM marshaling issues.
This is the kind of tooling that makes me still reach out for C++ when going outside managed languages.
My experience was that P/Invoke is much easier, but of course YMMV. COM I never played around with.
I also found a pretty ugly bug in the C++/CLR compiler - if you used in-place initialization for an array (something like auto arr = Object[]{obj1}), it would allocate an array of length 0xC0FFEE and set the elements you specified. They acknowledged the bug but said they will only fix it in a future version of the language.
This told me all I needed to know about how popular it actually was.
It is popular enough for being one of the major milestones on .NET Core 3.1.
P/Invoke can only do so much if a C++ library doesn't provide a C ABI, and WinDev loves to publish COM based APIs since they won the Longhorn dispute.
Yes, generics in C++ are created with templates.
I have never felt the need for generics but many of my friends complains about the lack of it in Go. The syntax seems to be as readable as any other lang with it.
Go is fairly dominant in some areas and Rust is not really a realistic replacement today. Generics will not change that.
>I kind of hate looking at Go code containing generics.
Perhaps that will change as you get more used to it?
And for code you consume (e.g. using a library) Generics will make your life easier and safer.
> I kind of hate looking at Go code containing generics.
Pretty much any mainstream language that supports generics is shit to read. (And I love to read code.)
While generics are definitely useful to implement container types, it seems that the Go design seems to prevent that. It seems to me that generics get overused when the language allows you operator overloading for example. Go is a "one way to do thing X" language, it doesn't give much room to write "creative" code
> What previously was elegant and easy on the eyes is now annoying and muddled by the additional layer of abstraction
You're just getting old is all.
What was once familiar now isn't ... how annoying.
If that worries you, don't: we're all walking down that path.
Give it a couple of decades and everything in your life will feel like that.
>My first response when the plan to add generics was announced was “meh”. In my 5+ years working in Go, I can probably count on one hand the number of times that I felt like I really needed generics.
And then the author goes to admit that they had written a whole library with the kludge that is textual code generation "to support both primitive and custom types".
Something I don’t often see mentioned in these discussions about generics: generics as a feature is massively important for library authors, not so much for library users. So of course if you’re mostly spending your time writing business logic and web APIs you don’t encounter the need for generics that often. But when you try to write for example a library for a data structure while keeping some type safety (so not relying on interface{}), you absolutely need generics.
Amusing that this post goes from
> My first response when the plan to add generics was announced was “meh”. In my 5+ years working in Go, I can probably count on one hand the number of times that I felt like I really needed generics. Most of the code I write in my day job is very specific to the domain and doesn’t fit the use case that generics aim to fill.
to
> I love that I was able to delete 95% of my code because of generics.
It's interesting because I remember all the early discussions against generics in Go centred around "what Real World scenario do you need it for?"
An argument against generics was that people found it hard to find examples that were 'real' where generics would be beneficial, and so because it was rarely needed the question of whether the language should be drastically bodged/ruined/adjusted for this feature was called into question.
In retrospect you had a self-selecting population of people who loved Go and presumably didn't have much use for generics, whereas people who did presumably used something else.
I guess all we can learn from this is that human imagination is poor, and many of us need the thing in our hand to work out what we can do with it.
That's why you need some breadth of experience with many different languages, folks. That's exactly why you don't limit yourself to a single (extremely simplified to the point of stupidity) language.
That's also why one should take a look at back and think if it is smart to uncritically said generics were just dumb.
Same with people who unconditionally recommended WhatsApp not that long ago.
Or people like me who told everyone Google was still nice and a driving force for good until a few years ago ( yes, I still have some hope that they will change their ways and don't think others are much better but I am somewhat bitter and I don't give them the benefit of doubt anymore :-| )
Another way to do `Option` without pointers could be similar to the following with a struct with two members.
type Option[T any] struct {
v T
isSet bool
}
func NewOption[T any](v T) Option[T] {
return Option[T]{
v: v,
isSet: true,
}
}
func (o Option[T]) Get() (v T) {
if !o.isSet {
return v
}
return o.v
}
func (o Option[T]) IsSet() bool { return o.isSet }
With this pattern you're able to use `Option` as a value without pointers. var o Option[int32]
o = NewOption(int32(1))
fmt.Println("value:", o.Get())
fmt.Println("is set:", o.IsSet())
Alternative separate `Get` and `IsSet` methods, is to combine them into one, similar to map look up pattern. func (o Option[T]) Get() (v T, isSet bool) {
if !o.isSet {
return v, false
}
return o.v, true
}
var o Options[int32]
v, ok := o.Get() // zero, false
o = NewOption(int32(1))
v, ok = o.Get() // 1, trueI agree with `Get` returning `(T, bool)` I don't see why one would want to return an `error`.
I don't understand your example, `isSet` is always true and can never be false. Missed something?
It works because it's false when uninitialized (as default value). So if not initialized it represents Nothing value. When it is initialized it's Just T.
The only time `IsSet` would be false is when `NewOption` was not used to initialize the value.
e.g.
or could have `None` helpervar o Option[int32]func None[T any]() Option[T] { return Option[T]{} } o := None[int32]()
I think you're making it easy on yourself by choosing "a library whose sole purpose is to stamp out the boilerplate needed to work around Go's lack of generic algebraic data types" to demonstrate how good generics are :P
haha for sure. but still better than code generation IMO ;)
I too was playing around with Go generics. I wrote some naive concurrent filter and fold (reduce) functions for slices and maps here https://github.com/unix1/gostdx if anyone is curious how those would feel.
Author here. I just removed the previous section I had around using build tags because I realized I was using `// +build 1.18` instead of the correct `// +build go1.18`. Oops.
Shame build tags aren't part of the language with a proper syntax check instead of magic comments.
Can you show me an example of a language that does syntax checking of build-system related directives at compile time?
C++ #pragma, #include? Golang import statements?
I see why they did this for the Go 1 guarantee, but would prefer if they used a keyword and had a defined restricted syntax for it. There are a growing number of these comments and they’re poorly documented and spread around different tooling. It’s kind of a hack.
Zig. Rust. The others in this thread.
There are quite a few. Go chose possibly the weakest-safety option of them all.
Nim. C++ attributes (coming soon to C).
FYI: Looks like code blocks are unreadable on mobile (probably a overflow: hidden CSS rule somewhere that truncates lines and doesn’t let you scroll).
Just fixed it. Thanks again!
thanks! I'll check it out!
Side question: Are there any languages that transpile to Go?
There is python to go transpiler from google.
My opinion with 9+ years since first learning Go, multiple of those using it for a full time job:
Putting the end first, my rule of thumb for using generics in Go is: Don't go down the OOP road of over planning and programming with fancy type work. 99% of the time, the common Go programmer won't need to write any generics. Instead, just focus on actually solving the problem and manipulating the data like you would normally. If you encounter a place where code is repeated and complicated enough to be worth a new function, move it to one. If you find yourself repeating multiple functions but with different data types, turn that into one generic function.
Generics are an incredibly useful addition to the language that I'll almost never use. Really to be more precise, Go has had some generics this whole time: Maps, slices, arrays, and channels all have type parameters, and have covered the vast majority of my needs. There are a few times where I've wanted more general generics, though:
- The sort and heap packages are rough to use. You need to specify a bunch of nearly identical functions just to get them to work on any custom type. The generic versions (not coming in 1.8's standard library, iirc) will be much easier to use.
- Was writing an Entity-Component-System game for fun, and needed a weird custom container. Turned to code generation, and really that turned out to be necessary anyways because it did more than any (non-metaprogramming) generics could do.
- We had one very complicated multiple Go routine concurrent data structure that needed to be used for exactly 2 different types. Others were writing the code, and very afraid of using interface{}. This is despite there only being a handful of casts. In reality if they caused a bug, it would be found immediately. There's a strong hesitation around type safety dogma that isn't risky in practice. Still, generics would've been the preference here.
- I was parsing WASM files, and there's a common pattern for arrays where it encodes the length of the array, then that many objects in a row. It led to a lot of minor code repetition. Replacing that with a generic function that took a function to parse a single object, and returned the array of those objects was a nice, but relatively minor win.
On the other hand:
I've never really been bothered by having to do sets like map[int]struct{}. There was one case where I saw someone put set operations out into a different library. I eventually found to my dismay that the combination of how the set library was used, and how it was implemented caused a performance critical part of the code to be several orders of magnitude slower than it needed to be. Had this code been more idiomatically inlined, this flaw would have been more immediately obvious.
I really don't like seeing map/reduce/filter type functional programming coming into Go usage. This type of code tends to need more dramatic changes due to minor conceptual changes, more than direct procedural code does. Also like the set example, how you iterate and manipulate objects can have large performance implications that using such functions hides away.
That’s a welcome feature even though I don’t like the syntax. But Go could become the perfect language if they just fixed error handling and a couple small annoying quirks.
I've never seen a person who writes Go for more than a few weeks complain about the error handling. I certainly don't mind it myself. Is it really a problem people have or just somewhat of a meme at this point?
Hi, I'm one of these people.
I used Go for about six months and eventually abandoned it to pursue Rust, a decision I've been extremely satisfied with. The longer I used Go, the more I grew to hate it and error handling was one component of that.
Well over half of Go source code in practice is dealing with errors, and somehow the Go ecosystem has convinced themselves that "verbose" is the same as "explicit" when it doesn't need to be. The worst problem isn't that it's just a lot of excess code, it's that it makes all sorts of very simple and common programming tasks ridiculously unwieldy. The most obvious example is calling a fallible method, doing something to the result, and returning it (or the error). This is one single character in Rust but a minimum of four lines—with branching—of copy-pasted boilerplate in Go. Which isn't a lot in the abstract, but then you multiply that by hundreds of times and now I have read, lex, parse, and mentally discard the majority of pages of source code that's doing something that could be done in ten lines with a massive incerase of clarity in a more reasonable language.
You've probably "never seen" us because we felt very let down by the overpromise and underdelivery of go and we left.
> This is one single character
So you're bubbling up the error without annotating it... great
There's so much wrong in just this once sentence it's going to take a surprising amount of text to cover it all.
First, if you want to add extra annotations or scope to the error, you can actually do so—and trivially—while still using that single `?`. Widely-used error crates like `thiserror` allow you to specify that (for example) an I/O error will be automatically wrapped with `?` by some custom error type specific to your crate that conveys more information about what went wrong. This is phenomenal for errors that need to be bubbled up to end-users.
Second, for the majority of errors that are normal, expected, and recoverable, annotating them is just pointless busywork since they'll never be visible from outside of your program. For example, errors that eventually bubble up to an `.ok_or(...)` receive zero benefit from being annotated.
Third, is your preferred alternative the Go approach where you function as a less-capable human exception handler? Having to hunt through the source to identify what actually happened through some contortionist `error: thing went wrong: subsystem died: api client failed: gcloud client: cache error: filesystem error: file not found: tmp.VRVcBX1j` with no line numbers or function names, and various random components of the error string coming from either third-party libraries or the golang standard library? This is just so comically terrible to anyone who's spent time in languages with decent error handling it's genuinely hard to believe that people regularly come to its defense.
But of course I'm being generous here when we both know the actual status quo in the overwhelming majority of production Go projects is to simply bubble up the error with `return nil, err` with no context whatsoever, so you just get `error: file not found: tmp.VRVcBX1j` with absolutely no idea of where it came from. Those are always my favorite.
So, to recap: with Rust's `?` operator you actually can have your cake and eat it too. You can add library-specific context to your errors while actually wrapping the underlying error and not merely mashing strings together. You can opt into stack traces for your own code if you want to. And you can skip the annotations for code where you handle errors and don't bubble them up. The only apparent downside is that it's not overly verbose enough for Go adherents.
My biggest complaint about go error handling is that it's impossible to enumerate all of the errors that a function has returned. I have a use case to translate these into user facing errors for external use and find it a nightmare to enumerate them all.
What do you mean by "enumerate?" The error interface has exactly one member of type string. If a function is returning an error to you, it's specifying its own human readable error message. What's wrong with log.Fatal(err)?
I'll offer a comparison to Rust here to contrast.
In Rust, errors are generally either a struct (to represent a single possible kind of error) or an enum of structs (to represent multiple potential underlying errors). These aren't C-style enums, they're sum types. So if your function returns a Result with an Error in it, that error is precisely one of those underlying struct types.
This has some enormous advantages. If the library author provided a way to convert their errors to a human-readable string, you can simply call that method and do so (similar to go's error interface). If they didn't or you would prefer to use your own string descriptions, the enumeration provides the complete list of possible error types to the compiler. So you can match (a.k.a. case or switch) on the error type and convert them to strings of your own choosing and guarantee statically at compile time that every possible error type is handled. You can use this same machinery to detect the error type and recover from ones you know how to handle or bubble up the ones you can't.
This is much more powerful and flexible than the Go equivalent and doesn't exactly come with much additional mental burden. With Go, the only thing you're promised is that your error type can be turned into a string, that's it. You can check that the errors are of a certain type, but there is no way to know at compile-time what all possible errors are. In fact, because most Go programs just use strings as errors directly (`fmt.Errorf("bad thing happened")`), the only types of errors you can generally detect and recover safely from are ones that happen in functions that can fail in precisely one way or functions that have only one possible way to recover from all their failure modes.
Of course, Go programs could implement error structs that you can switch on. But nobody in practice seems to do this. And even if they did, there are no compile-time guarantees that ensure you're covering every unique failure case. If the function you're calling adds a new error type, there's no way to know this other than to have an `else` that covers "everything I didn't know about".
Let's take one toy example: creating a file. This could fail because the directory doesn't exist. This could fail because we don't have permissions to write to the directory. In both of these cases maybe we want to fall back to an alternate location. Or it could fail because of something unrecoverable: your disk is full. In Rust, this is trivial to do. You can match on the error type, handle ErrorKind::NotADirectory, ErrorKind::NotFound, etc., and fallback. For anything else, bubble the error up. In Go, you get an error back. The docs promise that it's of type *PathError, but that isn't enforced by the compiler so you get to typeswitch. Even then that doesn't really help you much because fs.PathError is just
All you get about that internal error is that it's convertible to a string. So after typeswitching, now you could theoretically switch on `error.Err.String()` to do this but now you need to figure out every possible string that is returned for the error cases you want to handle. And of course, those strings could change in future updates or new ones could be added without you ever knowing.type PathError struct { Op string Path string Err error }A PathError can be identified using errors.Is as one of e.g. ErrNotExist or ErrPermission.
https://go.dev/src/os/error.go
You're absolutely not supposed to match the error string as it varies with the underlying operating system and locale.
Go developers aren't happy unless they are taking 10 lines of code and turning it into 150.
I like Go. It's useful for the things I need it for since it compiles fast into a single binary and has networking utilities in its standard library. I was used to Rust's error handling when I started, but I liked how simple Go's design was in comparison, so I stuck with it to get a proper feel for the language.
After a while, I tried using the Goland IDE, and its static analysis tool found a dozen places where I wasn't handling errors correctly: I was calling functions that return errors (such as `io.ReadCloser.Close` or `http.ResponseWriter.Write`) without assigning their results to variables, so any errors produced by them would simply be ignored. My code was compiler-error-free, go-vet warning free, and still, I was shipping buggy code.
A few months later, I try using the golangci-lint suite of linters, and again, it found even more places where I wasn't handling errors correctly: I was assigning to `err` and then, later, re-assigning to `err` without checking if there was an error in between. My code was still compiler-error-free, go-vet warning free, and now IDE-warning free — and I was still shipping buggy code.
I don't see how anyone can see this as anything other than a big ugly wart on the face of the language. It's not because it's repetitive, it's because it's fragile. Even with code I was looking at and editing regularly, it was far too easy to get wrong. I'm going to continue using Go because it still fits my purposes well, but I'm only running it on my servers, so any mistakes I make are on my head, rather than on anybody else's.
I also don't think Go's design is really amenable to things like the Option and Result types people are writing — yes, I would never have had these problems in Rust, but code written using them in Go is clunky and looks out-of-place and doesn't feel like it's the right thing to write. I wouldn't ever use the `Optional` type in the article. But it's definitely not a solution in search of a problem. There's a huge problem.
So wait, go has handle errors by returning them, but it also doesn't force you to actually handle all return values? I thought that was the entire point of implementing error handling like that.
How are we still repeating the same mistakes C made 50 years ago?
You're sort of forced to handle them, in that if a function returns (Data, error), you need to assign the error to a variable (or do data, _ := func(), but at least that indicates you're intentionally ignoring the error), and since Go treats unused variables as a compile failure, you might as well check them properly. But there's nothing that says you must check them, no.
Ah thanks, that's kind of what I thought. I've read examples of Go but haven't written a line of it myself. the way GP described it made it sound like foo, err = bar() was merely a convention and you could do foo = bar() and drop err. If you have to add ", _" that's fine.
> I don't see how anyone can see this as anything other than a big ugly wart on the face of the language.
Would you be satisfied if the compiler forced you to check error returns?
I'd be really happy with that! Building the functionality of errcheck[1] and ineffassign[2] into the compiler — or at the very least, into govet — would go a long way to allay my worries with Go.
I think the reason they don't do this is that it's a slight (albeit a very tiny one) against Go's philosophy of errors being values, just like any other. While the `error` type is standard and used throughout Go source code, it still just has a simple three-line definition[3] and is not treated as a special case anywhere else; there is nothing stopping you from returning your own error type if you wish. A third-party linter could simply check for the `error` type specifically, but the first-party tools should not, and there's nothing like Rust's `#[must_use]` attribute that could be used instead. I respect Go's philosophy, but I feel like pragmatism must win in this case.
[1]: https://github.com/kisielk/errcheck [2]: https://github.com/gordonklaus/ineffassign [3]: https://pkg.go.dev/builtin#error
There's a proposal for the Go 2 draft that addresses this:
https://github.com/golang/go/issues/20803
https://go.googlesource.com/proposal/+/master/design/go2draf...
Why should we have to waste our time doing something the machine can do? I normally only care about handling errors in 5% of places. The rest of the time it's just returning them. Life's too short
It might also be self-selection that people that truly dislike the error handling simply avoid golang. I’d really be interested to see how well go generics handle the Result type.
They probably stop complaining after a few weeks because there's no use. It is what it is.
meme. its like the least annoying thing I deal with on a daily bases when programming. oh no... I have to handle an error....
I like the go error handling
Can you elaborate on "fix[ing] error handling" and what some of those small annoying quirks might be? I've got my own annoyances, but am always interested in what other people think.
What would you like for error handling?
See Swift for a much more reasonable way to do error handling https://docs.swift.org/swift-book/LanguageGuide/ErrorHandlin...
Personally I think the documentation should enumerate all the error types returned by functions.
Either[E error,A any]
I see they went with []. Scala does this and it annoys me because [] also denotes arrays. Unlike <> which doesn't have any overloaded meaning.
They wrote about the syntax a while back, one reason they avoided <> is that they lack parse-time type information to properly differentiate between certain cases like:
There are two valid parsings of that if generics use <> and at parse time it isn't known which one to use. Either:a,b := w < x, y > (z)
ora, b := [boolean expression w < x], [boolean expression y > (z)]
https://groups.google.com/g/golang-nuts/c/7t-Q2vt60J8a, b := [generic function w] [with parameters <x,y>] [applied to (z)]
It could be my personal negative experience with maintaining code that overuses generics in other languages, but I have reservations about this feature. I almost never need them, but on the other hand I don't feel too good about having to repeat myself when writing library packages.
I almost feel I would be happy with generics in Go if Go made them illegal in anything but libraries (not allowed in package main, maybe? Or not allowed in a package unless it gets imported by another package?).
Yes generics / templates are mostly useful for general libraries. But if you afraid of programmers doing stupid things just for the fuck of it it is better not to hire such programmers, warn them if they're juniors or just hit them with the bat on code review.
That could be said about any language though? Yet here we are, arguing whether generics are good for Go, instead of just using Java.
>"That could be said about any language though"
Correct. Go is not special in this regard
How does Go handle the ambiguity between [] meaning generics and [] also meaning array?
You can use optional(?) parenthesis to make it extra clear.
[]MyContainer[T] // slice of generic struct or interface
can/must be written as
[](MyContainer[T])
but ([]MyContainer)[T] isn't a valid use of generics anyways.
What about accesses? Seems like this would require at least some context in the parser. And what about the human parser? Do you get confused between arrays, array accesses, templates? Or do you get used to it?
T int = 5 myarray[T]'T' is a completely normal identifier, it is merely the conventional one used as a type parameter. But you can also use Type, MyType, etc instead of T as that parameter identifier.
The compiler can easily detect if the thing in the [] is a int or type.
In most cases there is no parsing ambiguity. In the cases where there are, you need to use parenthesis to clarify.
person deleted 95% of code, but what about performance? is it more or less the same as the non-generic implementation?
In theory yes, because generic types are monomorphised, so I would expect performance to be no less bad than implementing each individually.
This is a phenomenal achievement, that I didn't expect to see in my lifetime!
Gives me hope that P vs NP will be resolved in my lifetime too!!
>"In case you’ve been living under a rock ..."
Not really but getting to know Go is not on the list of my priorities.
Looked at examples. Many languages use angle brackets for generics and templates but in case of Go they had to do it their own way and use square brackets that most programmers would perceive as an array. Funny.
Which isn't unprecedented, and lots of very widely used languages use other brackets for array literals or other array literal syntax. Even in languages which use brackets that way, they're often overloaded (see both TypeScript and JavaScript, but also JSDoc type annotations).
scala uses square braces. I don't even use scala and I like it, not having to hold shift is a win and language designers should think about how a programming language is typed because it's literally 50% of the reasons why a syntax is the way it is.