News for hackers. Stuff that matters.
Re:Twelve Years of Go by new_stranger on Wednesday November 10, 2021 @02:24PM (#29179281)
> I'd spend hours condensing 10 lines of perfectly working code into 1 line of the most concise text possible

Which is also the hardest part of convincing people to use Go for me: "Why can't I just .map()/.filter()/.find()?" followed closely by "Why do I always have to check for errors?"

What is odd to me is that while yes, my Go code is more verbose than my node services - I always end up writing less Go for the same thing.

Re:Twelve Years of Go by jerf on Wednesday November 10, 2021 @03:36PM (#29180381)
My non-Go code is starting to look more like my Go code.

One of the things Go taught me is that I was not being as careful about my errors as I should be. It can be argued that exception-based handling provides you a nice baseline default, but it makes it way to easy when doing network or system-type programming to thoughtlessly default to that, when you need to be thoughtfully defaulting to that.

With sufficient care, exception-based programming and errors-as-values converge in the end anyhow in "code that treats errors correctly". But my exception-based code is a lot more informed by the errors-as-values approach now; a lot more try statements with catch statements that actually do something.

Even Haskell's very nice Either monadic handling can make it too easy to be in the heat of the moment and not thinking about what the errors actually mean and what I can do about them.

I don't think it's appropriate for every domain, which is why I qualified the code I tend to work on. But in those domains I'm thinking a lot more about how every single line of code can go wrong rather than leaning on default exception-based handling and expecting it all to work out.

Re:Twelve Years of Go by vlunkr on Wednesday November 10, 2021 @03:51PM (#29180577)
> One of the things Go taught me is that I was not being as careful about my errors as I should be.

I identify with this so much. Especially when dealing with external things (file system, database, network) things can go wrong at nearly every step. And yeah, that means you have to check errors at every step, but it forces you to think about how you want to handle them, and what message you want to propagate when they happen. As a result, my Go code has very few unexpected errors in production.

Re:Twelve Years of Go by new_stranger on Wednesday November 10, 2021 @04:08PM (#29180836)
Hard to overemphasize this. Handling errors is similar to the benefits that writing tests provides - slower upfront, but a more stable product gets shipped.

Handling errors everywhere means problems are already solved before they happen. No 1 am pagerduty alerts because we didn't consider what would happen if a DNS server hung until we timed out and thought just wrapping in a try/catch and crashing was a good idea.

Re:Twelve Years of Go by closeparen on Wednesday November 10, 2021 @06:46PM (#29182533)
Things can go wrong at every step, which means each collection of fallible steps carries with it a combinatorial explosion of successes and failures. Do 5 fallible statements in sequence and it's 5+4+3+2+1 = 15 mock expectations you need to write. This tedious enumeration exercise is the majority of the time that goes into writing Go programs, for me at least.
Re:Twelve Years of Go by boloust on Wednesday November 10, 2021 @07:35PM (#29182972)
Most of the time, you'll return early after an error.

To get into a situation where you'd need to handle n! cases, you'd need to keep running the statements after a failure, and then collate and return all the errors at the end.

Not sure why you'd want to do that. You'd definitely need to go out of your way to do something like this, and probably start asking yourself why you're doing this pretty early on. Plus it would flat out refuse to compile in some cases where there are dependencies between the statements.

Re:Twelve Years of Go by closeparen on Wednesday November 10, 2021 @07:42PM (#29183020)
It’s + not * in that example.
Re:Twelve Years of Go by boloust on Wednesday November 10, 2021 @08:07PM (#29183222)
Ah, okay. In that case it becomes

  {n + 1 \choose n - 1}
What code are you writing that results in this set of possibilities?
Re:Twelve Years of Go by jerf on Wednesday November 10, 2021 @09:58PM (#29183965)
I have some code where I use multierrors and collect everything into one big error (I highly recommend this for "validation"-type code, you should generally return all errors not merely the first), but what code are you writing where you're running 5 statements unconditionally but need to handle nearly-arbitrary combinations of them failing, and multierrors aren't what you need?
Re:Twelve Years of Go by closeparen on Wednesday November 10, 2021 @10:09PM (#29184043)
If you have 5 statements like "if err := doThing(); err != nil { return err }" then you need to simulate the success of all 5, success of the first 4 and failure of the last, success of the first 3 and failure of the 4th, success of the first 2 and failure of the 3rd, etc.
Re:Twelve Years of Go by morelisp on Thursday November 11, 2021 @07:46AM (#29187305)
For a total of six cases to deal with. Not 15.
Re:Twelve Years of Go by closeparen on Thursday November 11, 2021 @12:20PM (#29190456)
I said 15 mock expectations, not cases.
Re:Twelve Years of Go by morelisp on Thursday November 11, 2021 @01:47PM (#29191485)
If all five of those things also need to be independently mocked, you're mocking too much and your mocks suck.
Re:Twelve Years of Go by closeparen on Thursday November 11, 2021 @03:51PM (#29192995)
If they're not mocked it's an integration test. Integration tests are welcome as a supplement to unit tests, but unit tests are table stakes.
Re:Twelve Years of Go by morelisp on Friday November 12, 2021 @03:08AM (#29197245)
An automated test suite is table stables. If you can achieve correct, maintainable code better with functional or integration tests than unit tests, you should.

There is no hard technical line between "unit test" and "integration tests", it's a semantic question of how you define your units. Even if do you take a tiny, restrictive definition of your units, you could still use a fake or stub instead of a mock.

https://martinfowler.com/articles/mocksArentStubs.html

To get very specific for Go, we use testify suites, tend to set up fully functional stubs in `SetUp`, and then test cases either use them to verify happy paths, or `s.BreakStep1()` in one test, `s.BreakStep2()` in the next, etc. So in this case we would write a total of five extra lines for the five possible ways to break.

Re:Twelve Years of Go by closeparen on Friday November 12, 2021 @03:05PM (#29203946)
I think that is a fair opinion and my life at work would be better if others shared that opinion. Go however does officially stake out the opposite position: coverage only "counts" if the test is in the same package as the unit under test.
Re:Twelve Years of Go by uncomputation on Wednesday November 10, 2021 @08:21PM (#29183330)
Go’s error handling is a strange this to praise considering it is entirely possible to ignore it without warning and the way to check error kinds was bolted on through type assertions when errors are supposed to be values in Go. Error kinds are important especially in network programming when the error can be dozens of different things. Compare this to something like Rust or Haskell which force you to handle errors and their variants.
Re:Twelve Years of Go by Thaxll on Thursday November 11, 2021 @09:41AM (#29188283)
Once again Rust does not force you to handle errors, you can ignore errors in Rust and it will compile. I can't say for Haskell but I doubt there is a language that forces you to handle error.

let _ = openFile()

Error ignored, it does compile, it will probably ends badly.

As for network error: https://pkg.go.dev/net#OpError

Re:Twelve Years of Go by Karrot_Kream on Thursday November 11, 2021 @12:15AM (#29184670)
Pretty much every form of Go tooling warns you when you ignore an error. If you only choose to run "go build", yes you can ignore it, but a simple "go vet" call and it'll be plain as day.
Re:Twelve Years of Go by kamaal on Thursday November 11, 2021 @01:32AM (#29185077)
Certain things need to be available out of the box.

The whole point of a programming language is to make the programmers life easier.

By now decades of exposure to programming languages have made try/catch the standard way of working with errors. Forcing the programmer to guess issues, when the compiler/runtime could be doing it- IMO is just plain waste of human labor. And for even medium to large applications is a pointless exercise.

Re:Twelve Years of Go by azth on Thursday November 11, 2021 @01:00AM (#29184909)

    a, err := foo() // oops, unhandled error
    err = bar()
    if err != nil { ... }

or

    fmt.Println("...") // oops, unhandled error
or more insidiously

    err1 := foo()
    if err1 != nil { ... }
    err2 := bar()
    if err2 != nil { return err1 } // oops
Re:Twelve Years of Go by Karrot_Kream on Thursday November 11, 2021 @01:25AM (#29185039)
In the first and the last case, it seems obvious to me where the error is. An obvious code smell caught in review. The second error, yeah, but lots of languages don't enforce error handling around printing to stdout, and even though Rust does in theory, in practice everyone just unwraps it and moves on with their day.
Re:Twelve Years of Go by azth on Thursday November 11, 2021 @01:47AM (#29185147)
> but lots of languages don't enforce error handling around printing to stdout

They do, they throw exceptions.

> In the first and the last case, it seems obvious to me where the error is

The last case is especially insidious. I've seen it in production several times now. Very easy to miss. Particularly when refactoring/copy pasting code (which you have to do a lot of in golang).

Re:Twelve Years of Go by ohwellhere on Wednesday November 10, 2021 @03:51PM (#29180585)
This is exactly my experience. Being forced to handle so many more errors showed me how much error handling I was inadvertently eliding in other languages!
Re:Twelve Years of Go by JoelMcCracken on Wednesday November 10, 2021 @03:54PM (#29180614)
I think this is for sure the first time I have ever heard someone say that Haskell makes it too easy to handle error cases.
Re:Twelve Years of Go by Karrot_Kream on Thursday November 11, 2021 @12:12AM (#29184649)
I've used Scala in production codebases and it was common to just "let the EitherTs handle the exceptions". What ended up usually happening was swallowed exceptions that nobody bothered to handle and log because it was much less idiomatic Scala/FP to actually log what happened underneath than it was to return the first error that happened by short circuiting the EitherTs in for/yields (Maybe and do notation, for Haskellers). So we'd get 5xxes and log lines like "row not found" and have absolutely no idea why.

I see a lot of the same issues in Rust code today FWIW. People just keep bubbling up errors and at the toplevel say "fuck it" and dump them, without _actually_ building a reasonable error chain to offer the programmer a story behind how the error happened. The fact is, error handling in any language is tedious. Whether you do it with a sum type, a product type, or Go's error values, either way there's going to be lots of verbosity and boilerplate.

Re:Twelve Years of Go by jerf on Wednesday November 10, 2021 @09:54PM (#29183936)
To be more precise, it is the language that makes error-as-values so easy that it actually strays over into "as easy to forget about errors as exception-based handling".

You can successfully build huge chains of code that are just blindly passing errors up without adding any context about where they happened or anything in a way that even Rust can't compete with.

Haskell mitigates this issue by also having one of the more clever ways of dealing with errors, if you start getting into monad transformers or some of the advanced stuff, so it's not all bad. But it definitely can make it so easy to "handle" errors that you just forget about them.

Re:Twelve Years of Go by initplus on Wednesday November 10, 2021 @03:46PM (#29180522)
I'm a huge go proponent, but I do think the lack of map/filter/find etc. is a big downside to the language. I know how to write for (if item == myItem...) or a for (if item > max...) but it feels like a colossal waste of time every single time I write one of these loops.

Go would benefit a lot more from some basic slice manipulation tools compared to features like generics that have actually made it into the language.

The error handling I don't find so frustrating - it's great that it explicitly marks at the call site which functions can potentially error. It's like a working version of an inverse noexcept from C++. I do wish they would take a leaf out of swifts book though, with the try keyword. In practice error handling flow in my go programs is identical to using exceptions, it would be nice to have some helpers to make this default less verbose without losing the ability to distinguish at the call site which calls are error-y.

Re:Twelve Years of Go by grey-area on Wednesday November 10, 2021 @04:05PM (#29180780)
I imagine they'll add map/filter/find after generics are in. It's pretty easy to define some slice types though which include those in the meantime, type Slice []string and add some functions then just use your new type for collections.
Re:Twelve Years of Go by ohCh6zos on Wednesday November 10, 2021 @07:37PM (#29182985)
I suspect but do not know that multiple return values in a function combined with the inability to write functions against multiple return types like (T,error) will limit the usefulness of traditional iterators in Go. I'd love to be proved wrong as they'd really clean up my codebase though.
Re:Twelve Years of Go by grey-area on Saturday November 13, 2021 @03:11AM (#29208269)
Depends what you're doing I guess, not everything needs to return an error, and errors can be accumulated and stored during certain operations and dealt with at the end.
Re:Twelve Years of Go by fpoling on Wednesday November 10, 2021 @02:56PM (#29179765)
Most perceived verboseness of Go comes not from the language or libraries but from the formater that does not allow to compress 3 lines of the error check down to single

if err != nil { return err }

Re:Twelve Years of Go by stouset on Wednesday November 10, 2021 @04:53PM (#29181436)
Worse,

    if res, err := actually_important_bits(...); err != nil {
        return nil, err
    }
The actually important bits are hidden in the middle of line noise. In the "common case" where `actually_important_bits` is just a simple function call it's not necessarily as bad, but the problem is when you have ten successive instances of this and one is slightly different. It's impossible to notice the important difference at a glance.

For an industry that is just starting to understand that code is read hundreds of times more often than it's written, golang fails at the few things we actually know for certain about what makes it easier to understand code at a glance.

Re:Twelve Years of Go by jrockway on Wednesday November 10, 2021 @07:03PM (#29182677)
If you're using `res`, then you aren't going to scope it to the `if` statement. That is maybe another problem, sometimes you have to do this:

    res, err := actuallyImportantBits(...)
    if err != nil { 
        return nil, fmt.Errorf("actually important bits: %w", err)
    }
And other times you have to do this:

   if err := actuallyImportantBits(...); err != nil {
       return nil, fmt.Errorf("actually important bits: %w", err)
   }
The problem here is that you really don't want "err" to leak to the outer scope, so the second case is preferable from an absolute reliability and least-surprise perspective... but you can only do that in certain arbitrary cases. I think it's a bit of a wart.

You can certainly handle "res" in an else block, or even write "err == nil" and handle it there, but that is surprsing. I would say it's simply not done, ever, but the Go codebase itself does it (src/go/parser/interface.go.ParseDir was the first example I found; but my search returned many screenfuls of candidates so there are probably more cases lurking in there).

The fact that you have a choice is not ideal, basically.

But, having actually important bits in if err := ...; err != nil {} blocks is not detrimental to readability. You will know how to read that after 5 minutes of reading any Go program.

Re:Twelve Years of Go by jerf on Wednesday November 10, 2021 @10:00PM (#29183985)
I almost never use that in my code, I almost always use

    res, err := ...
    if err != nil {
        return nil, fmt.Errorf("...: %w", err)
    }
for that exact reason. This doesn't scan any worse than any other text-based code. (Page in the people selling visual programming here.)
Re:Twelve Years of Go by Zababa on Wednesday November 10, 2021 @04:04PM (#29180763)
> if err != nil { return err }

I'm far from a Go expert, but I feel like this line is a bit pointless, especially if you write it a lot. At this point, why not just not handle the error, or panic?

Re:Twelve Years of Go by boloust on Wednesday November 10, 2021 @07:45PM (#29183041)
Because when you're writing a reusable, self-contained function, you often don't know enough of the context to "handle" the error.

Let's say you've got a function that reads from a file. If the file doesn't exist, what do you do? That entirely depends on the context the function is called in. If it's in a web server, you might return a 404. If it's a background task polling for some data, you might sleep for a while until the file is uploaded. Or you might want to create the file yourself.

Re:Twelve Years of Go by jhgb on Wednesday November 10, 2021 @10:56PM (#29184277)
Common Lispers in the back are excitedly waving their hands and whispering: "Restarts! Restarts!"
Re:Twelve Years of Go by closeparen on Wednesday November 10, 2021 @04:10PM (#29180860)
The community strongly discourages panic.
Re:Twelve Years of Go by Zababa on Wednesday November 10, 2021 @04:13PM (#29180904)
I think a panic is still better than a chain of if err != nil { return err } that bubbles up and does nothing. Of course the best solution would be proper error handling but not everyone does that (and it's not always obvious what to do).
Re:Twelve Years of Go by merb on Wednesday November 10, 2021 @04:49PM (#29181394)
and most of the time you can't handle it anyway. I mean consider you are having a database query and it fails because the connection error'd (network split). what to do know? restart the network switches and wait? of course not in http you will just print a 5xx err and hope it comes back. in go you need to bubble up these errors to your middleware and handle it there.
Re:Twelve Years of Go by Karrot_Kream on Thursday November 11, 2021 @12:07AM (#29184616)
If this is seriously what you do when something underneath you fails, then you aren't writing in the domain of software where errors really matter, and you're probably better off in a language that has a gigantic try/catch handler around everything. In that case you may as well just log (or not) and ignore the error in Go as well then.

At high scale, you want to perhaps try your query a few more times, backing off exponentially, maybe even round-robining (or more complicated load balancing) among different targets. Throwing up your hands and 5xx-ing a request that easily is not something you can afford at high scales of traffic where small outages happen _all the time_.

Re:Twelve Years of Go by merb on Tuesday November 23, 2021 @06:52PM (#29325184)
> At high scale

so basically you mean that this only applies to like 0.001% of all websites/apps.

thus it's stupid to do it. I mean even than you would have an error that you need to bubble up or circuit break it somwhere but probably not at this layer.

also KISS and MVP, never ever overcomplicate something when you do not know anything yet, so most people don't need to built for scale at day 1 and it also is stupid to do it.

Re:Twelve Years of Go by Karrot_Kream on Tuesday December 07, 2021 @03:55AM (#29470360)
You can get off-the-shelf libraries that do this for you with zero-effort sane defaults. Most of them let you tweak the settings. I'm talking about Python's `requests` and Rust's `reqwest` here btw, two of the most popular HTTP client libraries out there. Python's `urlopen3`, which `requests` uses, exposes options for backoff strategies. `aiohttp` has a lot of these parameters as well. Unfortunately Rust's equivalent, `hyper`, isn't nearly as ergonomic/easy to use.
Re:Twelve Years of Go by closeparen on Thursday November 11, 2021 @12:29PM (#29190584)
That’s something new handle as close as possible to the outbound client, which is several layers in from the inbound request. Much of it is in gRPC middlewares, so by the time a gRPC client returns an error to the gateway it’s already over.
Re:Twelve Years of Go by sangnoir on Wednesday November 10, 2021 @07:43PM (#29183025)
At the very least, you have to log that error.
Re:Twelve Years of Go by closeparen on Wednesday November 10, 2021 @10:12PM (#29184062)
Our coding convention (and I happen to agree) is that the outermost layer to touch the error is the one to log it. In this case the HTTP handler, or perhaps a gRPC middleware. You don't want to be logging errors as you propagate them, or the logs will show the same error at a bunch of different call sites vs. one complete picture of what happened.
Re:Twelve Years of Go by ohCh6zos on Wednesday November 10, 2021 @04:37PM (#29181233)
A huge issue with Go is ‘idiomatic’ Go.
Re:Twelve Years of Go by throwaway894345 on Wednesday November 10, 2021 @02:58PM (#29179796)
If this is the case, I think the fault here lies with flawed perception and not the formatter.
Re:Twelve Years of Go by fpoling on Wednesday November 10, 2021 @04:44PM (#29181336)
The formater in many cases essentially doubles the number of lines. That requires to scroll much more frequently than with more compact formatting.
Re:Twelve Years of Go by throwaway894345 on Wednesday November 10, 2021 @04:53PM (#29181440)
I understand, but I take issue with the implication that compression makes code more readable.
Re:Twelve Years of Go by pxue on Wednesday November 10, 2021 @03:50PM (#29180566)
Compressing this into 1 line provides absolutely nothing for the reader, in fact, it absolutely takes away reability.
Re:Twelve Years of Go by The_Colonel on Wednesday November 10, 2021 @03:54PM (#29180621)
Readability absolutely suffers when almost everything you see on your screen is boilerplate.
Re:Twelve Years of Go by thatswrong0 on Wednesday November 10, 2021 @04:17PM (#29180951)
Seriously. I see so many functions in my code see that consist of 1 line of thing I actually care about followed by 3 lines of boilerplate if err not nil… over and over again.

IMO if Go didn’t have its tooling, no one would care about it.

Re:Twelve Years of Go by pxue on Wednesday November 10, 2021 @06:17PM (#29182294)
i have the completely opposite take.

every if err != nil; return err let's me mentally draw a line in the sand and not worry about exception handling for code above that line. It lets me start fresh and restart my mental model with the line of codes below the error handling block.

it doesn't take years of writing Go to understand this, all you need is a open mind to how Go does things.

Re:Twelve Years of Go by thatswrong0 on Friday November 12, 2021 @04:36PM (#29204827)
> It lets me start fresh and restart my mental model with the line of codes below the error handling block.

My code is almost exactly:

err := doThing()

if err != nil {

  return err
}

err = doAnotherThing()

if err != nil {

  return err
}

etc.

There's no need for me to "start fresh". Each line of actual stuff doing might return an error which needs to be returned. That's it. It's a complete waste of space and inhibits readability to absolutely no benefit. And this is extremely common across our code base.