f, err := os.Open(filename)
if err != nil {
return …, err
}
is simplified to: f := try(os.Open(filename))
> I dislike the try implementationWhat would you suggest instead?
io.Reader is an example where the function can return io.EOF and still returns valid data together with a count in its first return value. If it returns a count > 0, the buffer has valid data. A lot of Go projects get this wrong because it's not obvious -- it goes against the behaviour of almost all Go functions.
Go is unfortunately full of little surprising warts like this where you have to tread super carefully. Nil channels is one of my favorites.
Down-vote if you want, but Go modules package management was handled in the same way. It's not without precedent.
Basically I agree that the process for modules may not have been ideal, but the outcome was pretty good. We could do a lot worse than getting the same again.
Dep was made by one of the authors of Glide, which coincidentally suffered from many of the exact same bugs, maybe because it reused some of the same code. Our entire team constantly had "dep ensure" failing in unpredictable ways (locally or on the CI server), usually caused by the solver not understanding something. These failures were completely incomprehensible and not fixable by the user. Dep's solver was also very slow.
From what I could tell, it was a combination of shoddy engineering and trying to do too much; if I remember correctly, Dep and Glide both tried to automatically detect and convert dependencies that used competing tools like Godep, which didn't work very well. For a long time, Dep didn't work at all with the Kubernetes client library, for example.
We've had almost zero problems with Go modules. I've encountered some minor bugs, but unlike Dep, nothing worth tearing my hair out.
The drama around Dep vs. Russ Cox shouldn't have happened, of course.
I have found the package management story around go so confusing its really stopped me from trying to use the language.
For context, I'm used to how composer/npm install the modules into a folder locally. I just can't seem to figure the go way of doing modules where I don't get completely confused.
This article looks like a good intro: https://roberto.selbach.ca/intro-to-go-modules/
The equivalent of "npm install" is "go get" (optionally "-u"). You can also edit go.mod manually. Like NPM, this must be done within an application's root.
A point of confusion might be the difference between a module and a package. A package is just a folder that declares "package foo" at the top in all its Go files. A module is the closest analogue to an NPM package. Similar to NPM, a single Git repo can contain many nested modules. Unlike NPM, modules can be imported with Git -- no need to publish to a special registry.
When a Go file imports a package, the referenced package might live in a module outside your own. It knows it's outside your module because go.mod defines the full root path (e.g. github.com/foo/bar/baz) of your module.
You might experience some confusion when you look at how the new module system interacts with the old GOPATH way. Go current supports both modes.
Likewise, the community doesn't have any say in how try works, and that ticket asking for feedback is window dressing.
You can tell that this is window dressing by looking at the timestamps on when that ticket was opened, comparing it to when this blog post was released, and then considering how the hundreds of comments on that ticket were completely ignored.
Dep was pretty good, and there was absolutely the possibility of just making that official (and maybe tidying away a few of the points raised during the module discussion). But no, that wasn't good enough.
Dave Cheney's errors package (pkg/errors) is a great contribution to Go error handling, used by a lot of gophers. Adopting that would have been simple, backward-compatible, and given us better error handling. But no, that wasn't good enough.
I'm so used to "if err != nil" that it's punctuation for me. Having some code use "try" because it doesn't need to be wrapped, and other code use "if err != nil" is going to break that. I'll have to actually read the error handler now, in case it does something unexpected.
Or not use "try" and stick with errors.Wrap. That seems way more sensible to me.
it's not democracy, but they'd be stupid to just ignore all that expertise.
Sure
Semantic import version and the rush of shit to the head in their attempts to break vendoring on the other hand.
I think I can live with it.
This is the C# 9.0-ification of a language. Let's add more constructs because we're compiler developers and that's what we do.
I'm saying this as someone who has helped a large organization of primarily Python programmers onboard with Go. Proper error handling is already something people don't do well (until we enforced lint rules throughout the company, there were already way too many `f, _ := os.Open(...)` types of error dropping).
I'd much prefer something more strongly typed with respect to error handling, rather than less. Like if Java had disallowed extending RuntimeException so all exceptions would be part of function signatures (and therefore have to be caught).
f, err := os.Open(filename)
throw err
This allows alternate signatures easily. I also think the syntax makes a little more sense, as you're "throwing" the error up the chain unceremoniously.f := try os.Open(filename)
Or perhaps better, leaving the working code alone and concentrating on the bits people actually don't like (the error handling boilerplate).
f, err := os.Open(filename)
check(err)
There is no need to attempt to put the error handling on the same line as the function, and if you do it just gets in the way of reading the actual function call, permits nesting etc. This approach would have the advantage of leaving normal code alone and only touching the error handling.
I'd even be fine with check as a new builtin function, since it would typically sit alone anyway, I don't think it needs to be a keyword, whereas with try I think all the brackets will make it harder to read.
In fact I've just found a variant of this proposed here:
(Though I guess it's a bit harder because you'd have to have your whole team not use it...)
"don't use it" is definitely an option, but it's nowhere near "it doesn't exist". It's opting into a constant struggle against the tide.
At the same time we are surviving the type aliases quite fine (who has ever seen them used? Not me...)
You night not have used them directly but you might have benefited indirectly from type aliases. For example, when the "context" package has been moved to the stdlib aliases allowed existing libraries that still used the old "x" context package to be used together with code that used the new package.
Implement exceptions. I'm honestly at a loss as to why the Go community hates them so much.
* Exceptions create a type of non locality that is hard to reason about.
In a language with exceptions every call to a function might result on the calling function exiting. Flow control might jump up serval stack frames unexpectedly. This works against the principles of structured programing and the normal expectation that a function when called will return a value.
This means the programer, the compiler and the runtime environment must always take this into account. This can create unexpected behaviour and creates a whole host of special cases for the compiler and runtime.
* Uncaught exceptions cause crashes
Even when the error is not something that would cause a problem if an exception isn't caught the process will crash.
* Exceptions are often too heavy for what they do.
In many situations an error is an expected/frequent outcome. Consider a disk based cache of some sort. The cache would attempt to open a file containing th cached result and if the cached result was not found then it would then do the processing to create that result.
In this case in most typical exception systems the exception would propagate up the call stack to the exception handler capturing the stack in the process. Once handler is reached all this work is thrown away.
* Exceptions create bad error messages
Too often I've seen a buggy java process spew stack traces into the logs where a single line error message saying what happened would have made the operator's 2am dealing with emergency much more manageable.
Oh, and by the way agreeing on what an exception IS brings whole another dimension into this discussion too.
So, I would go as far as "Go community hates them". People have asked for them more than once. No, we did not get them.
It doesn't encourage skipping error handling, it just allows skipping annotation where it doesn't add much. Sometimes in a chain of several calls, a function in the middle can't really add much context, and is better just to bubble the error up one level without annotation - that is what try is for.
I disagree that linters should or will recommend it except in cases where there is no annotation and there is therefore no functional difference in using try.
There is a reason Go doesn't have compiler warnings.
These tools are so common and so normal that while they aren't part of the compiler, they're part of the "social" toolchain.
So, it's odd that the author doesn't use a linter when the general tendency is to use a linter, a licensing checker (glice), a security scanner (gosec), a formatter (fmt), a static analysis tool (go vet), etc in your CI/CD pipeline.
It's so odd that it begs the question - does this author avoid common tooling? And by extension, do I care what they think about how Go should be written if they aren't using the standard tools?
Even if the try proposal were to result in eventual go vet errors, then surely go fix would be able to rewrite on your behalf, so this would be a non-issue, just as go fmt is a non-issue. But I don't think this will happen any time soon.
> everything that is `return nil, err` will need a special comment to silence recommendations
I don't use linters that can be ignored with special comments. I don't recommend them. This is a problem in other languages/ecosystems, but in my experience it is not a problem in Go, where it's considered an anti-pattern. This feels like FUD to me.
> nagging social pressure to reduce linter warnings will result in skipped error handling.
Yes and no? Social pressure to reduce linter warnings is a good thing, why use a linter if you are going to ignore its output. This problem only exists if you have linter warnings.
> a licensing checker (glice)
I've never encountered this. I don't think a linter that hits the Github API is a good idea.
> a security scanner (gosec)
This looks like a pretty opinionated linter. Some example rules:
> G103: Audit the use of unsafe block
> G104: Audit errors not checked
> G201: SQL query construction using format string
> G105: Audit the use of math/big.Int.Exp
> G501: Import blacklist: crypto/md5
Are these actionable? Probably not. There are perfectly valid reasons to do all of these things. Maybe this is useful to paint a picture of a code base you're evaluating, or to flag new changes as suspect, but I don't know what purpose this would serve in CI.
Don't use linters which just create noise and non-actionable output. They're not valuable, they're a waste of energy. Make them manual, or maybe run them against new changes only with the expectation that they're informative but often ignored.
These are all personal problems. Most Go users don't have these, I suggest you choose to not have them also.
I don't think applicative/monadic error handling is an afterthought in any way. There are combinators to recover, for instance. But it's optimized in such a way that you can focus on the happy-path of your code and you get a sensible default for error-handling (short-circuit..just like Go's idiom!)
It achieves what a real monadic Either / Result implementation would achieve in error handling, it stays simple, and it's backwards compatible.
While I'm not a fan of Go in general, I can't help but admire at achieving all this so simply and cleanly (as much as the underlying return-two-values thing can be considered clean).