4 ms·
I'm in the same boat. Go's simplicity is initially refreshing, then a huge pain once you find yourself writing the same thing over and over again. A good examp
by lobster_johnson 11y ago
I'm in the same boat. Go's simplicity is initially refreshing, then a huge pain once you find yourself writing the same thing over and over again.
A good example is errors being values — which is a great idea. But then you realize every single function needs to be littered 1-10 cases of
if err != nil {
return nil, err
}
It's an extremely common pattern. It's tiring to write, over and over. Tiring to refactor, too: If you change a signature (to add an error, or add another return value, for example), every error check has to be updated.
Unfortunately, Go doesn't offer any abstractions that might allow you to avoid such boilerplate. Functions that return errors cannot ever be chained, for example: If you have "func bar() (MyStruct, error)", you cannot do "foo(bar())". You must always assign the result to an intermediate variable. You could embed the error in the MyStruct struct instead, but that goes against Go's grain.
I find myself wishing for some magic macro that is smart enough to extract a value and return the error for me. Something like:
try! value, err := bar()
...would expand to:
value, err := bar()
if err != nil {
return
}
I'm with you on upper/lower-case names. It results in schizophrenic-looking programs. Unless I'm writing a library, I tend to just export anything that isn't obviously an internal implementation detail, for consistency.
I'm also up in arms about ":=" assignment behaviour. I've run into several bugs caused by shadowing inside blocks. You have to be careful when refactoring so as not to cause shadowing issues; really, editors should have their syntax highlighting set to highlight ":=" in blinking bright yellow or something. It's so hard to miss.
It's also inconsistent with how you (or, at least, I) want it to behave. It will shadow existing variables by default if there is at least one new variable on the left-hand side, but that's the least conservative behaviour, and feels contrary to Go's strictness — a language, after all, that considers unused imports to be a compilation error. Recently I've started avoiding ":=" in favour of vars, just to avoid falling into that trap by accident.
To be fair, I love many aspects of Go: Compilation speed, relative performance, ease of concurrency, static strictness. But after working with Go for a while and being quite productive with it, I'm at the same time seriously pining for a better language to replace it.
- plexicle 11y agoI'm not an expert in Go, but can't you inline a statement before the conditional? value, err := bar(); err { // there was an error } I like how that looks, personally. Obviously YMMV.
- kornish 11y agoIt does look better, but `value` will only be accessible inside the block. That's a no-go if you want to perform actions with `value` below.
- lobster_johnson 11y agoActually, that wasn't really the point of my paste. But since you ask: Nearly. You can do this: if value, err := bar(); err != nil { return err } Note that this syntax declares "value" in the "if" scope. You can only access it inside the "then" block or in an "else" block: if value, err := bar(); err != nil { return err } else { log.Print(value) // this compiles } log.Print(value) // does not compile So if you need it later, you have to do: var value MyType var err error if value, err = bar(); err != nil { return err } Note that you cannot do this: var value MyType if value, err := bar(); err != nil { return err } This would, again, declare a new "value" that shadows the outer "value" inside the "if" scope. (Go would fail to compile it since it doesn't allow unused variables.)
- vruiz 11y ago> If you have "func bar() (MyStruct, error)", you cannot do "foo(bar())". You must always assign the result to an intermediate variable. You could embed the error in the MyStruct struct instead, but that goes against Go's grain. Chaining does work, but obviously the function needs the same signature as the return values, which limits things since most APIs don't accept errors as input. But within your code you can structure it to get error values for things you know you will be chaining. Or create wrappers to make your types chainable. http://play.golang.org/p/5LHXdJlfIM http://play.golang.org/p/5LHXdJlfIM
- lobster_johnson 11y agoI swear I tried that once, and that it didn't work. Thanks for educating me. Still, having to let every function take an error parameter isn't really an option, generally.
- vorg 11y ago> Or create wrappers to make your types chainable It can't always be done. A generic wrapper only works for functions that return values. It would be nice to have a wrapper that can take any function, e.g. foo in: package main import "fmt" func foo(a ...interface{})interface{}{ if len(a) == 0 { return nil } else if len(a) == 1 { return a[0] } else { return a } } func barbar() (a, b int) { return 1, 2 } func bar() (a int) { return 1 } func baz() { return } func main() { fmt.Println( foo(barbar()) ) // prints: [1, 2] fmt.Println( foo(bar()) ) // prints: 1 fmt.Println( foo(baz()) ) // compile error: baz() used as value } Instead of the compile error, it would be nice if foo accepted baz() as a argument, which would read the array length as 0 and here return nil.
- pconner 11y agoHave you seen this blog post by Rob Pike? https://blog.golang.org/errors-are-values https://blog.golang.org/errors-are-values
- matthewmacleod 11y agoThis is very worth reading – it does offer some insight into the design process. In practice, there's still a lot of boilerplate error handling, but treating errors as values does help.
- jonesb6 11y agoIf there a paradigm for handling multiple potential errors in a function? I'm usually stuck using: reply, err := redis.Givemethevalue(key) if err != nil { return err } thing, err2 := postgres.getById(reply) if err2 != nil { return err2 }
- lobster_johnson 11y agoYes: reply, err := redis.Givemethevalue(key) if err != nil { return err } thing, err := postgres.getById(reply) if err != nil { return err } Go allows you to assign with := if at least one of the left-hand-side variables is new.
- forgotAgain 11y agoI use var err error at the beginning of the function if needed.
- jgome 11y agoThis helps prevent variable shadowing when using :=, which is yet another weird feature of the language... BTW, if you really want to make sure you don't make mistakes, use the compiler tools, or, better yet, https://github.com/alecthomas/gometalinter https://github.com/alecthomas/gometalinter .
- brandonbloom 11y agoI tend to use structs like mini "dynamically" scoped namespaces for breaking up hairy tasks. Pattern looks like: struct fooer { x int blah string // ... more state err error } func Foo(x int) (string, error) { f := &fooer{x: x}; defer f.cleanup() f.stepOne() f.stepTwo() f.stepThree() return f.blah, f.err } func (f *fooer) stepOne() { // ... do stuff and maybe set f.err } func (f *fooer) stepTwo() { if f.err != nil { return } // ... do stuff and maybe set f.err } func (f *fooer) stepThree() { if f.err != nil { return } // ... do stuff and maybe set f.err; set f.blah } func (f *fooer) cleanup() { // ... } While this is initially a tad more verbose, it has some nice benefits. For one thing, you can easily fmt.Printf("%#v", f) to dump all the relevant state. However, it also tends to be more resilient to refactoring: - No method signatures to patch up. - Easier to non-local jump via return if you have some unusual control flow, etc. - You can easily find all references to `f.err` to verify your error handling. - The method names and step list take on a self-documenting quality. Another key insight I had when learning to tolerate Go: It's ok to do throw away work. You don't need to abort instantly on failure, you can just make sure code paths handle zero values or other invalid states more robustly. For example, I sometimes add a function like: func (f *fooer) fail(err error) { if f.err == nil { f.err = err } } And then just add sprinkle some f.fail() calls, make code handle nils, invalid states, etc, and make cleanup code idempotent. Seems to work out much more nicely than if err, return.
- czbond 11y agoI agree, but i create Fatal and not fatal functions. func checkErrPanic(err error) { if err != nil { panic(err) } } func checkErrNonFatal(err error) { if err != nil { fmt.Println(err) return } }
- mixmastamyk 11y agoIt's odd, they've written that they were influenced by Pascal, which is where the operator " := " comes from. But it doesn't work quite right in golang... In Pascal you would use := every time you wanted to assign a variable, and a single equals when testing, such as "if (x = 5) then...". It is slightly more like math than most languages, which I appreciated. But, golang only allows it on the first assignment, the reasoning perhaps that it is a declaration? I don't feel that this third way of doing it with no advantage was a benefit, choosing one C of Pascal rules would have been better and less to think about.
- tptacek 11y agoI felt very much the same way about Go's error handling; that, coupled with straight-line synchronous-style socket programming, made me feel like I was writing the same dumb C code I did when I was a teenager in the mid-90s. I've come to believe that slicker error handling is a bit of a false economy, at least compared to other "informal" languages like Java, Ruby, and Python (all bets are off if you want to compare Go to Haskell; I concede everything in advance there). I feel like a lot of Java and (especially) Python/Ruby code is easier to read... until it matures enough to handle errors properly, at which point its just as crudded up. It's much easier to start sketching out code in Ruby and wrapping everything in blanket try/rescue blocks. But that code always breaks and always gets trickier before it's ready to ship. Lots of people I've talked to have had the same experience coming off C, Ruby, and Python as I have: when the Go compiler stops spitting out errors, your program tends to work --- usually not perfectly, but still far better than a first-run Ruby or C program does! Also, when people think about Go error handling compared to Ruby and Python, they rarely factor in the density of unit tests required for the Ruby and Python code.
- lobster_johnson 11y agoI think history has absolutely shown that exceptions are not the best solution; they don't scale, as you point out. And I agree with Go that errors are values, and should be passed around as such. Go basically tells us that errors are as really important — they should be in your face and handled by the caller. And then it goes ahead and makes it a second-class citizen that is really awfully laborious to work with. Why not make error-handling a first-class construct? Most code needs to just pass the error on to the caller, for example. Go would benefit from an easy way to just "bounce" the error off to the caller. Something like my naive, hypothetical macro in my previous comment. Some sum-type-based solution like Rust's Result enum would also have made the whole problem pretty much moot.
- pcwalton 11y ago> Go basically tells us that errors are as really important — they should be in your face and handled by the caller. And then it goes ahead and makes it a second-class citizen that is really awfully laborious to work with. Why not make error-handling a first-class construct? Not only that, it's very easy to just dump errors on the floor, which is why Golang needs tools like errcheck. For an easy example of this in practice (os.Setenv returns an error): https://github.com/search?l=go&q=os.Setenv&type=Code&utf8=%E2%9C%93 https://github.com/search?l=go&q=os.Setenv&type=Code&utf8=%E...