Trying Out Generics in Go(markphelps.me) |
Trying Out Generics in Go(markphelps.me) |
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.
While you can't do this:
type Option[T any] struct{}
func (o Option[T]) Map[U any](f func(a T) U) Option[U] { ... }
You can do this: type Option[T,U any] struct{}
func (o Option[T,U]) Map(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/2w2y1KEjXVEI 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
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?
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:
fn Map<U: any>(self: Option<T>, f: fn(a: T) -> U) -> Option<U> { ... }
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.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
public class JavaOption<T> {
public <U> JavaOption<U> map(Function<T, U> func) {
//todo
}
}
Kotlin might be a closer match in semantics if I use an extension function: fun <T, U> Optional<T>.map(func: (T) -> U): Optional<U> {
// todo
}At the max, one can make a couple of type-aliases for a bit more legibility, but that's all one can squeeze.
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 behavior
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.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...?
// 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) { ... }
In the second case, "Foo" is a method which has a "bar" instance as a receiver.I've edited my original post to make it a bit clearer.
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.
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".
> 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.
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.
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 :-| )
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, truee.g.
var o Option[int32]
or could have `None` helper func None[T any]() Option[T] { return Option[T]{} }
o := None[int32]()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.
a,b := w < x, y > (z)
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 := [boolean expression w < x], [boolean expression y > (z)]
or a, b := [generic function w] [with parameters <x,y>] [applied to (z)]
https://groups.google.com/g/golang-nuts/c/7t-Q2vt60J8I 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?).
[]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.
T int = 5
myarray[T]Gives me hope that P vs NP will be resolved in my lifetime too!!
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.
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.
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.
Tell me you’ve never used Go without telling me you’ve never used Go.
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.
It's that "correct by accident" part that's hard to have both safe and performant without generics.
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.
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.
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)
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.
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.
> 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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'd go for Rust tbh. I think it's a much more coherent language.
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
While generics moves Go in the direction of rocket science, it feels like this is solving a problem I always have.
If you want to unnecessarily encumber your mind with trivial but tedious memory management puzzles, then choose Rust.
> 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.
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.
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.
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...
Go is fairly dominant in some areas and Rust is not really a realistic replacement today. Generics will not change that.
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.
Pretty much any mainstream language that supports generics is shit to read. (And I love to read code.)
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.
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.
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.
The compiler can easily detect if the thing in the [] is a int or type.
func (o IntToStringOption) Map(f func(a int) string) IntToStringOption { ... }
// ... but that has to be repeated dozens of times
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.Correct. Go is not special in this regard
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.
There are quite a few. Go chose possibly the weakest-safety option of them all.
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.
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.
That's why languages like JS/TS, Haskell, Elixir, OCaml (which is way more modern than Go), ... exist and are used.
If only its ecosystem was more platform agnostic.
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.)
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.
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.
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.
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 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.
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.
At most I have used non-empty interfaces to solve very few number of issues (countable on one hand). I have never needed interface{}.
If you think C++ is a beautiful well deigned language, then I am sure you will not have issues with Rust either.
* not just toy examples
Concerning resulting API: isn't code generation solves a problem with interface objects and downcasting?
If you continue to disagree with me, please be specific about what operation Go permits on interface{} values that you consider type-unsafe.
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.
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.
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.
This is the kind of tooling that makes me still reach out for C++ when going outside managed languages.
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.
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.
What on earth does dealing with untrusted inputs have to do with anything?
> 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".
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.
So you're bubbling up the error without annotating it... great
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.
type X[T any] struct{}
func (x *X[T]) Method() {}
just fine. What you can't do is func (x *X[T]) Method[T2 any]() {}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..
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.
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.
How are we still repeating the same mistakes C made 50 years ago?
Would you be satisfied if the compiler forced you to check error returns?
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
https://github.com/golang/go/issues/20803
https://go.googlesource.com/proposal/+/master/design/go2draf...
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).
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).
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).
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.
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.
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.
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.
From https://itnext.io/generics-in-golang-1-18-no-need-to-write-m...
Someone should port OCaml to the Go runtime with a good high-level FFI. It could really give the community a boost.
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.
Certainly better than C++ or C# IMHO, and even my Python colleagues were amazed how easy it was to work with.
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
type PathError struct {
Op string
Path string
Err error
}
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.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.
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.
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 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.
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 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 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?
get_data() { filename=default ... }
=becomes=>
get_data(filename=default) { ... }
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): fn nth(seq: [int], n: int) -> int
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<T>(seq: [T], n: int) -> T
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?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 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.
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.
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?
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.
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 value maintenability (type safety helps prevent unintended consequences) and readability (Haskell and Clojure are out).
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?
Otoh, I do know that languages like OCaml, Haskell, or Rust take the burden of trivial errors from my shoulders for neglible cost.
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.
> 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.
Odd that you’re so upset by other people’s choices.
> 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.
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.
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.