In general, I found it a chore to maintain Go-based systems.
There's a lot to like about Go. But it doesn't seem pragmatic for the types of applications I see people using it for.
For example, the entire error thing is absurd. Anders Hejlsberg got this right many years ago: 9 out of 10 errors are "handled" by a central error handler (log + "sorry, try again"). You can do this with error values, but at best it's verbose. Maybe error values makes sense for system programming, I could certainly believe it makes more sense.
But, really, the main problem is the type system. I'm a huge fan of dynamic languages. I think they're so much more productive. This is especially true when your system is interacting with external data - like user input and a database. I did C# and Java for years. These interactions are, at best, painful with static languages. But they're downright infuriating in Go (poor reflection, no generics). I wrote a somewhat popular library to help deal with map[string]interface{} nonsense (2), but it's not enough.
When it comes to web/tcp stuff, I think OpenResty for any middleware-ish system and Elixir for everything else are far more productive and enjoyable ecosystems. We've been doing Elixir for about a year now, and I've never seen such a consensus on productivity and quality (everything else always had some issue: Ruby is slow, Node is single threaded, Java is verbose).
There's certainly more space than just that. Go might still be a good choice for CLI tools, and both OpenResty and Elixir have a distinct memory/threading model that precludes some use cases - but those are in the minority.
(1) - Nothing against Erlang model, but I do think it's more restrictive.
Why? I find when you're importing external data or user input, that's exactly where you want strong types as that's the most likely place unexpected values are going to be generated (e.g. unexpected null values, string instead of an array).
I do like Erlang's approach with hierarchical error domains ("processes" and "supervisor trees"), and I honestly think Go made a mistake here: The semi-recent introduction of Context acknowledges that a control hierarchy is present and an important part of a concurrent, distributed application and that there are aspects of this that need to flow through everything. The Context exists to permit a caller to call functions and launch goroutines that inherit deadlines, the ability to cancel their work, and so on. But this is a rather blunt and intrusive tool that could have been better solved by baking termination into the goroutine scheduling runtime. Of course, one reason goroutines cannot be forcibly terminated is because everything is mutable all over the place, but you could still have termination induce system-level errors that the goroutine would be forced to handle (or maybe you could safely induce a panic)... but that ship seems to have sailed a long time ago.
And yet a hierarchical approach similar to supervisor trees would have been awesome to have in Go. Managing hordes of complex, "crash only" goroutines in Go gets gnarly fast, and you tend to end up with your own ad-hoc type of supervisor trees that uses waitgroups and maps and what not to corral trees of composed workers to ensure they are all able to cancel, retry + back off, and time out appropriately. Even something like retry with backoff isn't part of the standard library, even though it's extremely core to distributed, concurrent programming.
I think I agree with Go's authors that exceptions are bad, but exceptions in an explicitly hierarchical, supervisor-type process model, where processes are "crash only", actually make sense. It means you tend to design your system into discrete parts consisting of dumb workers (that give up and crash on the smallest error) and slightly less dumb supervisors (that react to said crashes). It's not a panacea, but it's better than the loose, somewhat impoverished set of tools that Go gives you.
You are correct that this is how it's often done - 9 out of 10 errors probably ARE "handled" by a central error handler. But this is an incorrect approach if the goal is a reduction of catastrophic errors.
In the following paper, "Simple Testing Can Prevent Most Critical Failures" (OSDI '14). the authors discuss how some catastrophic errors occur in distributed systems that they test (Cassandra, HBase, Redis, HDFS, MapReduce).
http://www.eecg.toronto.edu/~yuan/papers/failure_analysis_os...
Notably section 4.1: Trivial Mistakes in Error Handlers:
"""
Figure 5 further breaks down the mistakes into three categories: (i) the error handler ignores explicit errors; (ii) the error handler over-catches an exception and aborts the system; and (iii) the error handler contains “TODO” or “FIXME” in the comment.
25% of the catastrophic failures were caused by ignoring explicit errors (an error handler that only logs the error is also considered as ignoring the error). For systems written in Java, the exceptions were all explicitly thrown, whereas in Redis they were system call error returns.
"""
I haven't used Go much but I personally much prefer Rust's error handling which appears to be largely similar.
* Unfortunately error handling sucks (no stacktrace by default? Let's go back to the days of peppering print statements around just for fun).
* DB support is terrible (I've spent hours doing what I could have done in half an hour with python).
* It's so verbose, even simple things like mapping over an array to create another array requires a for-loop dance. Same for just checking if a value is in an array. Same, obviously for no exceptions which you either love or hate. I hate.
I can't see myself doing any other projects with it.
Please, nobody write full fledged WebApps or all of your middleware in lua. Trust me that's a horrible idea. I have seen about everything you can do in lua/nginx and it does not turn out pretty.
This hasn't been my experience. I work in Python (and JS) by day, and I can't wait until we upgrade to 3.6 so I can reasonably employ type annotations. Without them, people tend to write hacky code, such as functions which do completely different things based on the data type passed in (rather than fixing the abstraction) or build up huge, nested data structures with no formal definition (various parts of the code base just tack on whatever they like, and the documentation inevitably falls out of date). Even my prototyping is in Go, because I would rather have a compiler catch my errors than to spend tenfold the time trying to debug. Also, when I'm working in Python, I sorely miss the tooling that I have in Go and other static languages.
I don't think Go is the end-all-be-all of programming languages, but I think any dynamic language would be more productive if it was at least optionally typed.
You can still utilize all of your cores in Nodejs. So which problem are you encountering?
My opinion is the opposite. Almost every go code base I've seen is extremely consistent compared to other languages. Going from one code base to another is almost always seamless because learning Go involves learning the Go tools which forces you to follow Go coding standards. I'd choose to work on a Go code base over Java or C++ any day of the week.
I don't have much experience with elixir, but that's because I like to stay employed.
>the entire error thing is absurd. Anders Hejlsberg got this right many years ago: 9 out of 10 errors are "handled" by a central error handler
This is almost always unacceptable in production, especially when it comes to mission critical hard/software. You need to be able to handle errors in production that you may not even be able to catch in testing and that means you need to catch all the errors and think critically about what should be done when that error is encountered. "Central error checkers" are almost never robust enough for mission critical systems.
Go basically forces people to write high-quality, resilient code bases, and I like it for that.
Actually C++ doesn't have immutable data unless you count compile-time constants and literals. You can declare things 'const', but that only provides a read-only (1) view on mutable data.
Also, to provide good 'const' support in containers, you usually have to provide extra read-only iterators that are largely identical to the writeable iterators. Go look at the C++ standard library. All the containers have both 'iterator' and 'const_iterator' (not just a 'const iterator').
Another approach is to just declare member variables 'const'. So you have a 'const map<int, int>' as a member variable. Except for that to work, you need to construct that member variable in the initializer list. The body of the constructor is too late. This likely means taking an already-initialized map as a constructor argument and copying it (probably exposing implementation details in the process) or writing a helper that returns the appropriate map type and copying it. Good optimizers will elide many or all of these copies, but relying on quality of optimizers isn't portable.
Anyway, I'd say the complexity of the Go approach isn't significantly worse that the C++ alternatives, if it's worse at all.
(1) Of course, with casting shenanigans or use of the 'mutable' keyword, anyone can circumvent 'const' without a lot of work.
const std::set<int>& s // This is a read-only view.
const std::set<int> s // This is immutable.
> Another approach is to just declare member variables 'const'. So you have a 'const map<int, int>' as a member variable. Except for that to work, you need to construct that member variable in the initializer list. The body of the constructor is too late. This likely means taking an already-initialized map as a constructor argument and copying it (probably exposing implementation details in the process) or writing a helper that returns the appropriate map type and copying it. Good optimizers will elide many or all of these copies, but relying on quality of optimizers isn't portable. Foo() : s(helper()) {} // This is a move, not a copy. const std::map<int, int> constMap = { {1,1}, {2,2 } };
Or take a const map copy-constructed from from a non-const map, if you wish.How is this not immutable data, and "only provides a read-only view"?
It is linguistically guaranteed to be non-mutating - const-casts excepted of course.
All languages that are constructing const data types on the fly are putting them in a writeable pages in memory, after all.
Ended up with this. [0]
Turns out the Go authors don't think you should do this so I had to majorly alter and recompile the stdlib.
I still appreciate many go tools and the cross platform single binaries that work great on Solaris/Illumos. But in many cases the strong opinions of Go authors really stand in the way of getting work done.
I left it a while back, with a lot of the same frustrations. But then I tried a bunch of other things, and nothing seemed to work better. There seem to always be some frustrations with whatever language. And I found myself craving that simplicity again.
I might get annoyed with it again, and try some other stuff. And that might stick. But I suspect Go is the worst language except for all the others [1].
[1] http://www.goodreads.com/quotes/267224-democracy-is-the-wors...
In retrospect however, I was frustrated with these languages because I was wanting it to be something it wasn't. To my mind, the loss of that feature meant that programs were less expressive - particularly in large codebases - I had to read methods to work out if something was being modified or not.
To complain here about Go lacking immutability and constness features seems to be the same as complaining that it is missing in Python, Ruby or JavaScript. It's a fact that constness isn't a feature of these languages, if you want these features, use a different language.
If that's enough for you to consider that a language provides a feature, just wrap the immutable ordered map implementation in your blog post in a library, publish it, and now Go provides that functionality too.
I don't know if there is a term for Go like programming. I'm tempted to call it minimalism. It may be uncomfortable for people coming from classical programming, but the simplicity of the language and the code is a clear win in the long run.
Now it seems everyone is bashing Go. Moral of the story; never buy into the hype whether it is positive or negative. I think that Go is just fine and so is Node.js.
Maybe the author of the article makes a valid point, but no language is perfect and ultimately you have to understand it well in order to use it well.
It seems a lot of these articles, and programming language theory in general, is just complaining about features without any thought as to why they matter. What is this "problem" stopping him from doing? Why does it matter? And why should I abandon a language that has served me very well just because some edge-case feature is not supported?
As noted in the post, generics could solve this, so I'm looking forward to Go 2 if indeed generics is included. There's a few QOL issues I'd like to see addressed in 2 which would make Go a lot nicer to work with, and I say that as someone who spends most of their time working on Go codebases.
Might as well not pretend something is an ORM if it can't do the fundamentals (like upserts), fully hydrating structs, etc.
Java doesn't do that well at all though; its immutable maps can be used in place of mutable maps, since there's no way of having a Map interface that extends ImmutableMap to add extra operations, so you can get nasty runtime surprises if you've got the wrong one. Conversely, if you want a mutable map keyed on a non-trivial structure, you have to do just as much boilerplate as the author complains about to set up private members, setters, builders, etc.
Obviously C++ can do it quite readily, but nobody accused it of lacking features.
This is really just another use case for generics, and the discussion there is well-trodden. Go probably isn't the language for you if you value generics more highly than fast compile times, native binaries, etc, but I'm not sure why that still seems to come as a surprise.
I often read criticism of Go which is based on a missing "feature". In my experience, implementing these features is surprisingly easy in Go, as the building blocks are all there. In this respect, Go has some resemblance on Lego :).
My background contains a lot of Scheme/Lisp, so abstracting things with functions comes natural to me. It turns out that Go has basically the equivalent function concept (first class functions, closures, ..) so enables all the powerful abstractions coming from them.
Guess it is due to a different architecture and design which makes the client faster (e.g. chaindata is smaller) but does anyone know why they chose Rust instead of Go for the Parity client?
Where is the list of problems that are easier to solve with generics?
Languages are like tools. If you need a saw, use a saw, instead of complaining about the knife you are using.
24 lines, really ?
"Oh, you think you want an ordered map? No you don't. Your reasons are stupid. You're stupid. Now gtfo and start using Go to write things we deem to be respectable."
Most sorted map implementations are based on trees.
If you just want a very temporary ordered list you could probably use a channel.