Hacker News new | ask | show | jobs
by brian-armstrong 3146 days ago
Go is woefully missing some really key features, which you encounter when tuning it for high performance. My list of grievances:

- Dep handling was never considered. Makes sense given Google's monorepo but thats not how the world works.

- Stdlib just loosely wraps posix features with many C flags copied verbatim. These APIs are old and could use a refresh but Go never bothered.

- No easy way to construct arenas/pools. Once you go down this route you have a great headache of releasing in the right places. The GC doesn't cut it, you need pools sometimes

- Debugging basically doesn't work on some OSes. No easy way to attach gdb and see what's happening. Doubly so if you use Cgo

- Similarly, Go doesnt bother to hide the differences of different OSes. Up to you as the programmer. Again not surprising for Google's all Linux world. If everything is Linux then OS difference doesnt matter. But even Python does a better job here.

- Logging is poorly thought out as evidenced by multitude of third-party log packages. Anemic compiler means you can't get verbose logging without paying a performance penalty.

- No RAII. Defers are a lazy attempt at this, but they're not even close to being as good as RAII. This is probably the biggest point where you realize Go can't dethrone C++

- Tricky Close() semantics force you to architect entire program around who will close() things at end of their lifetime. Lots of terrible hacks ensue when people build something that works but realize close ownership is ambiguous and rightfully don't want to rebuild it all

- Channels don't have a good way to deal with errors. You're forced to come up with an ad hoc solution to deal with your graph of goroutines/channels when one node errors

- No supervision tree. Erlang existed far before Go but they didn't learn from this key feature. But it would greatly enhance Go to have it

- Hacky reflection semantics that cause subtle runtime bugs when a JSON struct field's name starts with a lowercase letter. And of course, there are no generics, the larger issue here.

I was hopeful that Go would fix some of these things before it went 1.0 and locked in its syntax. Sadly that didn't happen as it was likely already locked in at Google. Go is ultimately kind of brain dead, useful for some very particular features but not so compelling that it can replace any other language.

4 comments

> - Stdlib just loosely wraps posix features with many C flags copied verbatim. These APIs are old and could use a refresh but Go never bothered. > - Similarly, Go doesnt bother to hide the differences of different OSes.

I disagree with these. Go runtime/stdlib is architected to work around many many POSIX headaches and design bugs, hiding them completely from programmers, and to be fully portable. For instance:

* Concurrency is completely redesigned (goroutines). * Signal handling is redesigned and doesn't cause bugs when interacting with concurrency. * Forking/Exec'ing is redesigned not to cause fd leaks in subprocesses (all file descriptors are marked as O_CLOEXEC, in a race-free way), nor have races while interacting with concurrency * Sockets are exposed through a higher-level API (Dialer/Listener). * epoll is not exposed but transparently used by a single thread to handle all supported file descriptors without wasting OS threads, to improve performance

In fact, I think the only thing that is pretty much low-level is os.File and filesystem access in general, which tends to expose lower-level details.

Having experienced a situation where I had to call select() directly, I have to completely disagree. If you run into situations like this, you need the syscall escape hatch. It's really painful to get this working, and it interacts poorly with goroutines.

Sockets are actually another good example. If you need to tweak then at all, you get to use the same old horrendous POSIX names for everything. No name aliases?

Why would you want a supervision tree?

This would suggest using an actor-like model, which is pretty senseless for single-machine usage. It's totally understandable for Erlang, where the VM spans multiple machines, so everything is unreliable, but in Go, I take it for granted, that my goroutines won't "just crash".

I'm not sure, but I think this also ends up being good for modelling interactions with good performance, as you know where you do network calls, where you have reliability, and where you have instant local calls. Though not to say that Erlang applications aren't blazingly fast too.

> It's totally understandable for Erlang, where the VM spans multiple machines, so everything is unreliable, but in Go, I take it for granted, that my goroutines won't "just crash".

Supervision tree gives a structure to put whole subsystems somewhere, including their boot initialization and shutdown (not just restarting it at crash), allows to spawn workers while keeping usual logging/crash monitoring for free, gives a way to inspect the system at runtime (much easier debugging), and allows to run several different applications in the same VM space, which is a flexible way of combining code. I can, for instance, run a CouchDB instance with bolted on my own HTTP server that exposes monitoring data or an administrative interface. It cannot be done in Go (or any other language, barring maybe Lisps) without modifying source code.

And no, none of this "spans multiple machines", it all works and helps on just one single node. Maybe except for debugger, where you spawn a shell node from which you operate.

And yes, your goroutines will "just crash" in situations you haven't expected, as every non-trivial daemon experiences crashes on transient bugs. Though in Erlang transient bugs don't bring the whole daemon down. Saying `my goroutines won't "just crash"' means `my code and code I use as libraries doesn't have bugs, ever'.

No, it's saying: if my goroutines crash it's the same as if my main thread crashed, which means: game over, application down.

Which gets handled by the container scheduler

So every unrelated request that happened to be currently processed is thrown away because of a rare corner case, and with no way to intercept shutdown and save state, flush buffers, or anything. Yes, totally right granulation.

Not to mention that you have just introduced a very complicated piece of software to run a single daemon.

Or, you just catch the panic in a defer up the call chain, log the error, and then exit that goroutine. Like: https://golang.org/src/net/http/server.go#L1694
Idiomatic Erlang involves handling only errors that are expected (read: part of the program’s specification), and crashing on any other kind of error. This keeps the error handling out of the business logic and makes the app code simpler and more reliable.

For instance, you can’t forget to close a socket, release a lock, or log the actor state and stacktrace, since these things are handled by the VM and supervisors when an actor dies. These are nice properties to have even in a single-node system if you’re aiming for high reliability.

I do agree though that a supervision tree doesn’t make as much sense in Go, since the VM does not provide the cleanup guarantees and strong process isolation that make the Erlang model really workable.

Wow! My network daemon at work, written in Lua (which uses Lua coroutines for processing, which are similar enough to Go routines/channels for this comparison) has a supervision tree. It can log the unexpected death of Lua coroutines and keep going, which is nice for the unexpected bit of input [1]. It's also helpful when testing new code as unexpected errors are logged (stack dump, so the location of the crash is reported) in development/QA.

[1] Yeah yeah, all input should be well specified. Could you please inform the vendors that their programs are spewing unspecified crap at me? Thanks.

Go can do almost the exact same thing, in fact, it's baked into the stock http server library.
I'm not disagreeing with you (I've previously criticized Go here) but I think it's helpful to think of Go as a more "modern" C. A lot of the warts you see here (Cludgey POSIX interfaces, no smart scheduling around channel error handling, bad reflection semantics, etc.) are the same as you'd find in C.
Except, it's not a compelling replacement of C. The type system doesn't give me good assurances about program behavior at compile time. Go inserts itself as a heavy unwanted additional layer for cases where I want C. Especially considering that now my program will have GC pauses.
> - No supervision tree. Erlang existed far before Go but they didn't learn from this key feature. But it would greatly enhance Go to have it

This is the one thing in your list that I don't recognise. What is a supervision tree? Can you (or another Erlang programmer) point to your favourite reference?

A supervision tree would be a tree of goroutines where a "master" (supervisor) gets notified when children die (normally and more importantly abnormally).

In Go, if a goroutine panics for some reason it just dies on its own and the world does not know unless you have some sort of ping service or you specifically send a notification from a defer.

In Erlang, if a process faults and it has a supervisor, the supervisor gets a message that one of its children died with some additional metadata, it can then log the issue, restart a child, … This also led to an interesting "let it crash" philosophy (since Erlang processes have isolated private heaps a process dying also cleans up any potential data corruption in the process's private state, this is often considered a feature, but at its core it mostly avoids processes dying unnoticed.

Incidentally, supervision is actually the result of two individual features: linking and trapping. Linking means that when one process dies with an abnormal status (any reason other than regular process exit), all process linked to it will also die (the EXIT signal gets propagated through the link), which is repeated until all linked processes are killed, trapping lets a process handle the incoming exit signal in code rather than automatically suicide

Finally, because supervisors and supervision trees are so common the Erlang standard library provides generic behaviours which let developers declaratively set up which strategies they want in their supervisor, the maximum restart frequency (beyond which the supervisor consider the system's broken and shuts down), ...

> In Go, if a goroutine panics for some reason it just dies on its own and the world does not know unless you have some sort of ping service

If a goroutine panics and is not recovered, it not only dies but tears down the whole process. This seems to be the more appropriate approach for a language with pervasive shared state (locks, etc.). Erlang is quite different here, as you say.

Restarting or otherwise dealing with the exiting process is best left to the service manager that started it.

http://www.jerf.org/iri/post/2930 is a really nice blog post from an Erlang programmer attempting to bring supervisor trees to Go as https://github.com/thejerf/suture.
Here's the relevant chapter from Learn You Some Erlang

http://learnyousomeerlang.com/supervisors