Some Rookie Mistakes in Go
engineroom.teamwork.com
engineroom.teamwork.com
This is one of the problems I have with C. You basically have to remember to manually check for an error on every function call, which means (1) an algorithm with short and simple pseudocode becomes lengthy and hard to read real code, because you need to add a ton of error handling, (2) it is easy to forget to handle an error in some place. Exception-based error handling lets you delegate errors to a top-level handler without cluttering up intermediate code (i.e. an exception anywhere in webpage rendering logic should be handled by 500'ing the entire page, which exception-based error handling lets you do without cluttering intermediate functions between the function producing the error and the function handling the error with error-handling logic).
I've been thinking about learning Go, but if its only error handling mechanisms are "pack it into a return value" or "panic() which is like exit(1) in C" then it's a black mark against the language. It's still a black mark if the language has exception-like error handling but it's not considered "idiomatic Go" and the stdlib and a good chunk of third-party libs report errors in return values.
To "miss the known universe" was the explicit purpose of the designers. It was supposed to be simple (whether their definition is good or not is debatable). The number of users is irrelevant.
For example, you get a nice operator for appending to a string, but not a slice. A zero-valued slice is empty, a zero-valued map is nil.
This isn't to say that Go is bad, it's just that its tradeoffs aren't in service of any big unifying vision. When range can only iterate over a slice, map, string, or channel, there's no forest. It's just four trees.
Go's error model doesn't differ fundamentally from that of C. The built-in unused value error helps a lot by catching a sizable subset of cases, though; Go's designers made the right call there.
It uses error values like in Go, but you must prefix any function which can throw with a try statement or it fails with a compile time error. That way you never forget to handle it, but it's not really an exception either.
Explicit control flow is more verbose, but it is also easy to spot when the control flow passes over something important by mistake. And as pointed out elsewhere, forgetting to check a return value is a solved problem via static analysis.
Go doesn't force you to deal with errors either. I see plenty of code ignoring them with
retval , _ := MightYieldAnError();
And some languages that have exceptions actually force the client code to handle them, like Java AFAIK .Finally, Go "explicit control flow" didn't have to be verbose like it is, that's a design choice its authors made.
Go is primitive in the sense that it uses C style code patterns , like the famous :
retval = MightYieldAnError( &outError);
Edit: I forgot to mention the panic/recover which ultimately means Go designers implemented some form of exceptions. That basically voids the usefulness of Go error system, and an admission that it's broken.> And some languages that have exceptions actually force the client code to handle them, like Java AFAIK.
But this misses the point. Any error which you have to explicitly ignore is fine, because then you were aware of it at some point and explicitly ignored it, and anyone reading the code can plainly see you explicitly ignoring them.
And errors which can only happen at specific points (like function return values) are also ok, because then you only have to consider error handling and cleanup at those points.
But if you have neither of the above - if you have the situation where errors can spontaneously result from any evaluation of any expression with no explicit notation required - then you have a poor situation indeed.
So in Go you have the worst of both worlds. Constant error handling code everywhere, AND you have to treat every single expression as if it may suddenly throw panics.
Oh and to make matters even worse, Go has typed nil, which is not equal to untyped nil. So you cannot correctly check interfaces against nil, the most common cause of panics in my experience.
// t is some kind of interface if (t == nil) { // t is untyped nil } else { // t is non-nil OR a typed nil }
I'm not even sure what the correct fix is. Should every interface come with a isValid() method to check if it's usable ?
This means there's no implicit control flow. You can always see where a function might throw, or know that a function will never throw. It also means, performance-wise, the implementation is much more efficient than C++-style stack unwinding.
I've not used this in practice, because I've not tried Swift, but I think it's a good approach.
https://developer.apple.com/library/prerelease/ios/documenta...
Ret, ok = Something(things),
There's no need for an "if" here, you've told it to handle a failure.
Rust is for the lowest level code which is not assembly, with all the performance tricks, but that should be secure.
Rust is expensive to develop - if you can afford to use a GC, you should use a GC.
So people want to rewrite all the world's C and C++ to rust, for security. But people are less passionate about rewriting all the world's python into go for performance.
This has not been the case for many people using Rust, including myself. Once you learn the language, there is no "cost" to use it. In fact, the compiler simplifies much of the mental overhead when dealing with concurrency, sharing data, mutability, etc... Sure the compiler has some strict rules, but it's not expensive to develop in by any means.
I would also say Rust is in a sense higher-level than Go in terms of expressiveness and abstractions.
I like GCs, but I wouldn't make such a blanket statement. Manual memory management as implemented in Rust also eliminates data races at compile time, which are in fact the last item listed in the original article.
n.b.: While I have seen very, very competent programmers writing good software in Golang despite the language's considerable efforts to thwart them, and doing so in a product-focused environment, my intuition, as an outsider (which I disclaim because, as somebody whose job is to build systems that do not fail, the worse-is-better ethos of the Golang community scares the hell out of me), is that there's not as much deeper discussion to be had when you are of a generally product-focused mode; many things are unspoken and many others are very specific to your tiny slice of the world--is there as much to say, publicly, about that stuff? And are the other loud people in that community even listening when you do?
Rust seems instead to attract immigrants from what I consider better programming environments; not necessarily better business environments (almost certainly not, in the cases of people I personally know) but environments that are more welcoming and enabling of building hard stuff. The Rust people I know are migrants from heavy-hitter C++ backgrounds, functional programming backgrounds (the biggest fan I know personally previously lived and breathed OCaml), and a few from the deep end of the Ruby community. Thus to me it follows that the interesting stuff that leaks out into the general public is going to be more esoteric to many outside of it (and maybe, for you personally but certainly for me, more interesting).
It also probably helps that the Rust APIs are really smartly designed and it seems--though I am a Rust novice, and this is a personal take--much harder to shoot yourself in the foot. There may just be less need for "these things work but will secretly ruin your day" in general.
A single language that can demonstrate this continuum pretty well is actually Java, I think--you see tons upon tons of "here's why you don't use == in Java"-level posts, but you also see stuff like Aleksey Shipilev absolutely throwing down useful and actionable stuff about the JMM. Java as a community is fragmentary and huge, and there's more space for, and more need for, discussion at both ends of things.
Or maybe that's the result of a scarcity of those kinds of resources. Hard to say.
That's my excuse, anyway.
If you start out on Rust, the first thing you hit is the borrow checker. There are a billion articles on that, but the questions there seem less novice because the problem is harder.
And then when you have gotten to the point you need best practices in error handling, Rust greets you with this:
https://doc.rust-lang.org/book/error-handling.html
Go has easier problems because it's an easier language. I say this as someone who loves Rust.
Rust is almost entirely at the rocket scientist end of the market.
We've tried with Revel, and I completely agree with the post here. We could use net/http directly, but gin is working super well for us right now.
package main
func test(n int) { for { n := n - 1; if n == 0 { break } } }
func main() { test(1); test(2) }
It definitely isn't ideal, but it can be dealt with. I believe Rob Pike had called out that the number of ways you can do variable assignment/declaration is something he disliked, but it's impossible to change in Go 1 at this point.
IMO, allowing it is the lesser evil.
Otherwise you're right - but all languages have sharp edges.
I'm using Go on my current project and I've gotten a lot further a lot more quickly and with better performance and reliability than I would have using Django, Rails, or Node.
Because I'm not in a hurry I thought it would be fun to write a simple custom server/router/middleware bundle around net/http without using any 3rd party packages. From a cold start it all turned out much easier than expected, and so far the whole thing is under 500 loc.
Being careful around a few sharp edges seems like a small price to pay.
I really think good web frameworks is one of the reasons why the web has progressed so quickly the last ten years.
The HTTP body is provided as an "io.Reader" which actually backs to the network stream, and since TCP streams can't be seeked, it's an error to think you can. This means that native Go HTTP handlers can handle a multi-gigabyte upload or download without consuming all that RAM at once (assuming there is some way to stream), but you lose seeking. You can easily recover read-only access by using io.ReadFull to fully consume a reader into a byte buffer, but then you pay the price of consuming all the RAM, of course. (https://golang.org/pkg/io/#ReadFull ) Whether or not it's a mistake depends on your situation. Fortunately, in the HTTP context, you have a Content-Length that you can examine to make decisions if you need to; the generalized io.Reader does not have that, either.
This all comes from the nature of HTTP itself. I tend to consider it a basic requirement for any web development environment that there be a way to correctly deal with an HTTP upload as a stream, and that the environment not "helpfully" unconditionally load the entire stream into memory for me.
Responses can be huge. Why would the library commit to storing them at all, and determine the data type to store them in?
Yes, you could make an API that allows the library user to control that, but that would be needlessly complex compared to having a single call "give me the response stream", and letting the caller of that decide how much of the data to read, and what to do with it.
As of go 1.5, its supported by the language itself: https://github.com/golang/go/wiki/PackageManagementTools
On a more serious note, I think that's just human nature - if you're invested in something - a technology, a stack, a business model, a car - you value that investment more than its worth, because it is yours. I think there's also an element of "I put in the effort to learn this, so damnit I'm going to use it".
Or just people like challenges.