Hacker News new | ask | show | jobs
by erikb 3223 days ago
I'm no Go expert, so the following may just come from having not used it enough.

The two things that shocked me out of continuing with Go were exception handling and package management.

Exception handling is basically not implemented. Instead the Go developers dediced to add half a step from return codes towards exceptions and work with that. And now the community seems to have decided to just reraise everything that comes their way. Nobodoy seems to see that such a simple basic feature shouldn't require a piece of code, it should be done automatically by the language. And the community doesn't seem to see why it's nearly impossible to debug. I really hope that Go developers have some secret tools I just don't know about, because there are no stack traces, and on the abstraction level of a user getting an error from an underlying framework printed out doesn't really help at all, especially when the code seems to continue after printing the error (making it a bug, by reporting a warning as an error). My favorite example is kubernetes helm reporting a connection error in my job's infrastructure, every time you use it, but after that error it actually switches from IPv6 to IPv4 and just works. But still it feels good about reporting that error. wtf.

The next thing is package management. I had high hopes here after going through the trouble of working on that topic in 2012 when the Python community was developing its package management. I mean if you don't add that on top of an existing world, but create it from scratch, there is a chance to just do it right and not have to bother with all the pains of legacy systems, right? Well, Go decided it doesn't need versioning on imports. Just put the Github link there, don't even choose a branch. How can any code ever get finished that way? My best guess is that now they have to develop stuff on top of that already-born-as-legacy system and also try to integrate that with their core. Sad.

I really hope someone can add some corrections to this view.

6 comments

The only time I miss exceptions is when I really need non-local return. Stack traces are available, see [1]. Needing non-local return really turns out to be the exception though (sorry). I find I want it when writing recursive descent parsers, and some other deeply nested control structures. In those cases, the panic/recover mechanism can be helpful. But for everything else, returning an error (using a standard error interface) works just fine.

As far as package versioning, what we ended up doing is vendoring everything into our source-control system. This fits our use better all around. For our production systems, we don't want inequality bounds on our library versions. We have tested our system with version X, we only want to ship with version X, not, X.1 or X.2. Vendoring allows us to pick a specific version. Admittedly, this makes more sense in the Go world, where executables tend do be statically compiled.

Vendoring makes it trivial to track local changes to code in our dependencies, and ship them upstream when appropriate. It makes new developer setup simpler. Finally, the Go toolchain has support for vendored code that makes the setup relatively painless. And near instant compilation means that you don't have to wait very long for your dependencies to compile.

[1] https://golang.org/pkg/runtime/debug/#PrintStack

Thanks for providing additional information. What do you do about the user of your tool having another point of view than you? For instance look at the helm example I provided. You may start a network call to IPv6 first, and when it fails (with an error, which is correct) you continue with IPv4. If either one of these works for your user there is no error. At best there is a warning scenario where you want to educate him about finally switching to IPv6. But if you report a connection error in almost any scenario that would be a bug in your code, because the connection didn't error. You did a test which failed which was expected by your code and handled correctly.
I'm about 8 months in to using Go for a few largish projects and I'd say these are probably the two biggest things I still struggle a bit with. (not generics as others seem to obsess about)

On errors, I'm really of two minds. In a way, it is a lot like how Java started with checked exceptions, it forced you to deal with the error. But at some point most people decided that was annoying and switched to runtime exceptions for everything, which while requiring a lot less code, still led to errors often bubbling up all the way to the user.

I think checked is the right thing, but it does require developers to be thoughtful and not just throw errors upwards. If you accept that checked is the route you want to go, I don't find Go's use of error values worse than exceptions.

On the package management front, I think the current best practice for projects that must not break is to vendor your dependencies. Go is working towards having better tools to allow you to specify SHA's for your dependencies easily but we aren't fully there yet.

While it took a bit to wrap my head around using `govend` to vendor my dependencies, in the end it really hasn't ended up being a big pain point in practice. I also never have to worry about a dependency either disappearing or pulling a trick of shipping the same version # with different code. (or having the repo/package manager be down, which anybody who has shipped a lot of code will tell you has happened)

So yes, I agree these are both, weird, but they aren't deal breakers. For our particular application, I really love Go, more so than I have any other language in recent memory.

> not generics as others seem to obsess about

One reason people obsess about generics is specifically because of error handling. With generics, you could implement Result and Option types, which make error handling significantly more sane.

Personally I loathe this style of programming. It's not that it's difficult, it just seems to obscure code a great deal.

Writing this sort of thing in Rust:

    fun some_function(a: &A) -> Result<&B, SomeError> {
        let c = foo(a)?;
        let d = foobar(a, c)?;
        Ok(if xfoo(c) {
            let e = blah()?;
            bar(d, e)?
        } else {
            baz(d)?
        })
    }
where you have to write every function in this pseudo-do-notation where 'return' is just wrapping the return expression in 'Ok' and `a <- expr` becomes `let a = expr?;` is just horrible.

I'd much rather write this:

    fun some_function(a: &A) -> &B throws SomeError {
        let c = foo(a);
        let d = foobar(a, c);
        if xfoo(c) {
            let e = blah();
            bar(d, e)
        } else {
            baz(d)
        }
     }
See how that's so much cleaner? It's not actually any different from exceptions anyway, you're basically using them like exceptions, and they're implemented in the same way. The difference is that in the latter the code is much simpler and easier to understand. That's all.

In fact, that syntax could be added to Rust (after 6-12 months of bikeshedding as usual) and just have it automatically translated to the above anyway.

The other issue with Result/Option is that people start doing really horrible things like adding Option::map. Sorry but it's not a container that has 0 or 1 things in it. It's an optional value. That they're mathematically equivalent doesn't mean that they're the same thing conceptually. It's as bad as pretending that Result<T, Err> is useless and everyone only needs Either<L, R> where by convention R is the error value. God please just no.

> See how that's so much cleaner?

No, I don't. I look at the former snippet and I can easily tell each and every function invocation that can cause SomeError. In your theoretical style, I have no idea whether foo, foobar, xfoo, bla, bar and/or baz will throw that error. I prefer explicit over implicit since I find it far more readable.

> really horrible things like adding Option::map

You can quibble about the names (Option and map), but Option is essentially the Maybe monad and map is bind, so you're kinda arguing against core functional programing constructs.

>No, I don't. I look at the former snippet and I can easily tell each and every function invocation that can cause SomeError.

The reason that functions have type signatures is that you can read them. You can tell which functions can cause SomeError by going and reading their definitions.

>I prefer explicit over implicit since I find it far more readable.

'Explicit over implicit' is dogma. Rust requires you to annotate your code with gibberish in cases where it is not necessary.

>You can quibble about the names (Option and map), but Option is essentially the Maybe monad and map is bind, so you're kinda arguing against core functional programing constructs.

That's literally my entire point. The attitude that it's technically a Functor so it makes sense for it to be called map? No, it doesn't. It's not a map. You're not mapping over anything. Naming is important.

Calling it 'the Maybe monad' shows that you actually have no idea what you are talking about. It's not 'the Maybe monad'. The Maybe monad is the instance of Monad for Maybe. It is not Maybe itself.

The entire concept of having the literal 'Monad' word as a word in your language, a thing that you use in programming, is very stupid. Monad is not a useful or good abstraction. Maybe is a good abstraction. Or Optional, or Option, or whatever you decide to call it. But Monad is a bad abstraction. Abstracting over superficial syntactic similarities between completely different constructs is completely stupid.

The name being terrible is not 'quibbling' by the way. Naming is incredibly important. Calling it 'map' just shows how out of touch Rust is with real programmers.

You'd need generics and algebraic types to implement Result/Option
False. That feature allows a more efficient implementation, but (T, error) (or an equivalent struct) can be reasoned about in much the same way as Result<T>. You don't need a tagged union when you can use the nil-ness of one of the two values in the tuple as the tag. Similarly, Option<T> is just a wrapper around a nullable T.
That would let people do this, and I hear people ask for it a lot. I dunno if it'd actually be better without the possiblity of not-wow-slow combinators.

Golang has all these weird performance pitfalls that you only hit if you contort the language too hard. Naming combinatior functions sometimes trips those conditions.

I'm of the opinion that even just pattern matching and the kind of nil type propagation checking that TypeScript does could help enourmously.

Generics won't fix error handling without compiler help, and the best way to get that help is to introduce pattern matching as a forcing function.

The type you propose can inhabit both different variants at once, the entire point of Option is that it is either something or not.
Checked exceptions suck. Every method has to explicitly throw them up to a higher level where they can be handled causing tons of useless boilerplate. Its much better to just let unchecked exception bubble to a higher level of the app. This is a best practice in java so letting the exception bubble all the way back to the user is just poor programming.
I have to google what the "checked" actually means. But in Java you can throw parent classes, in Python you don't need to declare them at all. And honestly having a bad implementation is still better than no implementation. The main point here is to get correct, helpful error reporting so people understand what's going on. If I spend another 4 hours debugging an error message that was actually just a warning and not a biggy at all I'm going to throw up.
That's exactly what you have to do in Rust anyway though.

Except instead of just writing `throws SomeError` in the declaration line of the function, you have to annotate virtually every line of your function with `?` and wrap the return line in `Ok(...)`.

The ? RFC did include a "catch" construct, and there's been some discussion about doing something more like "throws SomeError." We'll see!
It's far too late for "we'll see".
Package management is being worked on as part of the hopefully-to-become official 'dep' tool, which will prefer semantically versioned releases over pulling trunk. Things might be good here in 12-18 months. Its been acknowledged as a problem by the language developers.

Error handling is still exactly where you remember. Most of your code is still 'if err := p.something(); err != nil { return fmt.Errorf("I need to annotate the error so I know where this happened: %v", err) }'

Yes, you still tend to get regurgitated and poorly applied rationalizations if you bring up limitations. However, the language designers have been soliciting use cases and requests for improvements for Go 2.0 via their wiki. It does involve educating and convincing the community, which is a hard road and likely only to get submissions from the echo chamber, so I'm not holding out much hope beyond something like generics; it would be a losing battle to pitch error handling changes to true believers. Time will tell if this is a good approach or not, as opposed to messier changes like you see in something like Python.

Package management is currently been worked and the tool[1] is stable now, at least the user facing parts. Here's where things stand as of a few days ago[2] There has been efforts to use dep in kubernetes[3][4]

1. https://github.com/golang/dep

2. https://sdboyer.io/dep-status/2017-08-17/

3. https://github.com/golang/dep/issues/110

4.https://github.com/golang/dep/issues?q=is%3Aissue+kubernetes...

The go dep tool has been worked on and promoted by some of the Go committers over the past year or so, but we don't really know if the Go Language designers with the clout to change the toolset have been sold on the idea yet, or if it will be vetoed later on. Some of them haven't said anything in support of it (or against it), so go dep is still really just of proof of concept.
>especially when the code seems to continue after printing the error (making it a bug, by reporting a warning as an error).

Maybe I'm misunderstanding something, but if you call a function and it returns an error, it's your responsibility to ensure that you handle it. Generally you'd assume any data returned by the same call that returned the error is useless (except stuff like EOF, in some cases?).

Yes, exactly. That's what one would assume. I don't understand why a whole language community decided to not handle any errors and instead just bubble them up without filtering, without adding more notifications about the context like log messages, without trying to recover. Why would they expect a user to know their whole dependency tree and all their error messages.
I am not aware that they did. Most good Go code does not do this. That there exist some badly written Go programs does not contradict this. Go functions that can fail, should return an error as part of the function returns. Like with exceptions, you can decide to ignore any error or properly handle it. Even with checked exceptions, you can have an empty "catch", which means you are just ignoring it. So the difference is rather syntactical. And like with exceptions, proper error handling is entirely the responsibility of the programmer who writes the error handling code. The Go version of error handling requires less typing overall, and makes it a little bit easier to ignore errors. Unhanded exceptions on the other side can unwind the stack arbitrarily high up until they do eventually find a handler - but it is not said, that this handler can properly handle the exception, as it might be separated to far from the error source.
As far as I know bubbling up errors instead of handling them is recommended practice: https://github.com/nats-io/go-nats-streaming/issues/143#issu...
>The Go version of error handling requires less typing overall

Do you mean as compared to using exceptions in a language like Python or Java? If so, can you give an example of code that does the same task, in both types of languages, to make this more clear? Thanks.

I was mostly thinking of Java. Ignoring an exception would be done with try {....} catch (Exception e) {}, while in Go, you would do: x,_ := f(...). And if you want to check a returned error I think the if err!=nil {...} is still a bit less typing than the Java version, and you don't have to declare checked exceptions you might throw.
In Go, if you call three functions you need to repeat the

  x, err := do_something()
  if err != nil {
          return nil, err
  }
ceremony three times. In Java it happens by default, because the language designers agreed this is by far the most common case.

Ignoring an error is almost always a serious mistake, so the fact that Go makes it easy and not blatant is not a good thing.

Got it now.
Google C++ style guide states that you should not use exceptions https://google.github.io/styleguide/cppguide.html#Exceptions golang was probably designed by people following those same guidelines ...
If you want a stack trace with your errors you can use errors.Wrap()[1]. This repo should replace the stdlib errors package.

1. https://github.com/pkg/errors/blob/master/errors.go#L180

Thanks! Can this also be added to already compiled tools somehow? E.g. kubernetes uses a lot of go components, but of course I only receive them in compiled form.