232 comments

[ 2.9 ms ] story [ 247 ms ] thread
Go contains many good ideas that may be carried over to more pragmatic languages. Using it for anything beyond simple, self-contained command-line tools and network servers never really worked out for me. And that makes sense, since it was explicitly designed to scale over groups of inexperienced developers at any cost; the opposite of my needs. Modern C++ runs circles around Go for more complex software, and it provides the tools to add as much convenience as needed on a case-by-case basis. I recently ported a 7kloc Go codebase to C++ and wrote down some of my impressions here: https://github.com/andreas-gone-wild/blog/blob/master/rewrit...
Your blog entry you're referring to contains surprisingly little information to support the arguments mentioned in your posts. Maybe if you provided some specific example of why a given fragment of code works better in C++ rather than Go, we could have some interesting discussion.
I can't take responsibility for your expectations, differences between languages and their consequences for the code we write is a tricky thing to quantify. I have no proof and I'm not here to argue, only to share my authentic experience in the hope that it helps someone else. Both codebases are clearly linked from the post for comparison. Feel free to ignore my advice if Go is working out for you.
So... Go is excellent to write the software it was designed to write and it's no good at other things.
Which is fine if you happen know what it was designed for prior to starting your project, and can also be 100% sure your project will never mutate into anything else...
Worth reading the comments where someone points out a much simpler and more elegant solution, although it's still not an ideal situation.

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.

> Worth reading the comments where someone points out a much simpler and more elegant solution, although it's still not an ideal situation.

I'm always wary of such "<language X> isn't great because..." posts as I find that the frustration is as much a function of the developer's competence, (lack of) imagination and familiarity with the language and its idioms, as it is with the language itself.

Generics don't solve immutability.
No, Generics make it possible to create a generic immutable data structure which can be used instead of re-writing the same boilerplate for every data type, which is the authors complaint.
You are still talking about immutable data structures, which has nothing to do with generics.
No, YOU are still talking about immutable data structures, where as I am talking about the need to write boilerplate code in Go due to lack of Generics.

The blog posts topic was not "immutable data structures", rather the need for boilerplate code in Go because of the lack of generics. The particular example was one of immutable data structures, however that was an example rather than the topic. The author themselves notes that "generics would solve all of the issues above".

The comment i replied to :

> No, Generics make it possible to create a generic immutable data structure

I am sure i can read.

That was a reply to "Generics don't solve immutability" which was a reply to a post that did not contain the word "immutable" but does contain the word "generic"

James' comment you quote was responding to your comment, which he parsed generically as "Generics don't solve T", with a comment of the form, "Generics make it possible to implement a generic T."

I'm confident you can read, but I'm not so sure about your reading comprehension and critical thinking.

I'm confident that you have no clue what you are talking about.
What is the perfect language?
Let's just say it can't have a type-system limited to not understanding generics.
So, once upon a time I wanted to associate information with http connections (as I am accustomed to in every other language I've ever written) just to enable proper http keepalive and debugging through a proxy written in Go.

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.

[0]: https://github.com/doublerebel/publictransport

That's more a criticism of the standard HTTP server, which is a bit of a black box, apparently intentionally. There's nothing wrong with writing your own server to make up for it's limitations.
except for the fact that you shouldn't have to write your own server?
But that depends on what you're trying to accomplish. It's intended as a high-level, general-purpose server. It will always lack features if you go advanced enough.
Why not? Plenty of languages don't come with one built in at all, or not one widely used in production (e.g. Python's http.server, there are a heap of other Python web servers out there).
They count 24 lines of code as "infuriating amount of code to write". Why do you think they would interested in writing a web server ?
To be fair, 24 lines for something you know should be doable in only a few (or one!) is pretty darn frustrating.
I was counting less the 50 lines as "few lines".
when you set out to solve a particular problem, do you really think its a worthwhile use of your time and effort to create a webserver? Mind you i'm not talking so much about "built-in" webservers than I am about reusing an existing, battle tested one from a 3rd party (e.g. OSS) so you can focus on the actual problem your trying to resolve.
If the problem I'm trying to solve is "I want a better webserver", then yes :) That's a little tongue-in-cheek, but I assume is more or less the motivation behind all web servers in use today; otherwise we'd all be using httpd or something.
Yes, if you want to build a better webserver, go right ahead. But I think we're in agreement, that you shouldn't be reinventing wheels that you are not purposefully trying to reinvent. If we always started from scratch, nobody would get anything done.
Seems like you got work done by altering the stdlib because it was approachable enough to alter. I don't see what the complaint is here.
If you actually take a look at the many changes that had to be made due to the uncreative limitations imposed by Go, you'll see that it was was neither approachable nor maintainable. I'm just stubborn and found it hard to believe it would actually take that much work until I tried it.

This is supported by the fact that contexts have been added, and I won't be surprised to see Go reverse more of their decisions as the language grows and devs prioritize shipping over Go dogma.

I personally tend to choose Go for newer projects as I'm looking for the efficiency of C and safety/elegance of more "modern" languages. However, I always keep an eye on D and Nim. It seems these are the only two languages that can compete with Go in both above aspects.
Go is not in the league of C for efficiency though? Very different kinds of languages...
In my day to day work, I find Go very much in the league of efficiency of C, if, for robust and real programs, not beyond. Low level Go code compiles the same as C code does (yes, C compilers have better optimizers), you have sometimes to pay for some overhead (bounds checks, GC), but that often pales compared doing this by hand in C, when you need a reliable and robust application.
Go is nowhere near the speed of C. But why not consider Rust?
The Rust team decided to promote Rust as a "systems programming" language. And even though it can obviously do much more, and the bindings for most popular libraries are available [1], this label somehow attracts programmers who are more systems oriented and I think the language will continue to develop in this way, whereas Go/D/Nim will tend to try to be more general. [1] Imagine how surprised I was the other day to find Rust bindings to Allegro, a venerable game programming library from the old days of DOS and DJGPP...
Rust is getting to be popular for graphics and games, precisely because it has the performance of, and easy interoperability with, C, without GC, and in a much more modern environment.

Low memory requirements means portability to memory-constrained platforms. Interoperability with C presumably also implies easier access to low-level resources such as GPUs, and being able to take advantage of existing libraries for C, such as SDL.

People are also experimenting with Go in the same space, but it seems much less promising. Go is notoriously slow at calling C (or being called from C) due to goroutine context save/restore. It also has GC and a runtime.

I don't see why it's surprising to see a game programming library binding for Rust. I was drawn to it because it was advertised as a "systems programming" language. From a graphics perspective, this means that it's trying to be fast and gives me good control over low level stuff.
I've tried to get into Rust but I've found the syntax alone to be impenetrable despite reading through several guides multiple times.

If Rust was less complicated I might consider it.

Nim is among the fastest, more high level and people seem to like the Python-like syntax.
OT: I recently learned that the newer ETH client Parity is written in Rust and much faster than the older Go-based Geth ETH client.

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?

I don't know specifically why that is for this specific project, but in general Go has GC and bounds checking that slow things down.
Rust bounds-checks indexes by default (there exist indexing methods which bypass that, they are unsafe).
Another thing I can think of - Go's interface mechanism overhead is usually significant, and I've also found that the networking stack is slow compared to raw C. When I brought this up on the user group a few years ago I was told that this is a fair price to pay for Go's safety and concurrency abstractions.

I pretty much agree actually. I won't be necessarily writing a database in Go, but for robust and fast application services it's very very good.

The channel primitives are also slow and it is really painful to write fast alternatives as you have to resort to code gen.
yes, and they are heavily used under the hood in the standard library, for things like http connection pooling.
Re: databases in Go, I remember seeing a lot of these concerns a few years ago but it looks like many people found a way to make it work: https://github.com/avelino/awesome-go/blob/master/README.md#...
I actually experimented with a "database" of sorts, a redis compatible server with replication, persistence and an extendable API. It was okay as a toy project but never came close to redis performance, even when it only did ping/pong. Plus it had the issue of stop-the-world GC that was how Go worked back then. Not sure how well it would work today.
> Another thing I can think of - Go's interface mechanism overhead is usually significant

Yes that's a more likely culprit, interfaces mean dynamic dispatch, outside of hand-rolled codegen I don't think Go can statically dispatch abstractions.

If by "bounds-checks indexes" you mean "panics when given an out of bounds index, even if it's a compile time constant" then sure. I'm not sure why people talk about Rust being safe when this sort of thing happens and it can't catch it at compile time. At least C compilers have sanitizers that pick up things like that.
> I'm not sure why people talk about Rust being safe when this sort of thing happens and it can't catch it at compile time.

"Safe" as in "memory safe". Panicking is better than allowing one to read into that memory. I would prefer panic-safe, but we might have to wait for a new dependently typed systems lang for that.

> If by "bounds-checks indexes" you mean "panics when given an out of bounds index, even if it's a compile time constant" then sure.

Er yes, it's safe as in memory-safe, as in an OOB will not own the entire application let alone machine. Error on OOB is a very common (if not quite universal) strategy — and incidentally also the one Go uses, the other primary one being returning null (which would be difficult in a language where most types are not nullable).

If you want panic-safe, use .get.

How is an error differentiated from a panic? Can a panic as is caused by the OOB situation I described be recovered from, or is it necessarily fatal?
> How is an error differentiated from a panic?

I'm using them for the same purpose, but panic is rust-specific whereas error is not.

> Can a panic as is caused by the OOB situation I described be recovered from, or is it necessarily fatal?

Technically it can be recovered from[0] but practically it usually should not be. Panics should not be used as an exceptions system. An OOB panic is basically a failed assertion. If OOB is a normal part of your operations, use a panic-safe access method (e.g. slice::get[1] which returns an Option<&T>)

[0] https://doc.rust-lang.org/std/panic/fn.catch_unwind.html

[1] https://doc.rust-lang.org/std/primitive.slice.html#method.ge...

Interesting, thank you.
You mean, compared to C, which silently allows you to read data beyond the end of the array?

Sanitizers also exist for rust.

Furthermore, often time the bounds check can be elided when using standard constructs like iterators.

Using standard practices like iterators elides the bounds check, because the iterator is guaranteed to never go out of bounds.
> does anyone know why they chose Rust instead of Go for the Parity client?

Maybe ask them? According to their FAQ (https://github.com/paritytech/parity/wiki#our-priorities):

> Minimise Moving Parts: While Rust is multi-paradigm, we aim to write intra-function logic in as functional a manner as possible. Mutability is avoided except where necessary for the algorithm or efficiency.

> Minimum Footprint, Maximum Performance: Maximise references, minimise copying and holding copies. Rust makes it safe. When there are dynamic data structures, provide means for keeping them under control.

> Reliability: Through Rust's language-level memory and thread guarantees and a disciplined approach to exception-handling we can state with a high degree of certainty that our code cannot crash, hang or bomb-out unexpectedly.

But I can't tell you that's all of the story. In the original announcement thread on /r/ethereum, the main point of comparison was C++ rather than Go so ymmv.

I would think having better safety guarantees is a big reason. Rustc can detect data races, which I'd say it's very useful with an Ethereum client.
Go can detect data races as well, not sure it's the reason. Probably performance.
Go may detect data races if your test suite triggers them and you opt in to use the race detector.

Rust rejects programs with data races unless you opt out by explicitly marking code in question as unsafe.

Quite a bit of a difference there.

The Geth client is still widely used as it's kind off the standard implementation of the Ethereum EVM and Node Mechanics.

The Go based client isn't that much "older" either, both are actively developed and received some code overhauls.

I've also found the go client's web3 console to be much more reliable and complete than Parity's.

> You know how much code I’d have to write if this were C++, C#, or Java? None. They all have reusable notions of an immutable, ordered map.

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.

And java doesn't have them either. You need to use an external library, likely the google containers library.
Java does have unmodifiable wrappers but they implement the same basic mutable Map/List/etc. interfaces, just throwing at runtime if mutating methods are called. Not exactly optimal but better than nothing.
Better than nothing but worse than the C++ solution of returning a const reference which can be checked at compile time.

Both are just immutable views on top of mutable containers. But in both C++ and Java it is trivial to create a truly immutable container of T. Not so in Go.

But you can build and use generic immutable collections.
This is true. My beef with Java collections is the type-tree-gymnastics library authors have to do to get the interfaces exposed properly. Generally people end up with a family of ImmutableContainers and a family of Containers that parallel each other. The mutable Containers usually inherit from the immutable versions.

This is possible, but it's an unfortunate form of code duplication.

> 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.

  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.
Yes, humanrebar is really grasping at straws. :)

I would also mention that using mutable and const_cast is frowned upon and it can be trivially searched for and forbidden.

Their comment about const iterators also misses the mark. Yes, the designers of the C++ standard library did a bit more work and now everyone has a high-quality type-safe dictionary implementation available. That's how it's supposed to work...

> humanrebar is really grasping at straws. :)

On the contrary, I've had to mentor a lot of C++ developers where they were surprised when their put-const-on-it approach to providing safety resulted in production bugs. I think it's fair for people to be surprised by this sort of thing. And I don't think the things Go developers have to do are all that much worse than equivalent code in C++.

In C++, 'const' mostly means the compiler will give you nice diagnostic errors if you do mutable things to 'const' things. There aren't any guarantees about the data itself being unchanged.

> the designers of the C++ standard library did a bit more work

My point was that all container implementations need to do the same work if they want both const and non const iterators. You can't just return 'const iterator' from you 'begin' method because that implicitly converts to a non const 'iterator' and a mutable view of your container.

> Yes, the designers of the C++ standard library did a bit more work and now everyone has a high-quality type-safe dictionary implementation available.

I very much appreciate the hard, mostly unpaid, work that people put into the C++ standard. But everyone has to do a bit more work if they want, say, a read-only key-value store laid out in contiguous memory. The standard key-value stores are a pretty straightforward rb-tree and a suboptimal (to allow for iterator semantics) hash table. It's not hard to implement (again, due to hard work from the standard committee) a sorted vector<pair<Key, Value>>. It's a bit of arcane work, however, to make that optionally const or to have it share an interface with map and unordered_map.

Context is everything. If you come in the middle of a topic about declaring a bog-standard map and start discussing the subtleties of const, container design and the map implementation you will confound people and are frankly off-topic. :)

I mean the discussion was about declaring a simple map. In that context, it really is as easy as writing

    const std::map<int, bool> map;
Go can't do that. C++ can do that. Seems pretty straightforward.
That's basically only useful as a stack variable, though, which was my point. Well, that and "immutable" in the context of C++ is misleading.

It was on topic because I am saying the C++ boilerplate wouldn't really be better than the Go boilerplate. All the "out of context" details are basically my point. You need to know a lot of surprising things to C++ well.

> On the contrary, I've had to mentor a lot of C++ developers where they were surprised when their put-const-on-it approach to providing safety resulted in production bugs.

Can you provide any concrete example on how adding a const qualifier introduces bugs? Besides disallowing calls to mutable member functions, adding const qualifiers changes nothing in the code.

> const std::set<int> s // This is immutable.

> Foo() : s(helper()) {} // This is a move, not a copy.

First, I'll point out that the data of s is not immutable. The fact that you can const_cast away the const and change the value makes that clear. If people do things like that (or corrupt memory or have race conditions in the code), C++ doesn't necessarily give you clear exceptions or compiler errors. Many of the ways to break things result in "undefined behavior", which might involve data corruption and crashes. I've seen both happen to 'const' objects in production code.

You have a fair point about the move. But not all projects are using C++11 yet. And your use of a 'helper()' builder was actually part of my point as well. The article complained about all the extra code needed in Go to do immutable maps. But it's not true that in C++ you can just add 'const' to a few spots and move on.

I'll also point out that having the 'const' member in 'Foo' likely makes it impossible to implement assignment operators and possibly a move constructor, depending on what you are doing. It's likely if you want to have read-only semantics in your data in C++, you'll have to be aggressive using unique_ptr and shared_ptr so that you can have things like proper move semantics. And that means you'll likely need to be checking for null data.

Point being, you can achieve read-only semantics in C++. But it's not foolproof and it's not as clean as the article seems to imply.

  const std::set<int> s;                   // This is immutable.
  const_cast<std::set<int>&>(s).emplace(); // This is UB.
And what do the assignment operators of Foo look like?
I'm fixing your incorrect understanding of how const works in C++ and you are choosing to be argumentative.
> The fact that you can const_cast away

No, you cant. The standard states that "any attempt to modify a const object during its lifetime results in undefined behavior".

If you're intentionally writing code that relies on undefined behavior, you are intentionally writing broken code.

Let's take this const map:

  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.

>It is linguistically guaranteed to be non-mutating - const-casts excepted of course.

Technically even after const-casting, if it was declared const then it's still not possible to mutate it, as doing so is undefined behaviour. "Modifying a const object through a non-const access path and referring to a volatile object through a non-volatile glvalue results in undefined behavior." - http://en.cppreference.com/w/cpp/language/const_cast

It could for instance have been stored in non-writable memory, so any attempt to write to it will instantly segfault. This is one of my favourite things about C++ as people engaging in terrible programming practices like const-casting a const object get their just desserts, so to speak.

Yes, if you use the escape hatch from the type system, the type system won't help you. Do you also avoid the use of strong types since you can always cast to a *char?
I did Go for years, but stopped doing any serious work in it about a year ago.

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.

(2) - https://github.com/karlseguin/typed

>I found it a chore to maintain Go-based systems

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.

> I don't have much experience with elixir, but that's because I like to stay employed.

While the previous commenter gave an objective and rational overview why he think Go gives him a hard time you can't resist to get polemic.

I gave rational counter arguments and employment is pretty important too imo. It seems a lot of the anti-Go people tend to favor languages that no one gets paid to use.
Yes, but the discussion wasn't about the employment situation and if--your tone was slightly inappropriate. You could have also written: "I've no experience with Elixir but your words catch my interest. The inferior Elixir enployment situation (compared to Go) held me back from Elixir. But I should give it a try."

Same message and nobody loses his face.

(comment deleted)
In reality, Go is a language that practically no one gets paid to use. Go is niche and the few projects around here are short term.
I interviewed for two Go jobs in London last month that were specifically building up Go teams for long term projects. It's definitely getting less niche by the month.
You have the ability to choose Go - you could as easily have chosen Elixir.
You should stick with Cobol. It paid the developers for 60 years, and has a very fair chance to continue commanding reasonably good salaries for another 20 or 30.

Maintaining Cobol code is a chore, though, partly for the same reasons as Go code.

Paid for Elixir full time, and there is definitely a market :)
I definitely agree with you that Go codebases has a high level of consistency. It's certainly one of the many good things Go offers and something I hope other languages look at to see if they can replicate.

But, that doesn't really have anything to do with it being a chore. We can just agree to disagree on that.

However, I think you misunderstood my point about errors (or maybe you're just working with very different types of systems). Empirically, looking at Java codebases, the C# team found that most errors weren't "handled" beyond an outer exception handler. Anecdotally, this has been my experience as well. I admit that this is a very 'web' point of view, where requests are isolated and most exceptional situations are best handled with logging + showing the user a friendly error message.

At this point, they should adopt the Elixir approach and consistently provide a ! alternative across the stdlib (Must* is inconsistently available and verbose). They thought Go would be used by C++ programmers, instead it's being used by Ruby programmers (1). The language paradigm doesn't fit how people are using it as well as it could.

I'm not sure what other languages you've been exposed to, but your last line is hard for me to believe. If you look at other languages, you'll find a lot of techniques aimed at quality and resilience that are nowhere in Go. Again, that doesn't make Go bad, but "resilient" ? Go has mutable, shared memory, nils, no thread isolation, no generics (so runtime casting), no pattern matching and so on. Even the error system which you like is reckless (??) compared to what you'll find in languages like Rust and Haskell (Result and Maybe).

(1) https://commandcenter.blogspot.com/2012/06/less-is-exponenti...

>> the entire error thing is absurd. Anders Hejlsberg got th is 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.

Absolutely. My approach is usually that errors are always wrapped not just returned. The wrapping itself ends up looking almost like code documentation: "could not do X: <previous error>", "error while doing X: <previous error>". Every error string is then unique in the program, easy to grep.

"Could not perform the operation", maybe together with a 500 lines stacktrace in your logs (which are maybe line-by-line bacause that's the default)... you are up to a nice middle of the night debugging session :)

Exactly, once you wrap errors with their context, or just at the lowest level wrap it with a stack trace, they become much more useful.
When you wrap the error then you are kicking it out. Sooner or later you must catch the error. So, in most case, its just delaying the inevitable.
The problem I've found it that you might wrap errors this way, but the libraries you use don't, or approach it differently. So you're either chasing down strings that may or may not be grep'able or you're fighting with delve to track down what's happening. The whole error situation with Go is one of the main drawbacks of the language imho.
> This is almost always unacceptable in production,

This is not true for almost all software outside of embedded domains. In fact responding to an error near where the error occurred is almost always a mistake. The rare exceptions to this are when "error" is unavoidable due to concurrency (e.g. I/O), and even then many errors shouldn't be handled (misconfiguration, incomplete system initialization, etc.) or should be wrapped and transformed for human consumption (e.g. user tried to access a resource that couldn't be found - we still bubble up).

> you need to catch all the errors and think critically about what should be done when that error is encountered.

If you support this position, you'd support checked exceptions like Java - something that's widely considered a mistake.

There's lots of reasons why it's a mistake. A more important reason has to do with dynamic construction of control flow. If you use any monad design patterns in the construction of your application (easy to do because the monad design pattern is so powerful), you quickly create boundaries that errors need to cross unmolested because they're generic and cannot possibly encode policy. The same criticisms follow through into dependency injection and other forms of modular decomposition that use dynamic recomposition. Java style has you doing utterly pointless things, like wrapping all the implementation errors in a generic module-level error - obscuring the very thing that might theoretically let user code react to the error state!

More philosophically, abstractions fail in implementation-dependent ways. When you encode policy about failure outside the abstraction boundary, you're encoding implementation-dependent behaviour; but you can't put the policy inside the abstraction boundary either, because then the abstraction isn't reusable - different applications have different policies. Error handling is inherently opposed to abstraction. That's why microscopic error-handling close to the cause of the error only flies in small closed systems, like kernels, databases, embedded.

> If you support this position, you'd support checked exceptions like Java - something that's widely considered a mistake.

The primary problem of checked exceptions in java is not that they exist, it's that they are very badly integrated with the rest of the language e.g. it's impossible to be generic or transparent over checked exceptions, their hierarchy is messy and it's often unclear why specific exceptions are checked and others unchecked. Haskell's error handling (reified through Either error values) has similar effective semantics but nowhere near the dislike because it's well integrated in the language and you can be generic over the error, the value, and the result itself.

Haskell doesn't encourage runtime polymorphic module boundaries. That's the key difference with Java, not hierarchies. Abstractions invoked through runtime polymorphic interfaces compose poorly with statically typed exception specifications.

Generic composition that lets error types flow through abstractions also lets implementation details flow through, so it is an architectural choice, make no mistake. You don't dodge the tradeoff between abstraction and error handling.

I tried Go and so far I'm feeling that Go forces me to abandon error checking and even if error is catched, I have no idea where this error comes from, because there's no stacktrace or anything like that. Best thing I've come to is using strings as errors (completely throwing out any error hierarchy) and returning new error string for every call with call information (doing poor job which Java would do for me automatically).
This is not the right way to do things.
>I don't have much experience with elixir, but that's because I like to stay employed.

Yeah, because Go is the epitome of employability...

>"Central error checkers" are almost never robust enough for mission critical systems.

Go is not used in mission critical (embedded, medical, aviation, communications, etc) systems anyway.

Besides the notion is wrong: mission critical systems DO centralize error checking. The code local to the error wouldn't have enough context (and shouldn't have the reach) to call the right procedures for cleaning up and moving on.

> 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.

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).

Strong types, not necessarily static ones.
I don't disagree. It's just more tedious in static languages.

Let's say we're doing a user registration. In most dynamic languages the JSON body will get parsed into a map. Excuse the fat controller and pseudo-language, but it'll end up looking something like:

    func create(conn, params) do
      if not Validator.is_email?(params["email"]) do
        return error(conn, "email is not valid")
      end

      if not Validator.min_length?(params["password"], 10) do
        return error(conn, "password must be 10 or more characters")
      end
     
      create_user(params)
      true
    end
In Go, if you want this nice and typed, you'd add a RegistrationInput struct with the field mapping tags:

    type RegistrationInput struct {
       Email string `json:"email"`
       Password string `json:"password"`
    }
Map the user's input to the structure, handling the error:

    var input RegistrationInput
    if err := json.Unmarshal(req.body, &input); err != nil {
      ...
    }
And then write the exact same validation checks.

What safety does Go's version buy you?

In C# MVC, the JSON gets deserializrd into a tree of statically-typed objects you declared beforehand. Any malformations of the JSON are an error.
Well, the dynamic version won't complain until runtime the day you mix up the order of your parameters and invoke "create(params, conn)", for instance. Besides, you can make it statically impossible to generate an invalid user object with a static language. In a dynamic language, an invalid user object is just an assignation away.

Now, if we are talking about error handling, the way it is done in Go is not particularly representative. Most static languages would go with exceptions (or maybe checked exceptions in Java). Haskell/Rust/Scala would probably go with a Result type, and then it's more a question of the language giving you the tools to propagate the errors and wrap them (Rust with error-chain is excellent at this kind of thing).

Your example actually illustrates why dynamically typed languages should be avoided in general, especially when manipulating data coming from outside.

Your code starts by making a few checks on the shape of the data you're receiving and failing earlier if some conditions are not met. Great.

Once these checks are cleared, you now know that you can materialize an object with valid values in it, something the rest of your business code will be able to manipulate. So you instantiate that new object, return it and... all the knowledge about the shape of that data is lost.

The only thing your callers will see is an Object and zero information about what operations or fields are available on this object. They'll have to guess.

With a statically typed language, you don't have such a problem. You have asserted all these properties about the data that came in, you know exactly the class of the object you are creating, why forget all this precious information that will keep your code robust?

In my experience, dynamically typed languages are at an evolutionary dead end and offer close to zero advantages over modern statically typed languages (e.g. Kotlin, Swift).

> Once these checks are cleared, you now know that you can materialize an object with valid values in it, something the rest of your business code will be able to manipulate. So you instantiate that new object, return it and... all the knowledge about the shape of that data is lost.

I'm not really seeing how that necessarily falls out of the example given by GP. Could you go into that in more detail?

Is there something preventing create_user(params) from emitting an object with a well defined interface?

That function does create an object with a well defined interface, but because you are using a dynamically typed language, that object has the type "Object" (or Any, or whatever the mono type of that language is).

If you call that function, you have no idea what the type of that object is. Only the function knows it and that information is lost as soon as the function returns.

I think I will have to disagree with that. The type of object is not enforced by the compiler, but that's a long way off from "you have no idea what the type of that object is."
The fact that the compiler cannot enforce it is a big deal since it prevents automatic refactorings, often impairs performance and overall, hinders readability.

And yes, you really have no idea what the type of that object is unless you look at the source of the function. Which is obviously suboptimal, especially if you don't have access to that source.

> What safety does Go's version buy you?

A statically typed version guarantees you haven't made some fundamental mistakes and enables automatic refactoring.

Say we wanted to change an ID field from being a string to an int later in development for example. You've now got to hunt around all your files making sure the right type is being used. It feels incredibly primitive when you need to do this manually or have to rely on an exhaustive and annoying to maintain test suite for this.

No matter where the data comes from, there's so much more validation to be done that types can't handle. Why should validation concerns be split up?
It's one less thing to worry about, and cut out some tedious overhead for the developer?
But the way it's done in static languages​, it's one more thing you have to worry about constantly, even when it's not a real concern. And when you need to parse external data, all you get for free is ugly exceptions that you have to decipher to construct a user readable error.
Most modern dynamic languages have strong types. In Python you will get a TypeError if you try to add a string and an int or use > on a tuple and a dict.

As for data validation, the hard part of it is not checking type but that the value is safe and coherent. And dynamic languages excel at that because they make complex declarative schema descriptions easy while powerful.

See marshmallow for Python: https://marshmallow.readthedocs.io/en/latest/

It will validate everything, including nested recursive data with cross references in fields and a need for sanitation and casting. But you don't need a lot of code to do so.

Static languages are good to enforce safe practices at scale . That's why they are popular in big IT structures. Dynamic languages are fantastic when you have a lot of external data manipulations. Hence their popularity for the Web, banking, biology, geography, etc.

> Most modern dynamic languages have strong types. In Python you will get a TypeError if you try to add a string and an int or use > on a tuple and a dict.

>>> (1,2) > {1: 2} True

>And dynamic languages excel at that because they make complex declarative schema descriptions easy while powerful.

Marshmallow looks like it works the same way as existing systems in statically types languages. e.g. Serde in Rust:

https://serde.rs/

This validates at compile time.

  In [60]: (1,2) > {1: 2}
  ---------------------------------------------------------------------------
  TypeError                                 Traceback (most recent call last)
  <ipython-input-60-f9d83a4c8f73> in <module>()
  ----> 1 (1,2) > {1: 2}

  TypeError: '>' not supported between instances of 'tuple' and 'dict'
I said "modern". Python 2 is not modern. Python 3 is.

And the keyword in my schema validation sentence is "easy".

I love rust, but a low level language cannot be compared in productivity to a high level language for this stuff. They are much, much easier. That's the whole purpose. You trade easiness for rigor.

> Most modern dynamic languages have strong types. In Python you will get a TypeError if you try to add a string and an int or use > on a tuple and a dict.

You mean a dynamic check? It's better than nothing but I don't exactly want my e.g. scheduled data import process crashing because it was sent some bad data. I'd much rather have static checking and have all the possibilities accounted for before the code runs.

> As for data validation, the hard part of it is not checking type but that the value is safe and coherent. And dynamic languages excel at that because they make complex declarative schema descriptions easy while powerful.

Why couldn't you write this with static checks? Statically checked functional languages for example are great for writing compilers.

keyword being "easy"
I just don't find that personally. Having to run the code over and over again to manually check for errors or having to write unit tests to gain confidence it's working is more difficult to me than getting help from static type checking. I mean why do you think it's always going to be more difficult with static types? Because you have to write type annotations?
External data are never perfectly documented or known. You usually have huge and painful experimentation phase with parsing it. Experimenting is way easier with dynamic languages. You type less. Changing your mind has a lower cost. You don't need to think about doing the right things as much.
So in a statically typed language, if all I know is that I'm getting a string as input, it'll force me to deal with all the possibilities. Is it a number in the form of a string? A null value? A string that isn't a number? The type system will guide me through handling all the possibilities and enforce that I handle all of them. With dynamic typing, there's always a chance you've missed a case and next time the data comes in it'll trigger an exception.

If you have decent type inference there isn't extra typing and any time you think you're saving with dynamic languages gets eaten up having to manually fix things when errors happen at runtime. I'd much rather spend time fixing predictable compile time errors than unpredictable runtime errors.

Couldn't agree more. In my current life as a Ruby dev I've found the dry-struct (http://dry-rb.org/gems/dry-struct/) library a huge help in getting some level of sanity when dealing with converting inputs to data types. It also helps being able to reason about the inputs structure outside of just looking at some hash field indexing at time of use.
The Node event loop (the control plane) is single threaded, but the data plane is multi-threaded, you can easily write C++ bindings to run in the thread pool to handle CPU intensive work.
Speaking of Node in the context of error handling, what are current best practices in Node (other than running node-forever respawning)? Up to v0.12, Node used to have heuristic cleanup code for file handles and other resources on exception domain error handler execution, but the domain API has been deprecated for some time now ([1], [2]).

[1]: <https://nodejs.org/api/domain.html>

[2]: <https://github.com/nodejs/node/issues/66>

I don't like the "central exception handler" idea -- it is invariably too far away from the error site to do anything intelligent.

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.

I don't think a central exception handler means ONLY handle exception in one very specific point. It means that if you can handle an error handle it, otherwise let the central exception handler handle it.

I really like Elixir and the Erlang VM but supervisor trees are not a perfect solution either. For all the talk about how in Erlang you just let things fail and don't have to do defensive coding, any non-trivial application requires a lot of very thorough design on how to handle error scenarios and keep your application consistent.

If a process fails too many times too often (say your web service it's trying to ping is inaccessible) it can take out your whole supervisor. If that supervisor is holding an ETS table that holds important coordination data (but not data that requires persistence) that ETS table (and all it's data) is now completely gone, and unless that case has been handled you have a lot of processes running but not doing what they should be doing, and your application is inconsistent until you completely shut it down and restart it.

And if you don't plan for a lot of these scenarios then you will be caught completely surprised when it happens in production.

If you need an ETS table to have greater resiliency move it up higher in the supervision tree. Nothing prevents you from starting it at the application level.

Also you have a lot of control of your supervision tree. You can specify the quantity and frequency of failures that would cause a supervisor to fail. For some use cases, 3 failures in 5 seconds might be reason enough to restart that supervisor. For other use cases, a much higher failure rate might be justified.

Right, so you have to make a lot of decisions on what things can fail, how often they should be allowed to fail, what side effects can be tolerated with a major failure, what happens if a bad process fails when it comes back up (thus risking causing the supervisor itself to cascade restart itself), etc...

These decisions are not trivial and are exacerbated because of the Actor style of flow due to the independent pieces.

So from my experience you have non-BEAM languages where a single error can take down your whole application if you don't plan your failure scenarios properly, where as in BEAM languages if you don't plan your failure scenarios properly you are more likely to end up with an inconsistent system. The former isn't that bad in a lot of circumstances as you should have an external application monitoring and restarting a critical server that's no longer running (even for BEAM systems) but the latter can be very bad because it can be a silent issue.

I have systems working at my job written in .net that have been running for over 6 months without a restart, even when Azure had failures and it could not talk to its command and control server for an hour and kept running perfectly fine with using a well thought out architecture and a well placed central error handler for generic errors without things cascading throughout the whole system.

So resiliency has less to do with language or supervisor trees and more to do with making every step has failure scenarios thought about and planned for.

last I heard the idea of "tombstoning" (guaranteed cancellation) in Go was impossible and given up on.
>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 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.

Depends on the app. In GUI, yes, why not, display an error message and you are done. In infrastructure software, OTOH, you want to handle errors in thoughtful manner. If you can't it's often better to crash the application than to continue with broken state.
Erlang/Elixir is pretty interesting there in that it will "crash the application" but said application's scope is a single process (unless others are linked to it, which lets you build supervision tree and take down and restart the entire tree when one of the processes crashes).
Sure, if you're in a GUI then you report to the user that an operation failed and it's up to them to figure out what they can do about it.

As for infrastructural software, yes you want to handle errors in a more thoughtful manner. If you can't and the process has corrupted it's own stack or heap (in the case of C or C++) then sure, abort. But I'm saying this line is often chucked over to "log the exception and do nothing" too quickly because it's often the easier thing to do. 25% of catastrophic failures in this one study appear to occur because of this.

tl;dr: The devil is in the "If you can't".

Yep. And lack of exceptions in languages like C or Go makes it harder to pretend you have done the right thing, if you in fact did not, given that it forces you handle every single error condition explicitly.
(comment deleted)
I think the distinction between errors (bugs) and exceptional situations made here[1] is important: if you have a problem because of some transient condition (an exceptional situation like a network outage or a full filesystem) then it makes sense to try to recover somehow. If you hit an error (i.e. A programming error or a bug) all you can do is abort, since you can no longer trust that the program will do the right thing.

[1]: http://joeduffyblog.com/2016/02/07/the-error-model/

I always wonder if the Go error issues could be handled more cleanly by using the suture library. I haven't seen widespread use but it's supposed to give a supervisor pattern to Go.
"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." Im not so sure about it. Database is not dynamic and user input should be controlled, or number, or date, or text, for example an user could enters the price of a product a text.
>Node is single threaded

You can still utilize all of your cores in Nodejs. So which problem are you encountering?

That the utilitization of the cores is sub-par, and at a crude coarse level (node process).
Wat. OpenResty as an alternative to go? Wat?

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.

I honestly thought the OP's post was a troll. Lua is for masochists.
>Lua is for masochists

Is this a happy-hour for Reddit users on HN?

I've done my first project with go. Goroutines are undoubtedly cool and I like the strong typing.

* 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.

I get stacktraces from `panic()` - is this not what you're after?

    goroutine 1 [running]:
    main.b()
        /home/rjp/git/godiggy/test.go:8 +0x64
    main.a()
        /home/rjp/git/godiggy/test.go:13 +0x9b
    main.main()
        /home/rjp/git/godiggy/test.go:18 +0x9b
    exit status 2
I want to be able to handle errors, pass them up a few functions then log a stacktrace. I don't want to panic everywhere since robustness is important.
> I'm a huge fan of dynamic languages. I think they're so much more productive.

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.

In any production grade application where order and persistence is truly important you need to supply a time stamp anyway and at that point you'd want to just store your animals in a database. This is pretty trivial imo.

If you just want a very temporary ordered list you could probably use a channel.

The link to the immutable, ordered map that "Java provides" seems to be a link to a third-party library created by Google.

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.

Is it possible to write a reusable version of something like this in Go without using interface{}?
If "static codegen" is considered OK, then I guess yes, and I guess you can use The Thing[0] to even make them look like actual generics.

Every user of the library will need to run codegen to post-process their source files and generate the relevant instantiations of the "generic" collection.

[0] https://www.reddit.com/r/rust/comments/5penft/parallelizing_...

Then I could take almost any reasonable language, and transpile it to Go source. This worked for years for Javascript 5, and for C before that, and for Fortran even before that.

In either case, the reason was that the target language was considered not expressive enough, though ubiquitous.

So you can do it in Go in the same sense that you can do it in assembly language.
(comment deleted)
Having to encapsulate data to ensure read only semantics seems like par for the course from an OO perspective. In C# (which is 15ish years old at least) I don't think there has been an immutable map until very recently. Also (and this is probably a code style thing) I can't remember having reached for an immutable ordered map in a long career.
ReadOnlyDictionary has been around for 5 years, and even before that you could implement this with generics in a way that is re-usable everywhere rather than having to constantly 're-write the wheel' as in Go as the author laments.
Yeah lack of generics with the argument of "simplicity" is a special kind of crazy.

I was thinking of the ImmutableDictionary that's more recent. Normally in C#/Java you should rarely expose collection types directly so you almost always encapsulate the list/dictionary and provide e.g GetReservation(string id) to the outside, which makes it less common to even need the readonly types at all. Immutable ones are a whole different thing since they enable proper sharing of whole collections.

> I can't remember having reached for an immutable ordered map in a long career.

Me neither. But I have reached for a fast single producer, multi consumer queue, futures/promises, concurrent safe maps, non-blocking queues, and even plain async queues with batch semantics, many many times.

So either the author, me and everyone else that is claiming this is a problem are doing it wrong or its actually a problem.

Go was explicitly designed to be a simple language, without a rich tapestry of data structures to suit every use case.

Languages are like tools. If you need a saw, use a saw, instead of complaining about the knife you are using.

No, languages are like toolboxes, they provide you with set of tools.
No, languages are like Ogres, they have layers.
Then you should be using the toolbox for metalwork when trying to weld two steel plates instead of using the one for woodwork.
I am not sure where the authors exact requirements fit in the greater picture of the desired application. But one important tool which works great in Go is functional representation. If you want e.g. an immutable map, make it a structure with private fields, and give access vial functions (methods). The first comment to the article describes a nice example of this approach.

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.

But you still have to write the function a hundred times — once each for every immutable map you'll use.

This is the opposite of DRY.

Go also supports libraries.
Go doesn’t really support libraries of data structures and containers, though. You can’t build a library of different kinds of immutable and mutable maps, trees, etc that are reusable – precisely because generics are missing.
Yes, if you needs lots of different immutable maps as your interface to a library, then the lack of generics is annoying. But the question is, is the immutable map such a common interface, or should your library and abstraction barrier be something else? I am a professional programmer and have prefer abstractions via objects/functions.
The author discounts this as a good solution because you'd have to re-implement the structure and functions for every single type you might put in the map; i.e. that would be a valid solution iff Go supported generics, but it does not.
Go's introspection tools do provide enough support to offset the lack of generics, at the cost of having to write your own type checks, and at the cost of having runtime instead of compile time type checking.

But it can be done.

Anything Turing-complete could by definition be run on a Turing machine. But Brainf*k, a straightforward Turing machine implementation, seems less productive than almost any other language.
People say this, but it isn't really true- you could use the sort.Interface interface like everyone else. Consumer wants to use your custom map with their custom type? Make sure it supports Less.

Speaking of sort.Interface, people say they want generics, but I think there is an implicit ask for operator overloading, too. Or the code they are asking to not write is probably still going to be there.

> People say this, but it isn't really true- you could use the sort.Interface interface like everyone else. Consumer wants to use your custom map with their custom type? Make sure it supports Less.

1. So you have a map of sort.Interface, and that's just as absolutely-not-convenient-or-type-safe as a map of interface{}

2. TFA does not want a sorted map, they want an ordered map, so sort.Interface has no relevance whatsoever: the order of iteration of an ordered map is the order of insertion, not the sort order of the elements.

> Speaking of sort.Interface, people say they want generics, but I think there is an implicit ask for operator overloading, too. Or the code they are asking to not write is probably still going to be there.

Not really. Java does OK without operator overloading. Having to use methods instead of operators can be annoying in some cases (mostly when working with non-built-in numerics e.g. decimals or infinite-precision integrals) but it doesn't limit you much if it does at all.

The OP suggests "some form of codegen" as a way of getting around the Go dev team's, shall we say ... unusual hostility toward generics. But that way lies C++ templates, which were originally meant to be implemented as macro-generation -- and which lead to the messiness and complexity of C++ builds which were one of their major reasons for doing a new language in the first place.
Templates don't help C++ build times, but the root problem is that it uses the C model for compilation (several sequential passes).

D compiles faster than Go usually and with templated generics all over its std lib

I think you have to work with the language you're in. I had similar frustrations when I first moved into doing some dev in Java and C# and it really bugged me that there was no compile-time guarantee that an Object reference passed as a method parameter would not be modified within that method (as I was so used to using const-refs with in C++)

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.

Sometimes it doesn't cost much if anything to add a feature which improves (type) safety. Now that Python has some type annotation support, it could make sense to also add a const annotation.

Go should have had it from the beginning IMO, immutability can help avoid some coding mistakes and also makes it easier to reason about code. It'a nice match for a language with static typing.

I don't see what's wrong with venting frustrations with particular technologies. It helps no one if people just move on silently.

It deprives the creators of that technology of the opportunity to react to any complaints and it deprives everyone else of the opportunity to choose a different technology from the start.

Out of curiosity, what is the use case for immutable structs? (asking as I'm a ruby/js/go dev and as such I've never used such idiom)
It's a code safety thing. If you create it and the compiler makes it immutable, then you can't screw it up later with an accidental mutation.

Basically, something that takes coder discipline in Ruby/JS/Go is handled a compiler/runtime enforced restriction.

Oh, I see, thanks. I get now how it can be a problem when you're used to rely on it and it's not in the language.
Of course the problem he has is that it doesn't follow the hot new immutability fad, and of course he doesn't explain what he needs it for, just links to a stack exchange question which essentially says that it might be useful for some cases.

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?

(comment deleted)
Spot on. As a card-carrying dinosaur I've found myself from time to time needing to read up on some "new" (usually turns out to have been invented in the 60's) coding thing.

Once I figure out what it is, I ask myself what problem it solves (the literature typically doesn't say). The answer tends to be one of:

1. Saves some typing.

2. Saves some work when refactoring.

3. Avoids some class of bug.

4. Highly useful in a kind of programming I don't do (e.g. compilers).

#1-2 in my experience are much more common than #3-4 and often #1-2 can be dealt with through tooling (IDEs for example).

Absolutely. With respect to this article and immutability, the strongest arguments I've seen for it are for concurrent programming, like the very successful Erlang solutions used at Ericsson, which I generally don't have an interest in. So, it is reason #4 on your list. For the types of program and the types of problem I need to solve, it's a moot point - people have been getting along just fine without immutable state and continue to do so, and to learn how to do it differently would just be more trouble than it's worth, if not an outright regression.

This is the major problem I have with this sort of article; people never talk about languages in the context of actual working programs, just engage in dry theorizing or armchair programming. If the author put the complaint from this article into context, I'd not have a problem with it. But because it's a toy example with no relevance to actual programs people might want to write and use, it's unhelpful and misleading.

Also, sometimes the people making the argument for language feature X don't fully understand the existing solutions to the problem, or how much of a practical problem it is. E.g. the "less typing" concern -- when you write code day in day out a bit less typing really isn't in your top 10 list of concerns. That list would more likely include things like "easy to understand what the code does" and "easy to debug".
Limiting mutability is not new and it's not a fad. It is at the very core of every single principle of modularity and dependency management ever invented by mankind, even beyond programming.

Almost 30 years ago when I first learned C one of the first design principles I was taught was to avoid mutable global variables. And ever since that time I haven't found anything as key to avoiding bugs as knowing and controlling exactly what code can change what data.

I find it totally baffling that a practicing programmer could possibly think of mutability as a purely theoretical concern.

Don't move the goalposts. Global state is not the same issue as immutable data. And even if it was, Go has the `const` keyword anyway, making it entirely irrelevant to the OP article.

This fad of using immutability everywhere and treating it like a silver bullet absolutely is new and is a fad.

The article isn't advocating "immutability everywhere". It complains about Go making it difficult to create immutable (or otherwise customized) data structures at all. So it is you who is moving the goalposts.

I mentioned global mutable state as an anecdote and also as an example on one extreme end of the spectrum that should make it clear what the problem is in principle: Not knowing what code changes what state.

But I see that you have decided to avoid debating the core issue entirely.

Your mention of Go's const keyword in this context leaves me scratching my head as you probably know that it doesn't allow you to define immutable maps, global or otherwise.

Thanks for the "no u".

One thing you're correct about: I have avoided debating it, because debating concepts like this is pointless and has nothing to do with real-world code. Which brings me back to the point I made on the article: why does he care so much that he can't have an immutable map? What problem can he not solve as a result of it? The example he's given is a toy problem, and the constraints that make him want such a thing are left unspecified.

>I have avoided debating it, because debating concepts like this is pointless and has nothing to do with real-world code.

It does have everything to do with my real world code. There is not a single piece of code I have ever written that let me get away with not keeping track of all the mutations that could possibly occur.

Let's leave aside complete immutability for a moment and consider a closely related issue that you can see in his code. He needs a map with defined iteration order, something I have needed occasionally in real world code.

He uses Go's builtin map type and a slice to define such a map. Now, how do you keep these two data structures in sync without restricting mutation? I think it's obvious that controlling mutation is a requirement here.

Go even admits that restricting access to data (i.e encapsulation) is necessary, and obviously preventing inconsistent mutation is a key reason why that is needed.

But then Go turns around and says, oh but you can't define fully featured data structures (i.e supporting range loops, type safety and indexing expressions) of your own that use encapsulation.

I have defended Go many times before, but the only way I can defend it is to say, yes it's true, this is a major weakness. We just don't know how to fix it without creating a lot of complexity elsewhere.

"Saves some typing" is not that important as "saves some reading", which is one of the most important activities people do with source code.
For me, it goes like this:

1. What the heck is this thing everybody keeps talking about, with an impressive fancy name? It must a major step forward, after all this time, I should finally have a look at it.

and then

2.a. Oh, it's just a big shiny name for something I accidentally kinda do sometimes, I didn't know it bore a name. Why is it suddenly a fad to base everything on this?

or

2.b. Hmmm... right... so in C it would mean doing this and this... hmmm... okay... and why would I want to do that? I could do it in C, if I haven't done it yet, that's because I didn't see the benefit of it, and I still don't.

Sounds like a case of the blub.
That's a reference to <http://wiki.c2.com/?BlubParadox>?

Possibly. But I use other languages, and I sometimes use a few of those other features in those languages, but I don't miss them at all when I come back to, let's say, C. They don't fit, or don't bring benefit, etc.

Being able to see it happen in the debugger is my top peeve in this space. Yes there's a magic short cut for something, but no you can't see wtf is happening at runtime so when inevitably something seems to happen that doesn't accord with the expected outcome you are none the wiser as to why.
I asked this same question on reddit. And I am not trying to be snarky. I have programmed a long time and I cannot think of a single time where I thought, "this would be easier with generics."

Where is the list of problems that are easier to solve with generics?

You don't mention your level of experience with generic type systems, but I find it's mostly the case that I can't imagine any use for techniques I'm not yet familiar with.

Here is a random example from my recent adventures, a channel implementation in C++ which uses generics to provide value-semantics for user-specified message types:

https://github.com/andreas-gone-wild/snackis/blob/master/src...

And here is a column class that uses generics to inject both record type and value type into the implementation:

https://github.com/andreas-gone-wild/snackis/blob/master/src...

Doing any of these without generics would mean either erasing types or writing duplicate code. Go provides generic channels, slices and maps; but it doesn't extend that power to user-code since users are supposedly not smart enough to be trusted with power.

Channels, maps, and arrays are generic in Go. They solve a lot if key problems.

The problem is that you can't create your own generic structure. Try building a statically type-safe red-black tree, or something like that, without copy-pastig the implementation for each new node data type.

Higher-order functions are also basically impossible.

> Channels, maps, and arrays are generic in Go.

Pointers and slices as well, and of course the various builtin operators or functions used to work on them (make, range, len, close, …).

> You know how much code I’d have to write if this were C++, C#, or Java? None. They all have reusable notions of an immutable, ordered map.

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.

Lombok takes care of most of such boilerplate in Java.

Code generating tools for Go also exist. The difference is that they produce source code, while things like Lombok, or any reasonable macro system, never surface intermediate code, unless explicitly asked.

That's still solving the problem by working around the language though. And surely it's the job of your build system to automate and hide the codegen process? Admittedly go build & go generate don't do that well, but they aren't the only options out there.
Java does it well, you can easily make good immutable map interface. The problem is with Java Collections which is just a poorly designed library, like most other standard Java libraries.

I really hope that someone will use JVM with another built from the ground sane library.

The only thing Java misses is covariance/contravariance for generics, but with some unsafe casting it'll work.

> That’s an infuriating amount of code to write

24 lines, really ?

It was down to 10 after a suggestion in the comments. Given the number of times you might actually need an immutable, ordered map (total count so far in my life = 0), having to write 10 lines of code does seem like an odd reason to be furious with an entire programming language.
I keep getting drawn back to Go.

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...

I did the back-and-forth for several years with Go. What drew me in was promises and wishes, what made me leave every time was realizing I had fallen for the same stupid trick again. It only looks simple because of the features they skipped and the arbitrary limitations they decided on. Once a codebase reaches a certain level of complexity and the need for more powerful abstractions arises, the perceived simplicity is no more.
Go is seriously limited language by design. If the limitations are starting to bother a person, they're better off just using a different language.
It's so interesting how the hype cycle works. Only 1 year ago, people on HN were bashing Node.js hard and saying that you should use Go instead and everyone was praising Go.

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.

I've been reading HN for a couple years now and can't recall there ever being a consensus positive opinion about Go. All the discussions pretty much looked like this one.
In addition to what you describe, my other big complaint with Go is the attitude you find from the devs, in threads discussing these types of challenges.

"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."

Agreed, one of the most dogmatic communities I've had the misfortune of stepping into. But then that's pretty much a requirement given the amount of pretending they need to pull off to keep their bubble intact.