Hacker News new | past | comments | ask | show | jobs | submit login
Go 1.18 Beta 1 is available, with generics (go.dev)
216 points by pcw888 on Dec 15, 2021 | hide | past | favorite | 162 comments



Go with generics has definitely surpassed Python for me. Python is good for prototyping but the package management story and the "feel" of the language has ruined it for me. I used to love Python, until I used Django professionally, and the façade crumbled. I'm done with OOP and dynamic typing.

Go felt good, a bit verbose, but my issue was that map/reduce/filter are a much better abstraction over imperative loops, and with generics, the dream of Go with more functional and high-level concepts is closer.

This comment is going to be very controversial, so let me remind you this is my very own opinion.


Note that you still shouldn't use Go as a functional language (using map/reduce/filter); it's not optimized for functional programming, and the syntax would be pretty gnarly.

A for-loop has what's called "mechanical sympathy", and will go through things in the order that it is in memory. It'll allow the compiler and runtime to manage things quickly through memory, CPU registers, and CPU pipelines.

A great post on the subject is "Why Go Getting Generics Will Not Change Idiomatic Go": http://www.jerf.org/iri/post/2955


The bit about "mechanical sympathy" is wrong, though. Plenty of languages inline map/reduce or iterator type code down to very efficient cache-friendly traversal of memory, most notably Rust.

Go's compiler is just particularly naive.


Actually I think while the compiler is a bit naive, the compiler writers are not. They are striving for simplicity, maintainability, speed, etc., vs trying to optimize high-level FP-esque idioms into fast machine code.

The compiler has definitely gotten more optimal over the years, but it's a different beastie than the rust one. Because go is not rust.


It should be noted, though, that rustc does not really have any particular tricks up its sleeve to optimize functional patterns, or indeed much of anything. A lot of the LLVM IR that rustc generates is astonishingly naive and verbose – largely by design! rustc totally depends on LLVM's optimization passes to throw out all the boilerplate and output lean and mean optimized machine code.


Right but that just means it's LLVM being clever, not rustc.

The main compiler is the self-hosting gc go compiler. Iirc ~~it's the reference implementation~~ erm I guess it's determined by spec, not reference implemenation. Gc is definitely "the default" though.

Then There's the gofrontend, which is a compiler frontend to other compilers, which can work with gcc and llvm. I am not aware of the state of the art for these, but I wonder if there are performance differences.


Go is not Rust, but there is a lot it could learn from Rust without sacrifice: sum types, Result for error handling, iterators with for loop support, [edit] non-nullable types etc.


I've wanted a Result type I could map over in Go as soon as I saw the idea. But someone recently pointed out, without pattern matching and early return, it's kind of nerfed. Not sure I totally agree (I think it'd still be awesome to have Result, if only for .map()), but I see their point.

I disagree that it would be without sacrifice. That's kind of the whole point.

Unless I'm missing something, go has iterators with for loops.


Go switches with exhaustion checks like in [1] would go a long way. An early return construct would be important as well, though, you're right.

Go's for loops can only iterate over builtin types: slices, maps, channels, etc. You can't produce a custom type that is to be iterated over.

1. https://github.com/BurntSushi/go-sumtype


Not as simple as it seems. For instance, what is the zero value of a sum type? Let's say, the zero value of a Result<int, error>?


Perfect example of a caveat to "without sacrifice". For context, go really cares about having "zero values" - every initialized variable of some type must have a zero value.

The zero of Error is nil, so logically this would be Result(0, nil). But what do you set the internal state? It's not Ok, cause that int isn't used, and it's not Err either.

Option would work fine though and it would be amazing to map over options instead of nil checks.

I think you'd need to build Result on top of Option, it's naturally kind of ternary - "I have value" "I have error" "idk", either internally or externally.

What might be awesome would be if the Error were a slice type, nil would be "uninitialized" and empty slice means "initialized". still not...pleasant.


Zero values are definitely an issue, and it'd be hard to get rid of them while sticking to Go's imperative/statement-based style. Still, it'd nice to use an Option type instead of pointers.


IIRC in Rust it has to be either of the Ok or Err variants and the enclosed type can then provide its own "zero value" via the standard library "Default" [0] trait or your own implementation for the particular type. That trait is also implemented not for "Result" but for "Option" giving it a default value of "None".

[0]: https://doc.rust-lang.org/std/default/trait.Default.html


Yes, for Option the default value is quite obvious. I'm talking about Result, because it's the second best-known sum type, and it doesn't have an obvious default value.


Bindings in Rust have to initialized with an explicit value if the first use comes before the first modification/assignment otherwise [0] you get an error message with error E0381 [1]. Even if that explicit assignment is just through the use of the type's default method [2], if available.

Since it's implemented for Option<T> you can do it the same way [3] for that, but that's not available for Result<T,U> since the semantics are different from Option which is basically just Rust's alternative for 'null'/'nil' (but verifiable by the compiler).

Thus you'd have to choose what variant of the enum you want to have at initialization, after which you can use the Default implementation of the enclosed type, if available (and so on, if there are more nested types).

Though honestly I can't recall using the kind of pattern in Rust that would "need" something like that. Through the handling of code blocks as expressions, early returns etc. you for example don't need to initialize a variable before having an if-/match-/for-/while-/etc. block, you can simply use that block as an expression on the right side of the assignment [4].

[0]: https://play.rust-lang.org/?version=stable&mode=debug&editio...

[1]: https://doc.rust-lang.org/stable/error-index.html#E0381

[2]: https://play.rust-lang.org/?version=stable&mode=debug&editio...

[3]: https://play.rust-lang.org/?version=stable&mode=debug&editio...

[4]: https://play.rust-lang.org/?version=stable&mode=debug&editio...


In Go, every type has a sensible built-in zero value. In Rust there is no need to worry about what the 'default' value of a Result type should be. So choosing a sensible default value for result would be a problem in Go (but not in Rust).


The zero value of a sum type can be the zero value of the first (or chosen) constituent. In this case, it could be Result<0>.


I'm not sure that would be any better, in practice, than doing return 0, nil.


It would allow functions to be treated generically as returning error-able types. Since some functions even return 3 arguments where the third is the error, this isn't possible today.

It would also allow defer-like syntax to be used on the value a function returns, to return early from the calling function. Rust's ? is an excellent example of this, allowing explicit delegation of error handling that is still very lightweight syntactically.


Function composition becomes a lot easier with Result/Option, because you can write functions in terms of assuming a "good" value (not nil/err or otherwise "bad") and then map over it. Or if we really wanna be fancy, flatmap/bind over it.

Foo, err requires either that all receiving functions accept (foo, err), or manually nil-checking at every stage.

Try/catch is the least composable, because deeper error types can "jump out" from any layer. Which means you have to keep wrapping catches, until you hit some base layer you can recover from. You get stack traces, but you lose type information along the way.

Sum (result) and product (foo, nil) types preserve type information. Meaning you can operate more generically over it. Meaning less code duplication.


Speed and maintainability vs trying to optimize high-level FP-esque idioms into fast machine code is a false dichotomy. OCaml has a really fast compiler, it has been maintained by a small team for 25 years, and it produces fast machine code. A big caveat in comparing OCaml and Go would be multicore support, but it's coming in OCaml, without breaking existing programs and with only a few percent of performance lost.


> Plenty of languages inline map/reduce or iterator type code down to very efficient cache-friendly traversal of memory

Very few languages outside of Rust and Haskell actually do this reliably and that's including languages like Ocaml and F#. Rust and Haskell are certainly the only even remotely popular languages that do it.


Does Haskell actually succeed here?

This is perhaps not the right way to measure, but: When I last checked the programming language benchmark game [0], the Haskell submissions were basically as fast as C/C++/Rust. But, this came at the cost of writing non-idiomatically/non-functionally – essentially abusing the language to write C in Haskell. I would be curious how well "idiomatic" Haskell compares to C++/Rust, in terms of the compiler optimizing away the functional abstractions.

[0] https://benchmarksgame-team.pages.debian.net/benchmarksgame/...



Thanks, that's pretty convincing.


I think OCaml can do it with flamba, at least that's what this library readme says: https://github.com/c-cube/iter

> with flambda under sufficiently strong optimization flags, such compositions of operators should be compiled to an actual loop with no overhead!


Does the elephant-in-the-room of C++ do it? I thought it does.

I can't bet on what Java and JavaScript JITs make of map / forEach / filter / reduce, but I suspect that they can be pretty efficient, given the clear delineation and the repeated patterns.


(The key phrase is “stream fusion”.)


I love python, and I don't blame you. I can't stand the Django way of doing things. Everything is so opinionated, dynamic, and magical in the bad way. Pytest has a similar vibe and it drives me nuts. Not a fan of OOP, either, I much prefer immutable objects that are either pure data, or convenience methods that return a copy. No more setters, state mutation methods, setattr, unless it's deeply abstracted away.

Nowadays I use Fastapi, pydantic, Result, and static typing everywhere. Protocol interfaces are great.


This!!! I’ve done a fair bit of python api development mostly for ML services. We used flask/gunicorn and I’m considering moving to fastapi. I’m sure Django is awesome for what it does well but python has a lot more options.

Fwiw - I do dabble in Go. I’ve used it for some very targeted systems work where I don’t have to install a python interpreter on the target machine and hacking some k8s operators. I really like Go - but the ML ecosystem is pretty heavily python.


> We used flask/gunicorn and I’m considering moving to fastapi.

Do it, do it nao! And by that, I mean at your earliest convenience. It's such a step up from Flask. All the serde is handled for you via Pydantic. No more manually vetting data. Just write constraints.


> Package management

Are you seriously saying that github.com/somelib is a better package management? Of course nobody forces to use that, but it's what actually been used in most go projects. Even NPM looks better in comparison.

The best quality package management I've seen is Maven, followed by Cargo.


Go has had built-in package management for a while now. It's extremely fast and generates reproducible builds effortlessly in its default behavior in a way I've not experienced before from any other tool.


As somebody currently working on something that has a mix of Node and Go, I definitely prefer github.com/somelib. In that case there's no worry about package name conflicts (everybody has equally terrible names), so you can always end up doing imports with reasonable names because only the last bit matters. With a single namespace, people can (and do) camp on the obvious names, and often they move on and replacement libraries have to have cute names.

This doesn't mean people _will_ name their things sensibly of course. I've seen golang projects with packages named after gardening, meteorology, and aviation… and each is totally confusing when you're new to it, especially when the naming theme is completely unrelated to what the project actually does.

Here maven would be acceptable (similar to go, there is a domain identifier embedded) whereas cargo (bare words) wouldn't be great.


I disagree

for private Go packages all I need is to point it to another git repo. for nuget/maven etc - I need an artifact registry, with it's own bells and whistles


> Are you seriously saying that github.com/somelib ..

Well, yeah, I mean if one can say Maven is best package management then I think I can get away by saying Earth is flat or stationary or some such interesting facts.


To be honest, maven is not the best, but I would take it any day above npm, pip and composer. It's definitely orders of magnitude better than many 'modern' package managers out there.


npm is just another layer on top of git. go uses git directly. how is that any worse?


From someone who briefly used 'bower', which was JS dependency management using git, relying on git is a lot worse.

Github repos can vanish, NPM will only let repos be deleted if they have no direct dependencies also on NPM. So you have some guarantees your dependencies wont vanish.


If the repo was used at least once, it will be cached by the default proxy. At that point, deleting it will have no effect on builds


Does go have some intermediate cache repository between github and enduser?


Yes, https://proxy.golang.org/

You can also configure go to use your own for internal projects or if you just want to avoid the global proxy.


I want to second this. With quick compile times, fat binaries and first class support for crosd compiling, go is suitable for pretty much everything python would be used for. My only use cases for python are as a replacement to bash for shell scripting.


Eh, not really in the data wrangling and machine learning space, at least not quite yet. It's not really amenable to writing libraries for numerical processing - it ends up being super tedious to write something like numpy (you have to implement for every data type, there's at least a dozen+) and nigh-impossible to write an API like pandas, because dataframes are just too dynamic. You could do these things, but it's just super unpleasant.

I'm hoping generics makes it easier to write things like numpy, without doing `meao_u8`, `mean_u16` etc.

+ there's bool, int/uint8,16,32,64, float32/64, complex64/128, that's 13 right there, plus object, a bunch of extended sizes on some architectures, and c-alias types.


That's a fair point - I don't work in the analysis or ml spave so have no understanding of it!


Other people point out ML is a use case where Go isn't suitable. And I agree.

But as someone who used to write a LOT of Python, Go has replaced it for anything service or business-logic oriented, and while I do very little data wrangling and machine learning, Julia would be my go to for that. Julia and Go are in, many ways, complementary opposites. If you need to use PyTorch/TensorFlow, okay, fine Python (I'm not going to seriously advocate for just wrapping it all in PyCall.jl unless it's just incidental usage), but Julia is a really strong language for this stuff nowadays.


Sorry to break it to you, interfaces are OOP and trace back to Objective-C and Common Lisp Object System protocols or BETA patterns.


I'm talking about Go vs Python, not about the origins of OOP.


> I'm done with OOP and dynamic typing.

It is hard to be done with OOP when Go also does OOP, even if in a different way than Python does it.


I like Python the language just fine. Static typing needs a good bit of work, but the real pain points for me have always been performance and tooling (especially package management).

But Go has solid answers for these problems and an excellent static typing story (generics are gravy, but sum types would be nice). I’ve been very happy with Go on balance.


Except when you need to use struct tags, then everything is flimsy. I would rather have C#'s attribute over strings only.


I haven’t had any problem with struct tags personally. I wish Python had some sane way to reliably auto marshal objects to JSON in the stdlib.


I have also left Python for projects where I can chose the tools, but I have not stuck with Go. I went to Racket which has much like Python a "Batteries included" feeling, it is a very easy language with a lot of power and a really good JIT compiler. For me, it is perfect for exploration and algorithm design. For example, I could develop an algorithm in Racket and easily port it to C++11 which was what the client / employer ultimately wanted - still having a much quicker total development time.

For things that are performance-critical or require high reliability, I have successfully used Rust, even if I am still much slower writing it. One thing I like is that the resulting code is not more verbose. Which also means that it is easy to read even after months of not touching it.


That is a strange sentiment, based on the reasons you've given. Package management on Python with pipenv or poetry have not been an issue in my experience. The other half is kinda hard to pinpoint since "feel" doesn't say much, but is surprising given how clear and powerful the syntax is. I've also taken to use type annotation in Python to ease some of the typing obscurantism of pure dynamic typing.

And python does have map / reduce and much more... so I'm not sure what you mean there.

OTOH, I feel Go and Python are mostly used in quite different domains. To me, Python is the new bash. Go is more for those who dislike C++ and Rust (or Haskell or...) and prefer a certain kind of simplicity.


If you don't like Django then you should really try out Flask. It is everything that is right which is wrong about Django and vice versa.


fwiw, I think your experience speaks more to Djanjo and other OOP-inspired packages. Python allows for many different styles of packages to be written... and it's not always a good feature to have.


> The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.


A quick[0] 13min video on generics by Ian Lance Taylor.

[0]: https://www.youtube.com/watch?v=nr8EpUO9jhw


Thank you. I'm not a gopher - but shouldn't code-gen be mentioned as an alternative too? Or is that used for something else?


I'm going to start writing functional Golang and none of you can stop me


More than being able to use it I am happy others will stop making jokes about Go


Wait til they find out that methods cannot introduce generic parameters. I predict that complaints will shift from ‘no generics’ to ‘no generic methods’.


No more complaining about lack of generics, yeah! But you will still have people complaining about error handling in every single discussion about Go


People who are looking for reasons to complain will always find more than enough - in Go and any other language too...


I guarantee we will still hear about how long it took them to add generics to the language and how the language is only popular because of “massive Google marketing campaigns” and how the language is “stuck in 1970” and so on for years to come. People who make silly criticisms aren’t likely to be pacified, and I don’t think generics will make people less upset that Go is eating into their language’s marketshare.


I've been using Go since maybe 1.4 and that's only 7 years ago. I started using it as my main language around 2016 or so. I knew people complained about generics and wanted them myself, but the ease of using this language have far outweighed any complaints I've ever had about generics or comma-error or comma-ok patterns that have evolved out of the core design.

I would love some of the proposals surrounding errors to move forward, of course. But I'm OK with the current state, and will be happier with 1.18.


Ok then... how about something easier...

  for bar := range list {
  }


And of course "list" is defined as []int, so you get really peculiar results.



This caveat exists in every language that supports closures, afaict.


Err, no? Most languages will make a copy for scalar values (e.g. ints), e.g. Javascript and python do that. C++ and Rust go further and provide the user the ability to take the arguments by value or reference, even for classes/structs, which gives you control over what you want. And in this specific case, `v` should be a new instance for each iteration really, it makes very little sense to say that a reference to v of a previous iteration of the loop stays alive for the next iteration. Which is why the program as it exists would be a compile time error in rust, for instance (Maybe in C++ as well, I'm not well-versed enough in the lambda rules of C++).


What does this have to do with values or references? They are orthogonal to scopes, and this example is an issue of scoping. A closure finds free variables in one of its enclosing scopes. If the for loop creates one mutable binding for its control variable, even multiple closures created within the loop's body can all see this single mutable binding because they all share the single environment with this mutable binding in which all of them were created, so they can all observe this binding mutating over time. There are no "arguments by value or reference" because the closure in the example is nullary -- it has no arguments at all.


They're talking about capture by value or reference. If you use capture by value, created closures will use the value of captured variables at the time the closure is generated.

e.g. in C++, where one has to make capture by reference explicit

    int i = 0;
    auto f = [x = i, &y = i] { //x captures i by value, y by reference
      std::cout << "Value:" << x << ", Reference:" << y;
    };
    i++;
    f();
will print out "Value: 0, Reference: 1".


I'm not sure you can call this "a closure". That term has a rather specific meaning related to lexical scoping which simply doesn't deal with copies or references or anything of the kind. There's no "capture by value or reference" since it's not a copy of a value, or even a reference to it -- it's one and the same variable binding inside and outside of the closure (whereas a reference would be another variable name referring to the same value, or at least a differently scoped variable of the same name referring to the same value, but that's not lexical scoping anymore). To put it bluntly, C++ is faking closures, since without subjecting environments to GC or refcounting, it can't delete the shared environments at just the right time. They just jumped on the bandwagon of closures being fashionable nowadays and tried to emulate closures as closely as possible by constructing artificial automatically generated objects in the background using some syntactic sugar without breaking the existing language.


I don't really understand your objection. How is

>at least a differently scoped variable of the same name referring to the same value, but that's not lexical scoping anymore

not lexical scoping? That's literally just how closures work under the hood.

And that's exactly why closures have to deal with references (or copies) - every implementation of them is a function pointer and a record for the lexical environment, the latter of which requires that at closure creation time the relevant bindings are captured, either by creating a reference or making a copy.

I also don't understand why C++'s (or Rust's) closures would be 'fake'. If you capture by reference, they work exactly like the closures of other languages. Not having a GC just means you have to be wary of lifetimes. If you use a reference counting pointer (/some other GC'd pointer) you literally have the same closures as other languages.


> How is ... not lexical scoping? That's literally just how closures work under the hood.

In the presence of references in the language, these two cases may be observationally indistinguishable (maybe, I'm not a C++ guru?) but they're still different language features. Lexical scoping does not require taking a reference.

> And that's exactly why closures have to deal with references (or copies)

No, they don't. They deal with bindings. References are either an explicit thing you create (as in, int &b=a; or such), or a parameter passing mechanism...but a free variable is neither (it's not a parameter to the closure in any case), and it exists even in languages with no notion of references whatsoever, such as for example Scheme.

> I also don't understand why C++'s (or Rust's) closures would be 'fake'. If you capture by reference, they work exactly like the closures of other languages. Not having a GC just means you have to be wary of lifetimes.

Yeah, and that's at least one of the things that make it fake. Why would you have to be wary about lifetimes? Closures in languages that support them Just Work(TM).


>References are either an explicit thing you create (as in, int &b=a; or such), or a parameter passing mechanism

This strikes me as an unusual definition. I'd say that references are simply a pointer that is treated like the underlying value i.e. they're 'boxed' values. This is a notion that is essentially present in all languages. e.g. Java (or Scheme) which don't have some explicit reference feature, still have 'unboxed' values (e.g. integers) and 'boxed' values (e.g. lists).

>They deal with bindings.

Sure, to create a closure, you need to capture the relevant bindings (the lexical environment). But how does on deal with bindings? Well, a binding associates a variable to a value. Now, when you capture a binding, you can either point to the original binding (capture by reference) or copy the value (capture by value).

>Why would you have to be wary about lifetimes?

I mean, that's just how these languages are, right? Even if you do something incredibly mundane, like returning a value or calling a function with a parameter, you constantly have to be aware of lifetimes. That's why to me it seems like a completely orthogonal issue to closures.


What do you mean by "languages with no notion of references whatever"? At least in Scheme I think references are a pretty immediately obvious feature. It's not like every assignment does a deep copy, nor does free variable capture in closures. The moment you do stuff with set-car! and friends you are very explicitly working with references


Scheme doesn't have references.

> The moment you do stuff with set-car! and friends you are very explicitly working with references

No, you're not. At least not in the sense that references are references in C++ or in the established CS term call-by-reference. Likewise, you probably wouldn't say that C has references because you can "do stuff" with pair->car=foo; in C.


Yes, you are.

Notwithstanding the fact that C pointers are syntactically not really references, at least in my opinion, the way to spell an example for what they are referring to in C would be "pair = foo". You aren't modifying *pair, but the reference object stored in your own private stack frame.

As long as mutability is out of the picture and the aptly named notion of referential transparency is preserved, there is no way to discern whether any two symbols reference the same value.

The moment you flip the proverbial page to chapter 3 of SICP and start using setter-functions!, you lose this. You should very much become aware of the fact you are working with references whenever you mutate things, because the mental model of lists as values falls apart when you write, for example:

(define a (list 1 2 3)) (define b (cdr a)) (set-car! b 'ta-da!) (display a)

And then get left to perplexedly stare at the REPL's output.

It is true that references are not, in some sense, a language-level/opt-in feature of Scheme, as in that you cannot arbitrarily create a reference to an integer, but cons cells very much are represented by references to them only, in much the same way as class types in Java are.


You are wrong about Python. It absolutely does the wrong thing:

    >>> funcs = []
    >>> for i in range(10):
    ...   def f():
    ...    print(i)
    ...   funcs.append(f)
    ...
    >>> [f() for f in funcs]
    9
    9
    9
    9
    9
    9
    9
    9
    9
    9
    [None, None, None, None, None, None, None, None, None, None]


String is a pointer/slice type under the hood, even if you don't see the asterisk. So when you are passing v, you are passing a reference to a slice which gets updated by the loop. Since the goroutines can run in parallel, you print whatever v is pointing to at the time.

C++ lambdas do the same thing if you pass a pointer.

Python would do the same thing if you had a mutable non-threadsafe objects passed to threads. Eg if you iterate over a list of dicts, and modify the dicts, you'll get the same effect.

So then the question becomes, why does v get re-used, instead of assigning a new pointer each time? Dunno, probably performance. I could see the latter generating a lot of garbage, which can be totally avoided if you don't pass mutable references to goroutines.

What go really lacks IMHO is some kind of const modifier for these kind of references. Maybe it does, all I remember are const scalars.


> String is a pointer/slice type under the hood, even if you don't see the asterisk. So when you are passing v, you are passing a reference to a slice which gets updated by the loop. Since the goroutines can run in parallel, you print whatever v is pointing to at the time.

You'll have the same problem with a simple integer (for example, as in https://go.dev/play/p/uqvRoO3ynZt). Nothing to do with v being a reference to something.


Java's solution to this problem is maybe the simplest and most elegant: The compiler simply won't let you reassign to a local variable after it's been closed over. I think Go erred by not doing this. (Although I guess it would have complicated things a little because Go has mutable value types, so you'd need to figure out how to handle those.)


There are languages people don't complain about and languages that people use.

For me, Go is essentially pointless without generics and better error handling (I love that there are no exceptions, but since 99% of the code just return the error anyway, that needs to be the default path), but even with it, it is not much more than C with garbage collection, a few nice features and better syntax in some cases.

So even if it got these problems solved well, I still don't really think I would be using it.


Why does one get bothered with that argument anyway? If it works, it works. People who complain are the people who have a problem. But it’s a different problem and they don’t realise that.


Agreed.

There are only 2 kinds of languages: Those people complain about, and those nobody cares about.


>will stop making jokes about Go

If you seriously think that's gonna happen, you haven't been to Go mockery stations on the interweb.


We wont


I hope the next big thing is a proper error handling instead of if-hell.


I think people are constantly focusing on the wrong target here though.

The main problem of error handling in Go is not the tediousness of writing

    if err != nil {}
after each function (it's not nice but bearable).

The main problem is how hard to actually process errors.

In Go, there are three types of errors: custom structs with contextual info, predefined error constants of type stringError aka sentinels, and wrapped stringErrors, produced in-place by fmt.Errorf.

The first ones can be matched by type coersion or errors.As function. The second ones can be compared with == operator. The last ones can be either parsed by regex, or matched by the error they wrap and then unwrapped.

Oh, and the signature is nearly always just error, so basically to process errors gracefully you have to read the docs for each function instead of just looking at the signature.

It's so extremely inconvenient and tedious so nearly nobody process errors in Go, despite what language creators say. People either return error to the top of the callstack, maybe wrapping it in additional string, thus emulating poor man's exceptions without stack traces, or panic in-place. Nearly nobody catches and matches errors, as many in C++, Java, Scala or OCaml|Haskell world do.


Exactly. My experience and opinion as well, but better explained that I could have.

My IDE helps a lot the typing of the error conditions and is collapsing it nicely. This is not the problem, but the error handling.


There was a spurt of this before they seriously went to work on generics, and the conclusion was "actually, it's fine the way it is". There's huge discussions on the matter as well as various proposals, but they all came with caveats that, in the end, weren't considered a significant improvement.

That said, with generics, it should be possible to change to the 'Either' style [0] of error handling. And with another compiler feature to be added, er, 'exhaustive switch/case' or whatever you want to call it, they can make it a compiler error to not handle the error case. That said, I don't see the language developers or community making it a standard anytime soon.

The current style of error handling doesn't hurt enough. It's verbose and repetitive, sure, but if the alternative is something clever or magic, it's not the way to go.

[0] https://www.scala-lang.org/api/2.13.6/scala/util/Either.html


Give it 20 more years of evolution (considering we're talking about Go, probably 70 years), and we get another C++. A Google of the future (let's call it Scroogle) starts another language that can be used by recent graduates that have no idea about what they're doing to write mountains of JSON fiddling boilerplate. Rinse and repeat.


Hey, don't be so pessimistic. We'll probably have another serialization protocol by then.


This is the biggest complaint after generics, and there are a few proposals to dynamically resolve it in either libraries or language features. These proposals are in the go2drafts section, but keep in mind that this is where generics were originally placed on the timeline. Yet here we are with a backward-compatible change to the language in the sub-2.0 version of the language.


There are 2 kinds of languages, one that people love to talk about and philosophize about and others that are used in real projects. Golang firmly fits into the 2nd category of languages that are highly opinionated and full of pragmatic choices. It is rare for a language to capture so much mindshare within a decade of it's launch and I'm glad that Go had managed to reach this stage. I am really looking forward to what this new era of Golang brings with it.


I'd add to your first category that there's a lot of languages that adopt features that other languages have, making a ton of languages converge into basically the same language with some caveats. I don't remember the post now but I believe it was posted on HN some time ago, it was pretty insightful.

Anyway yeah, I'm glad Go is resistant to change.


No idea why you are getting downvoted, but I fully agree with that sentiment. There are already enough open source languages who seemingly want to be "everybody's darling" and implement any reasonably sensible feature request. One example: PHP's new "match" expression (https://www.php.net/manual/en/control-structures.match.php). Of course, I can understand that people like Go's more flexible "switch" better than the "traditional" C-style one, but if a language already has the traditional version, does it really have to add a new one which does mostly the same thing and in doing so cause headaches for thousands of developers who use classes or constants named "match"?


http://www.jerf.org/iri/post/2908 ?

(BTW, 2011 on that. I would not today agree with the statement "the Haskell community is the only community I know that is doing new things in the field of language and API design". I think Rust is doing interesting new things now. In 2011 they were still working on basic functionality. There's a couple of other interesting little up-and-coming languages.)


They resisted generics since day 1 on the basis of pragmatism. Now they included them on the basis of pragmatism?

I remember something similar with their approach to package management which they said wasnt necessary for years.

They really do seem to resist doing the right thing for as long as possible.


I believe I watched a talk from Rob Pike a few years ago where he mentioned that Go didn't have generics, not because they didn't want generics, but because they wanted a Go-like way of doing them, and wanted to spend a lot of time making sure they were done right.

I'm paraphrasing and I wish I had a source for you but I'm pretty confident I didn't make that up.


The FAQ said something similar - not that they were vehemently opposed, just that it wasnt a priority.


They never resisted generics as a concept. But they refused to implement generics in a way which compromised other Go values. Compilation speed was a big one. It's easier to implement generics if you sacrifice compilation speed (see: c++, rust, and iirc java). To their credit, it took the community a looong time to figure out how pull that off, because it's a hard problem. But I think they've done it.


FWIW Java doesn't sacrifice compiler speed for generics, they're entirely erased at compile time which is cheap.

What they sacrificed was runtime correctness, because the type erasure lets you sneak invalid types into unsuspecting code. How much of a problem that's caused in the real world is debatable.


Are you sure you are not mixing up runtime impact with compiler speed? In Java generics are still part of the type system and are type checked which is a non trivial part of the compile time increase in generics typically.


Ah yeah, it seems I was...


They weren't ever firmly opposed to generics. Here's the FAQ from 2009:

https://web.archive.org/web/20091113154906/http://golang.org...

> Generics may well be added at some point. We don't feel an urgency for them, although we understand some programmers do.

> Generics are convenient but they come at a cost in complexity in the type system and run-time. We haven't yet found a design that gives value proportionate to the complexity, although we continue to think about it.

12 years later, here they are.


Java was created in 1995 and got generics in JDK 1.5 in 2004, I don't see anyone using this argument against Java. The weird fixation people have about Golang generics is really tiresome, that too after they have added generics after much deliberation.


Having watched this for a while, I would say, my fellow programmers, there's no need to build elaborate psychological defenses for why you don't want to use a language. Just don't use it. There's too many languages to know them all anyhow. You don't have to decide not to use something, then justify your decision with a few comfort blankets that you post every time the topic comes up. Just don't use it, and leave it at that. Or at the very least, skip posting your comfort blankets every time the topic comes up. There's a dozen languages I'd like to know better, and don't use them, and I don't run around giving myself "reasons" I don't learn them. The "reason" is simple: I don't have time. Languages are not an attack on you. Nobody who designed a language was even thinking about you when they created them.

I speak here to those who slag on Go and haven't so much as run "Hello World" in it. Those who have used it, especially for some period of time, and especially those who tried long enough to learn to program Go-in-Go (as opposed to Rust-in-Go or C++-in-Go, etc.), and decided they didn't like it due to their experience, I invite to continue discussing. You're not who I'm talking about here. Real experiences with the language are valuable contributions.

Those who haven't had real experiences are not making valuable contributions. "Lol generics, lol they realized they're wrong and I was right all along" and "Lol error handling is bad in Go" are not valuable contributions.


This here is the right sentiment, thanks for saying this. I would not bother the internet if my preferred feature is not present in Zig/Nim/Elixir.


You don't think it contributed at least a little to Go eventually adding generics?


No, I don't think the whining did a thing. It may seem impressive on HN, but compared to the continuous stream of whining any successful project receives about all sorts of things I suspect it was weighing less heavily on their mind than HN would think, because it's proportionally much smaller than HN thinks. Also, it's not like Go isn't already fairly successful; if I wrote a language as successful as Go I wouldn't exactly be crying myself to sleep every night, you know?

Cruise through the Go issues and look at all the closed issues. Then, for context, cruise through Rust's or Python's, or your favorite open source project of some size that isn't even a programming language. It's a constant deluge.

What did it was real use cases from using the language for a long time, offered by serious people who weren't just berating the designers. If you read the generics design, and especially if you read the whole history of the design, you can see where this has had an impact, where the features are tuned for the use cases presented by the community, and how the design has changed at least twice in a major way (depending on how you count, of course) as people work through the use cases.

You'll also come to understand better why it's not like you can just flip a switch and "turn on generics" and why everyone running around claiming it was easy is dead wrong. You can also learn some of this by paying attention to discussions of generics in other languages when Go is not a topic, because it'll become clear when not discussing Go, there's a lot of broken generics and strange edge cases and poor decisions made by existing languages that we have to live with forever. Not that all of them are broken, but, again, the idea that some care needs to be taken wasn't something the Go team just made up. There's plenty of languages, even big name languages like Java, that "flipped the switch and turned on generics" and it turned out not to be as awesome as presented, because it's a legitimately hard problem even in 2021.


> Java was created in 1995 and got generics in JDK 1.5 in 2004, I don't see anyone using this argument against Java.

Java was created in an age where very few languages with mainstream relevance had userland generics.

Go was not, and generics had already had to get (somewhat painfully) retrofitted in two of the above.


Yeah, this. In 1995, about the only mainstream language with generics was C++ with templates. (Same in 2000 when C# was released, incidentally)

When Go came out in 2009, though, Java and C# had pushed generics into the mainstream. Additionally, they'd shown some of the problems with trying to retrofit generics onto a language and ecosystem that didn't previously support them.


In 1995, Standard ML, Ada and Eiffel had generics and were hardly mainstream, C++ templates were still experimental, everything else did not used them.

A tad different from the computing world in 2009.


Is it really that much of a bad thing to iterate based on real world experience?


It is when they spend a decade insisting that they right and everyone else was wrong, then ended up doing it anyway.

Generics aren't a cutting edge feature that they wanted to see how it would develop. Their objection against it was ideological.


I don't really understand this type of criticism. People complained that Go didn't have generics, and now that they've delivered, people still want to complain for some reason.

I can quite believe that the Go team wanted to be sure that the approach made sense for Go, that it was the right solution to the right problem and to keep to their backward compatibility promise. Experience is a good teacher. I really don't know why it matters in the slightest that they didn't arrive at that on day one.


>they spend a decade insisting that they right and everyone else was wrong

This just isn't how it happened. The Go team never said that they were opposed to generics. Their stance was (i) that it was not a top priority and (ii) that it was difficult problem and they'd take the time to implement generics correctly.

The idea that this is about one group of people finally proving that they're 'right' is a distortion based on viewing events though the lens of snarky comments on the internet.


Why are you so bitter about it?


The Go team takes their time to thoroughly consider and conceptualize new features. That is a often forgotten virtue.


It’s not like they just had them ready to go at the flip of a switch and we’re just holding out.

There were trade offs and they didn’t have a solution on day one.

They’ve always said they were open to them with the right implementation.


No, they didn't.


Exactly, just like firefox / chrome

you hear about firefox everywhere, everyday, and yet only has 3% marketshare


Cool. Does anyone know of libraries that are using Go generics yet?


A better xmas present than anything I've ever received. bravo!


Never needed in my opinion but here we go...


Not needed, but good to have. I've immediatly installed Go 1.18 yesterday and created a generic stack, tree and set with corresponding methods for traversal or union, intersect, ... it makes live easier.


So now it turns out that generics are good?


The Go team's stance on generics has never been "generics = bad", but "generics have advantages and disadvantages, and if we find an implementation which finds a good compromise between the advantages and disadvantages, we will do it". Which they now did. Let's hope it really turns out to be a good compromise...


[flagged]


Have you even looked at other languages' implementation of generics? Java's implementation takes up a significant part of the language spec, compiler, and VM for example.

Here's an insightful comment from some time ago, explaining why it's a technical issue, not a political one: https://news.ycombinator.com/item?id=9622417 It links to this FAQ highlighting a LOT of the complexities and pitfalls of generics in Java, things that are all to easily overlooked by armchair language expert: http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypePa.... Note that that's just one chapter in a very large document.

Comments like this say more about their author's arrogance than that of the Go language team.


Which part of the JVM is complicated by generics? They're type erased so the VM shouldn't really care. The compiler on the other hand has plenty of complexity...


To me, "takes up a significant part of the language spec, compiler, and VM" isn't a meaningful reason to exclude something from a language, unless it's a hobby project. The job of languages, imo, is to provide tools/features/syntax for expressing problems, whether their implementation is complex or not. Ruby is an incredible language to use, but its implementation is notoriously hairy. That internal complexity is not something that matters to me in the slightest.

I understand the Go team's reluctance to get the user-facing interface wrong, and end up with something like type-erasure in Java; that's slightly connected with implementation complexity, but saying "it's complex to implement and specify" doesn't seem enough to me.

[edit: made it clearer that this is my thought on the job of languages and not some universal truth]


I actually have a different view. One of the things I love about go is it's a simple language, and I can hold the whole language spec in my head while I'm programming.

A generics implementation can't sacrifice the simplicity of the language, or make it any harder to read go code.


    I can hold the whole language spec in my head while I'm programming
People love to say this about Go, but it's not something that, like, actually matters (and I doubt it's literally true). A language can be reasonably understandable such that you can effectively and productively program in it without being aggressively simple. Again, to use the case of Ruby, I might not remember every single syntactical construct or handling of every edge case, but the language is reasonably understandable such that I can write programs without constantly asking "what was the syntax for that?" or "how do I express this?" As a counter-example, I think the complexity of C++ is not just limited to its implementation, and leaks out to its interface.

    ...sacrifice the simplicity of the language, or make it any harder to read go code
These are two separate concerns which, while somewhat related, are not directly correlated. A simple language can make it more obvious which syntactical constructs are being used and what literal operations are being performed, but there are many more dimensions to reading code than just those.


lisp and smalltalk both manage this while allowing sophisticated abstractions.


I can hold the entire spec to brainfuck in my head, but it doesn't make it any easier to write.


> It should be noted that "takes up a significant part of the language spec, compiler, and VM" isn't a meaningful reason to exclude something from a language, unless it's a hobby project.

If we assume complexity ~= "takes up a significant part of the language spec, compiler, and VM" then I think it's an excellent reason not to include something! Somebody has to maintain this stuff and making it complicated makes that hard, which in turn means less time to spend on things like tooling, performance improvements and documentation which then makes our lives as users of the language harder.

Of course there is a trade-off here but I think, in general, the go team manage to strike a nice balance.


Right, but the entire point of my comment was that "takes up a significant part of the language spec, compiler, and VM" is not a good approximation of complexity. It can be related, but it is not necessarily true that something with a complex implementation has a complex interface.


I think we are talking past each other a bit, as I completely agree with

> it is not necessarily true that something with a complex implementation has a complex interface.

In turn, my point is that as a language implementer you only have so much time available to work on the language and surrounding ecosystem. If your time is spent dealing with a complex spec, compiler and VM then you have less time to spend on the rest of the ecosystem. I think go has shown that spending tim on the surrounding ecosystem can be very valuable (e.g. go fmt, go vet and more recently go fuzz).


Ah, I understand now. That's a fair point! I agree that the excellent Go tooling is one of its strengths.


It was due to the careful and deliberate nature of the Go team, and the backward compatibility guarantee. It took years for the group to put together a proposal with minimal changes to the runtime and core libraries, and this implementation in 1.18 will be limited in scope to only add the feature, waiting for future versions to update other core libraries to adapt generics in a backward-compatible way.


Is there a source for that?

I ask because instances I read from the team were in the direction of: "We will gather enough use cases to decide on the matter".


While the GP was a bit...harsh, the main reason people have that view is generics, as a problem space, were solved. Java's way was published years ago. As was .NET's. Other languages exist which have some form generics. The argument of "gather enough use cases to decide on the matter" feels like a punt given the use cases already exist and were known in the industry for the past 30 years.


The problem space may seem solved, but apparently it's still not as easy as it looks from the outside. You don't have to take my word for it, Cthulhu_ linked a comment by Russ Cox (a major contributor to Go) from a few years ago above: https://news.ycombinator.com/item?id=9622417

> We have spoken to a few true experts in Java generics and each of them has said roughly the same thing: be very careful, it's not as easy as it looks, and you're stuck with all the mistakes you make.


I agree. From my Java experience most people don’t do a lot with them. List<Something> and the like. It makes things like boiler plate easier.

I’m not saying it’s easy to implement. Rather the ideas are there and implementable. Typescript has them and it’s relatively young.


Perhaps. But in hindsight, Go's generics implementation turned different enough to warrant benefit of the doubt in my opinion. A notable difference is that Go interfaces are implicit while C# and Java are explicit and that affects Go generics.


It remains to bee seen, meanwhile Rob Pike did comment[0], we should refrain from changing libraries in 1.18 immediately after generics are in.

"The reason is simple and compelling: It's too much to do all at once, and we might get it wrong."

[0]: https://github.com/golang/go/issues/48918


> It's too much to do all at once, and we might get it wrong.

If only people realized this about most software projects. There's a lot of hubris and under-estimation in software engineering.


[flagged]


As person who uses both Rust & Go at my $DAYJOB, I would say, both have their merits and demerits, I think, we should view them as tools instead of attaching ourselves to any particular language emotionally. I like to think them as different tools for different use-cases. Use the right tool for the right job. In my opinion, go does an excellent job at networking (TLS/HTTP) & quick cli apps while rust shreds text & does an amazing job at memory management (this is a figurative example; don't drone me if I missed your favorite feature).


I often see people recommend Go for CLI apps but I'm not sure why. The built-in `flag` package is really limiting. These days I mostly use Rust with `clap` which is the nicest flag-parsing experience I've found so far in any language.


Are you using "clap" because the "built-in" one in rust is not good? :P


There is no "built-in" command line parser in Rust. All it has is std::env::args (https://doc.rust-lang.org/std/env/fn.args.html) and std::env::args_os (https://doc.rust-lang.org/std/env/fn.args_os.html), which give you the raw command line. Rust does not follow the "batteries included" philosophy for its base standard library, to avoid "leaking batteries" problems like seen for instance with Python (https://news.ycombinator.com/item?id=19948642).


I know there is no flag library built-in in rust (I do quite a lot of rust as well as go). That's my whole point. It should be compared to something like https://github.com/urfave/cli . That would be a fairer comparison.


Sure, for example I usually use Cobra. But what I meant was that people often mention Go as being explicitly preferable to Rust for CLIs, and I’m not sure why if I have to pull in a dependency in either case.


Nobody is pushing you to use Go. Nobody is telling you Rust is bad. There is no language war. It's not an either/or. Stick with Rust if you're happy with it, nobody is attacking you over it.


So, for you, it won't be enough until Go (and possibly every other language that is different from Rust) is "rustified" and turned into a rust-like clone? ... just why?

And BTW, Go is very safe! it has automatic memory management, and managed concurrency for writing bug-free concurrent code.


The "lack" of "features" that are prevalent in other languages is a virtue of Go, not a flaw.


Go is a very opinionated language that lacks many features on purpose. If you want complex languages Go is definitely not a good choice.


Go write Rust then, what's the problem?


Nice!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: