184 comments

[ 4.2 ms ] story [ 329 ms ] thread
I've dabbled a lot with Go. I've found it _very_ effective to a wide variety of problems I don't really have most of the time.

If I wanted to implement RAFT I would probably pick Go. If I want a simple REST/GraphSQL server then Node.js is so much easier. `async/await` is nicer for me than goroutines and I find my code easier to reason about.

Full disclosure: I'm a Node.js core team member and a Go fan. Part of my reasoning might be how much nicer Node.js got these last couple of years.

Channels are basically the primitive with which you can implement futures, thread-safe queues, etc.

Can you elaborate on why you think async/await is easier (for you) to reason about than a goroutine and a channel?

Yes,

An async function is explicitly async (in its definition).

The syntax is obvious which is why JavaScript chose to go that route (rather than adopt channels at a language level for example).

Plus, 95% of the time I care about the singular return value of a function - I just want something that's a function but async - and not a green thread that's using a channel to send information back. Both conceptually and in the code it's a lot simpler.

Most of the time you write `let x = await foo()` in js should become `x, err := foo() if err != nil then return nil, err end` in go, not anything to do with channels
What about `const [a, b] = await Promise.all([thing2(), thing2()])`.

Or

    const results = await Promise.map(xs, x => process(x), { concurrency: 2 })
Or having 2 concurrent loops processing some data?

All things that require more boilerplate in Go while trivial in Node.

I think it's a decision between levels of abstraction, with Node being a level higher than Go in that area. Probably easier to use it to link services together (glue apps), because with Go you'll probably have to define types and such a lot.
OP is complaining about Goroutine stack size at 4kb per connection, his test shows that Node8 is taking up to 150MB of memory with 5k users, 4kb*5k = 20MB for Go memory, I don't understand how Nodejs can take less memory than Go, and without real numbers / test I'm pretty sure he's doing something wrong somewhere.

From my experience on some large prod deployment, Nodejs app takes much more memory and CPU vs Go app for doing similar work.

It's 2 per connection, so that's 40MB of RAM. The OP also said that was minor compared to the channel overhead -- it seems the number of channels was exponential to the number of users in a room.
Seems like it should be linear??
Hopefully it's linear, or it's implemented wrong.
Hopefully, but all he said was "fanout" which could be either.

He knows it's implemented wrong, that's the point of the article: that Go doesn't make it easy for him to implement it right. The only question is the degree of wrongness. :)

It being implemented incorrectly is not a property of the Go language, though.
It is a property of the Go language that the most recommended and explained concurrency primitives are slower than even 20 year old implementations.
I don't think this criticism holds water since he opted for JavaScript. If he was having problems with goroutines and channels, he could have picked a Node-like architecture (single-threaded, callback-driven) and still likely enjoyed better performance (and optimization tooling!) than with Node. If his issue was Go's type system, then he shouldn't have chosen a language with a strictly worse type system. In other words, in the worst case, some scenarios may require you to drop into `interface{}` in Go, which has the same safety guarantees as any type in JavaScript. This way only ~3% of your code is type-unsafe instead of JavaScript's 100%.

While I think there are lots of valid criticisms of Go (concurrency correctness is still hard and its type system is not very good for certain tasks), none of these are reasons to switch to JavaScript, as the author did. If he was struggling with goroutines and channels, he could still elect for a Node-like architecture (even making it single-threaded by setting GOMAXPROCS=1). If he needed generics and unions (as he cited), he switched to a language without them and without any static types at all (or a single static type, if you will); in Go he could have downgraded to `interface{}` which is analogous to JavaScript's one static type, and he would have only gave up type safety in the bits of code where Go's type system was lacking instead of everywhere.

I don't want to give the impression of overselling Go here; it's just that for the cited criteria, Go is strictly better than JavaScript.

Sure, if you don't mind writing your own websocket and pub/sub libraries.
(comment deleted)
Node makes the Node-like architecture much easier than it would be in Go, sort of by definition. Also, wouldn't Node's JIT fare much better in the face of dynamic typing than using reflection (interface {}) everywhere?
interface{} is not reflection, and using both reflection and excessive amounts of interface{} are not a good idea in Go, and frankly, I don't see anything in this code that would require using either
To be clear, my point is that interface{} is the worst case in Go and the only case in JS.
No, there's nothing special about it, and I probably shouldn't have used that term because it might convey that it is particular to Node. Event loop, callback based programming is easily implemented in most languages.
Both Go and JS are async and callback driven. The difference is that with JS those nasty callbacks are painfully explicit, whereas it is all hidden in Go. Node's callstack is a goroutine, except that the latter can also be preempted. If you have 100000 requests in progress in Node, it will consume at least as much memory as a well written Go program would.
In Javascript you have the option to gradual type into Typescript which offers much better static typing than Go. So I wouldn't be too dismissive.

Also, the "just write Node-like code in Go" isn't a solution at all. You're back to fitting a square peg in a round hole.

> In Javascript you have the option to gradual type into Typescript which offers much better static typing than Go. So I wouldn't be too dismissive.

It's not reasonable to switch from a language that gives you type safety in ~95% of cases to a language that gives you type safety in 0% of cases but which provides an easier transition to a type-safe language. This is why I'm dismissive.

> Also, the "just write Node-like code in Go" isn't a solution at all. You're back to fitting a square peg in a round hole.

You're wrong here. I read and write lots of Go code, and it's perfectly idiomatic to write single-threaded, asynchronous code. In fact, I'd probably do just this for his application, modulo a thin compatibility layer to deal with the fact that net/http spins up a goroutine for each request.

Goroutine takes 2kb, so 4kb for read / write per conection.
And I thought the memory usage of channels depends on the buffer size.
A fully connected graph of n users contains n*(n-1) links if users don't connect to themselves (I would describe this as polynomial growth in the number of channels, a lot better than exponential). A chat broker that acts as a switchboard between users could, I suppose, reduce this to a linear relationship between users and channels.
You have to divide that by 2. It's "half the matrix".
Thanks, my mistake. You're right, there are n(n-1)/2 arcs in the fully connected graph (K_n).
> it seems the number of channels was exponential to the number of users in a room

I do not see this in the code linked to from the article.

I count three goroutines per connection: One for ChatHandler.Loop (when invoked from ChatService), one for ChatHandler.socketReaderLoop and one for socketWriterLoop.

Each ChatHandler has three (buffered) channels: outgoingInfo.channel, sockChannel, readErrorChannel (not buffered)

There are some things that could cause bottle necks that are not related to goroutines and channels: Every published message calls GroupInfoManager.GetUsers to get a list of all the users in a group. That can be expensive (I don't know if it is or isn't). And then for every user in a channel, their userOutGoingInfo is retrieved.

I would suggest profiling before switching languages.

EDIT: changed "benchmarking" to "profiling" EDIT2: changed "are likely to" to "could" (... cause bottle necks) since I don't know

Java or C# solved all of this like 10 years ago but people like making this hard on themselves.
Why not use mutexes and callbacks instead of channels for pubsub?
This is not considered idiomatic Go and would be a premature optimization unless you ran a profiler and determined that channels are actually your bottleneck. It sounds like this has not been done.
Not only that, but channels are internally implemented with mutexes anyway, so a naive mutexes+callbacks implementation wouldn't be any faster.
Channels have a performance overhead over handling the locking yourself. Maybe look at stuff from Tyler Treat to learn more (maybe his most recent talk? https://www.youtube.com/watch?v=DJ4d_PZ6Gns) but there are some other discussions (mostly with earlier Go versions) but maybe something changed?
That's the funny thing modern Node is surprisingly usable and if using TS has very advanced tooling.
It's incredibly easy to throw stuff together in Node.js, it's much harder to maintain over time. TS helps with this but you still run into really hard to diagnose memory leaks (mostly in 3rd party packages written in C++, but sometime in Node core itself). The tooling for these type of issues is really poor in my experience.
My main concern with node is how fast everything moves. Try to maintain a project that started with es5, then migrated to es6 (but some legacy code is still es5), then added some es2017 and now is adding slowly typescript to the monster, and all of this in just 3 years!

Things become a mess in no time, tooling changes constantly, and maintaining legacy code while developing new parts with current best practices helps you build a Frankenstein in no time.

(and don't even mention about coffee, or it'll make me start ranting about the move from coffee to es6 and now TS!)

I don't get this.

No one forces you to upgrade your perfectly functional es5 code to es2015, es2017, or typescript.

Yes, node moves fast and new features are introduced, but everything is largely backwards-compatible. You don't have to incorporate every shiny new feature, framework, or tooling.

If you do so by choice, this rant is rendered meaningless.

> (...) developing new parts with current best practices (...)

I sincerely suggest that you stop doing this. If JS folks had 10% of the "don't fix what isn't broken" philosophy of the Python developers who were presented with v3 as the future, we wouldn't have any of those "js fatigue" posts.

The secret is to rely on as little npm packages as possible and if you do, rely on very very popular ones.
Which applies equally well to Ruby, Python, Go etc.
Some random thoughts as somebody who codes a lot of Node.js at work and a lot Go in my freetime (and sometimes at work):

Did you try using pprof and the other tooling go provides to better understand your performance limitations? Tooling is a lot better in go ecosystem for understanding CPU and memory consumption, so if/when you run into into issues with Node you're going to be in a world of pain (this is basically a large portion of my job in a large node.js code base in the $day_job). You'll basically have to resort to using lldb and heapdumps in the Node world. I'm surprised the number of concurrent clients you go with Go was so small. I know lots of people using Go and Gorilla websockets that exceed 1.5k clients with similar memory constraints. To be perfectly honest, it sounds like you're doing something wrong.

As of Go 1.4, the default stack size per goroutine was 2KB and not 4KB.

If you add in TypeScript, you'll have a better type system than Go provides in the Node ecosystem. That's a huge point for using Node.js, especially if there are multiple contributors and the code base lasts for many years.

I've found typescript an invaluable tool.

Having worked on large backend codebases in both TS and JS, the difference is stark. The TS API in my case had something like 90% less errors, and those were all subtle bugs in business logic.

On the other hand, the large JS API over time had all sorts of unexpected type errors and undefined behavior. I'm aware that this is anecdotal evidence, but TS is pretty much a no-brainer now on backend.

Is it not a problem in practice when TS type declarations and their associated JS code are written by different people at different times for different versions of a library?

I have no experience with maintaining TS projects over a longer period of time, but I have started some experimental TS projects where I was looking for type declarations.

What I found didn't seem to have any formal reference to a specific version of the library. I found that a bit scary. Not sure if it actually causes problems though.

(comment deleted)
Not caused us any yet over 3 years. You've got to think of it as the opposite, when we find a missing method because we at some point updated a library, we update the ts definition.

So it's not that it's a source of errors as most libraries are backwards compatible.

> Is it not a problem in practice when TS type declarations and their associated JS code are written by different people at different times for different versions of a library?

It can be. But most popular libraries have very good version syncing, or even include definitions in the module itself. In the worst case, or if there are no typings available at all, you can just import the plain js, and use it untyped, which can be acceptable if it is not used in a lot of places.

> didn't seem to have any formal reference to a specific version

Yeah. It mostly works if you install the latest version of them both at the same time. Sometimes I've had to fiddle with backing the definition or the module back a version to get them to match.

> The TS API in my case had something like 90% less errors

That sounds like a red flag to me. I hardly have any type related bugs in my pure JS server code. Must be a poor developer in the team. Too easy to blame JS for that.

That's the great thing about Type systems. It doesn't matter if the development team is amazing or not. Typescript still makes huge classes of errors impossible. It also speeds up development due to superior IDE support.
I've only used TS on the frontend, but I had the same opinion as you until I dug into a couple of TS projects. I don't know if I've ever caused a JS production bug that was a type error at its core, but I've had plenty of bugs like that in development. TS lets me cut out those little dev cycles where I'll write some code, rebuild the app, then see an obvious error in the JS console when I run the app. That adds up to a lot of wasted time over a week of coding. Smart autocomplete and smart variable renaming is also really nice, and I have way more confidence refactoring a TS project than a JS one. I think using TS does result in fewer production bugs, but the big win for me has been having fewer development bugs.
Tests are also a red flag for the same reason. Just suck less, I always say!
If Typescript can deliver 90% less errors with the same team, saying "Must be a bad developer" is a useless comment.

If TypeScript let's that developer make good code, then the variable is JavaScript.

A great carpenter can hammer a nail with a screwdriver, but that doesn't mean it's a good idea.

When the tools help you, a good developer can focus on making better things rather than not making mistakes.

Aside from the other responses you are getting, consider that the "90% less errors" doesn't actually tell you anything about the overall quality of either codebase. It could be a million lines of JS/TS with 10 errors found in the JS vs 1 in the TS. That would be a 90% reduction, but 10 errors in a large codebase would still be pretty excellent.
Types are good for more than just "int instead of string". What you consider a type related error is likely not the full set of errors a type system can prevent.
There are quite a few things I like about Go, but it's just so hard to leave the safety and reliability of TypeScript. Go's limited type system is its fatal handicap IMO.
It’s interesting how divisive Go’s type system is. For some people it’s the main reason they use it, for others it’s a fatal handicap.

FWIW I predominantly use PHP, so I have no dog in this fight.

I think a lot of the differences between how people view Go is based on where they are previously coming from. The C/C++ folks are used to worse or more complex type systems so they view Go as fresh air. The “better python” people love the easy to use type system and the performance improvements. But those “better python” people are used to things like package managers which the C/C++ have done just fine without. The Java people don’t understand why they should give up an expressive type system for faster compile times. (Might see this from people who love C++ templates too.)

At least that’s some generalizations I’ve observed, YMMV.

What a great comment. I know it’s a bit of a generalization but it’s helped me understand why some might get fanatical.
Good points - where one comes from really makes a difference.
I think some programmers want to carefully select a type specialized data structure, and others just bury the problem under a pile of array iteration.

And to hell with compile time. If the build takes 10x as long but we run 5% fewer prod servers, that pays for itself in minutes. And if type and nullability checking prevents one prod outage, that saves more time and stress than every build I've waited for this year.

I often don’t understand the Go teams obsession with compile times, but I’ve never worked on large Java or C++ code based that take forever to compile. I mostly want them to optimize for runtime performance even at the expense of compile time, but this does not seem popular with the core team.
You're right. You need to have worked on Google scale C++ programs to understand and acquire the obsession.
Java - expressive type system ? The bar must have been set really low by Go...
Java has generics, Go doesn't.
I am not too familiar with TypeScript (I only know that typing is optional for backwards compatibility).

What are some practical examples of strengths it has over Go?

> Tooling is a lot better in go ecosystem for understanding CPU and memory consumption, so if/when you run into into issues with Node you're going to be in a world of pain (this is basically a large portion of my job in a large node.js code base in the $day_job).

Is there no movement to change that? What about `node --inspect`?

node —-inspect is a great debugger but last I looked the tooling around memory wasn’t very good. As far as I know, the best practice is to use Ben Nooordius’ heapdump tool (which is super flakey between bugs and Linux oom killer) or lldb with a node plugin. Joynet has/had some cool stuff for illuminos based OSes and IBM seems to be working on some tooling but it’s not there yet.
This is a very interesting direction to take. I've built a lot of my personal stuff on JS, and TBH, the one thing I really wish I had right now was a statically typed codebase.

I spend a lot of time thinking about why I'm creating a certain data model, whether I might need to change something in future, etc. About 60% of my productive time is spent thinking about how and why, so I hardly refactor. However, when the need arises, I wish I had something like Kotlin.

For the past few months I've been writing new JS code in TS, adding types here and there, I haven't tried out Kotlin on JS, but I'm hoping to go there.

I'm learning Go, but for other reasons. I find JS to be performant, my oldest active codebase has been around since the v0.8 days.

I've refactored (or should I say, annotated) a >5K LOC node.js app to Typescript. It's an incredibly powerful system. Structural typing gives you 90% of the flexibility of dynamic typing with 90% of the security of classical static typing (of course this is just a feeling - it's not like I'm presenting a scientific result here).

I had more than 90% test coverage, with meaningful tests so I didn't really find too many bugs but I could delete a lot of tests and run-time checks which were making sure stupid input don't cause unexpected behavior.

When I check my commits & logs, LOC/hour didn't change significantly but bugs/month reduced to nearly half if my SQL skills aren't failing me.

You have two excellent options:

TypeScript and PureScript.

Even better, they play fairly well together. You can work in Purescript but still provide well typed integration points for contributors that can't.

And uh, no one here is talking about how fantastically slow Go channels are. But I just saw the code last night, and it's not hard for 20 year old techniques to beat out a "futex for literally every message send" technique.

I'm already using TypeScript, but you sometimes have to bend over backwards to get it to work nicely with a huge JS codebase (which I have).

Talking about channels, I haven't gotten there with my Go learning. Few things beat websockets + a nice wrapper (with simplicity). For example, I'm doing realtime transit, and as part of it I'm sending out hundreds of vehicle positions per 5-7 seconds.

On the back-end I have a pubsub through a gRPC stream, and I stream positions to socket.io topics. Works beautifully, I can't imagine having to roll it out manually over websockets. In another thread here, someone mentions how non-trivial working with websockets is in Rust. I think that until we have a socket.io (server) version for other languages, Node.js will always beat most other languages.

I'm having trouble understanding what you're saying here, but I think we agree?

Please do consider checking out PureScript. It's a lot of the good parts of Haskell and some of it's libraries look like sorcery they're so good.

I have no dog in this fight because I find static typing tedious, but it’s interesting that Flow doesn’t seem to be winning much mindshare.
Would Matrix be a good fit for this use case? There are a bunch of different server implementations and the (web based) protocol is relatively simple and very well documented.
Last I looked at the reference implementation ("synapse"), the authors hadn't started to optimize its memory usage yet, so it was pretty thrashy on the 512MB ram droplet I ran it on.
It's still the case, and it requires quite a lot of CPU time too.

There is no way Synapse is a good fit for something like RPi. Maybe when Dendrite is ready, it will make this possible.

I've been working on a similar project lately which also uses the gorilla/websocket library. I just tested connecting 1500 connections in parallel like was done in this link for Raspchat, and my application only uses 75 MB along with all other overhead within it. I'm not sure how this would cause a Raspberry Pi with 512MB memory to thrash and come to a crawl unless Raspchat has a ton of other overhead outside of connection management.
I feel his pain about the lack of decent WebSockets support in Rust. There's a few Websockets implementations but all of them are meant to run on a separate port from the web server. As in, they want you to run your web server on port 443 and the websocket on... Something else. Which makes zero sense (browsers will deny access to the second port because of security features related to SSL certificates).

Also, unless you go low level (lower than frameworks like Tokio) you can't easily access file descriptors to watch them (e.g. epoll) for data waiting to be read. It makes it difficult to use WebSockets for their intended purpose: Real-time stuff.

Rust needs a web framework that has built-in support for Websockets (running on the same port as the main web server) and also provides low-level access to things like epoll. Something like a very thin abstraction on top of mio (that still gives you direct access to the mio TcpListener directly).

In my attempts to get Tokio reading a raw file descriptor I just couldn't get it working. I opened a bug and was told that raw fd support wasn't really supported (not well-tested because it only works on Unix and cross-cross-platform stuff is a higher priority). Very frustrating.

I wish the Tokio devs didn't make the underlying mio TcpListener private in their structs.

Interesting - I wonder if a much thinner, simpler library is the answer here. The part with websocket on same port is interesting, did you hear reasons why that hasn't been done?
I don't use Go but I find the reasoning fueled by a confirmation bias to pick JS.

goroutines are now 2k. If you use 2 goroutines per connection that's 4k. If you have 10k connections that's roughly 20mb only which is very reasonable.

No clue about Go, but how can it take more memory then Javascript on Node? This is hard to believe.
I think it’s the event loop model reuses more per connection. He mentions nothing is stopping you from implementing an event loop model for the pub-sub in go, just there wasn’t any library support and he didn’t want to spend time building it out when it is the default model in Node.
It's a bad comparison, cause the Go version is using a "db" (key/value store) that tends to use quite a bit of memory.
We had pretty good number of sim.connections on 512MB instance. Unfortunately there is no enough details on methodology you used to compare, so I can't compare the number of clients it can support. I appreciate if you can do it yourself.

https://github.com/ro31337/hacktunnel

I know very little about both Node and Go (currently learning the latter but haven't done anything really interesting so far :) - but, really it's hard to believe that Elixir/Phoenix would be disregarded so quickly if the crux of the problem is to have good pub/sub support.
I was curious about the memory usage for Elixir. It’s the second time I’ve read (without details).
I actually did some experiments with Elixir, and my conclusion from benchmarking them was that Elixir is in fact significatively faster and consumed less memory than Node. In any case, Elixir is a great language, and even if BEAM was slower than Node I would still suggest to choose Elixir (+Phoenix) over JavaScript.
Why two (or three) go routines per connection? Why not one net socket (for reads) and two channels (one for writes, other for pub/sub) and select() between them? It seems like the OP is trying too hard to avoid event loops.
Please upvote parent. This is the first thing that struck my mind too. golang offers `select` for non-blocking calls. Deeply skeptical that node.js is the best solution for a chat service. (unless the backend is serving rendered UI's)
You generally need one thread to read for incoming messages, typical:

  for {
      select {
      case <-finish:
          done <- true
          return
      default:
          // read from the socket
      }
  }
You need another thread to listen for messages on a channel and send them out the socket typical:

  for {
      select {
      case <-finish:
          done <- true
          return
      case msg <- msg_out:
          // write msg to socket

      }
  }
Instead of?

  for {
    select {
    case <-finish:
      done <- true
      return
    case msg <- msg_out:
      // write handling
    case <-pubsub:
      // handle oob controls
    default:
      // read handling
    }
  }
You wont be able to write messages while blocked on the read in this scenario.
So, I am not a big fan of Javascript. I do not despise it or anything, I just never got to like it. I guess.

I did really love POE, though, so when I heard of Node.JS, I thought I will probably like this very much.

I am not sure what happened. I think it was the tutorials being always out of date. Node.js seem so be such a fast-moving target. I do not mind asynchronous, callback-driven code. But when a tutorial that was written three months ago fails to run because some library had a breaking API change in between, that tends to drive me away.

Think of Go what you want, but its policy towards backward compatibility is a big plus.

You're comparing core libraries of Go with non-core libraries of Node.js. Since Go libraries are not versioned without a tool like dep, you're putting trust that master doesn't have breaking changes vs hoping the maintainer follows SemVer correctly. In fact, even the core Go team has problem with this and has to revert changes to things under golang.org/x/ when they break backwards compatibility.
This seems to be a commonly-misunderstood point, so I would like to clarify a few things for readers. Go's tooling isn't great, but to characterize it as "trusting master" is inaccurate. Besides tools like `dep`, you can also vendor your dependencies manually or use submodules to pin to a known-good version of your dependencies. There are also tools like Nix which give you fully-reproducible builds. Just because Go has historically punted on the problem doesn't mean there aren't lots of better solutions than "trusting master". :)
Node's fs library changed within the last 2 years. I copy pasted code and it failed.
I guess you’re talking about well published changes resulting in new major versions being released?
Usually those things are pretty solid especially wrt backwards compat. I am curious as to what you copy/pasted and how it failed.
To be fair, you shouldn't be copy pasting code anyway. But I do understand your feelings, any tutorial from before 2016 probably isn't relevant anymore.
I remember the error now. I was using an older version of node (installed via apt) but the current version of the documentation.

Stupid mistake. Please ignore.

> In fact, even the core Go team has problem with this and has to revert changes to things under golang.org/x/ when they break backwards compatibility.

The Go team has no problem with this; /x/ packages are explicitly not covered by the backwards compatibility guarantee, and in fact they may be considered to disappear at any point.

I’m specifically talking about things not under the Go 1.0 compatibility guarantee because those things are obviously reverted. Different Go packages from the Go team have different guarantees, for example being compatible with the with the last two Go releases only. I’m on mobile so I can get you examples later after thanksgiving festivities are over but this definitely happens, but you wouldn’t notice unless you read a lot of the Go commits or run into the issue yourself.
> You're comparing core libraries of Go with non-core libraries of Node.js

OTOH Node.js is much more dependant on third party libraries than Go

If you liked POE you might enjoy Mojolicious :) http://mojolicious.org/
Writing async Mojo::IOLoop code is so much less verbose, easier than POE. It's possibly the most elegant Perl event loop. Thank you.
Thanks for the hint, it looks very interesting.

Most (well, all) Perl programming I do these days is simple scripts for automating tedious tasks, reporting, etc., and the odd CGI script.

Funny story, though: during my training I spent some time in a team doing in-house Perl development, and I sat next to Sebastian Riedel's desk! :-) It's a small world!

Please look into typescript. Makes js development sane.
One thing to note is that they were using boltdb, which is an in process K/V store designed for high read loads, and doing a lot of writes to it. boltdb also tends to use a lot of memory, as it's using a mmap'd file. The switch also moved them to sqlite, which I would say is a much better fit for what they are doing, but means a lot of this is an apples and oranges comparison.
And now to get ranty:

Boltdb was being handled badly: https://github.com/maxpert/raspchat/blob/79315d861968c126670... You should be using bolt's helper funcs, so you don't forget to close the transaction

https://github.com/maxpert/raspchat/blob/79315d861968c126670... At least this is actually deferred, but still.

https://github.com/maxpert/raspchat/blob/79315d861968c126670... These messages are horrifying, would be much better switching back to json and implementing a strategy like http://eagain.net/articles/go-dynamic-json/

It copies in an unlicensed "snowflake generation" thing? https://github.com/maxpert/raspchat/blob/79315d861968c126670... and then essentially relicenses it? That's illegal...?

They do have a better json pattern for some stuff: https://github.com/maxpert/raspchat/blob/79315d861968c126670... but I'd still steer away from reflection based, there's so much better ways of doing this stuff, like so: http://eagain.net/articles/go-json-kind/

https://github.com/maxpert/raspchat/blob/79315d861968c126670... This is a spinner?! No wonders the code is slow.

The general use of inheritance and very non-idiomatic code makes me think this is another person who ditched Go before understanding any of it. It seems a very popular sport.

As the time-old adage goes "a bad workman blames his tools".
And if you look where that code is called from - wrapped inside a mutex...

And then people wonder why it eats up 100% CPU and are completely maxed out before reaching 1000 requests/sec...

Yup... Bad slow language!

Illegal relicensing aside, this entire kerfuffle still makes anbad case for Golang.

If it's THAT much easier to make a more efficient server with a fully dynamic language using just coroutines then why should anyone ever use Go in this case?

Saying, "this implementation is bad" might work in another comparison, but given how much of Golang's implementation and engineering philosophy has been driven by an argument of "simplicity" it seems like we keep seeing precious few returns.

No. There was a lot of effort put into things that make no sense in the go version, including using a concurrent, lockfree, snapshot iterating hash array, where they'd most likely have better performance in this case with a map and a mutex.

This was NOT simple. The node.js version is simple. The Go version is overblown in complexity and abusing the language.

Rewrite the go version using just as simple of structures, and using a sqlite, and I'm extremely confident that it'd be more performant.

On the data structure: arguing for locked mutation is pretty an tricky because at low volumes it'll be faster and at higher traffic volumes it will be slower.

And since it's a library, who cares?

Golang isn't a language that's old enough to have accumulated a ton dissonance about the "right" way to do things. Golang encourages that kind of code and you see it all over GitHub. Too bad it's something of a trap.

JS is single threaded. Putting a mutex on one data structure is not going to reduce your performance significantly below single threaded.
Mutexs and futexes are more expensive than coroutines and Golang uses futexes a lot.
Go is multithreaded. Paying a small price to use multiple cores vs. only being able to use a single core? I'll take the multicore. It's not like concurrent lock-free datastructures are without cost...
If your workload is primarily I/O then coroutines will lose on the wall clock. And of course parallel workloads are fiddly.

But it's also worth noting Golang's current implementation of channels and messages is (as I've noted) going to be notably slow. Odds are you are going to avoid channels entirely if you care about maximizing multi-core performance.

Which is not to say that Golang "isn't fast". Just that it's not at all surprising that for some workloads NodeJS would outperform it.

Erhem, please excuse me. I was walking my dog as I wrote this and I accidentally inverted first statement.

For workloads where lots of concurrent and contingent I/O dominate, coroutines win handily.

As someone who has neither Go, nodejs, or RPi experience the results seem surprising. Many have already commented that the author must have been doing something wrong; the code is there for everyone to see, so could some wiser gopher take a look and tell whats actually going on here?
I did, and wrote some else in here, but a lot of it boils down to poor choices in the Go code. They're using an embedded K/V store designed for high read loads and are writing to it often, there's really complex concurrent lockfree datastructures, and very poorly designed deserialization systems. On the flip side, they switched to Node and to a db that can deal with mixed read/write, nixed all the complex datastructures, and node can deal with unstructured JSON.
It's interesting he's referencing the Disruptor pattern. I wrote the Go port of the Disruptor implementation that he links to (https://github.com/smartystreets/go-disruptor) a while back and it performed beautifully. That said, channels weren't slow either. Our finding showed that we could easily push 10-30 million messages per second through a channel, so I'm struggling to understand what he defines as slow. That said, with a few tweaks to the Go memory model and I think I could take the Disruptor project to completion. Without those tweaks, I have to do memory fences in assembly.
(comment deleted)
have you tried seeing how many messages you can push if you change how often the garbage collector runs? after seeing the Golang garbage collector blog post from cloudflare?
I never tweaked GC. With channels, I imagine there would be quite a bit of garbage. With the Disruptor, it was zero allocations and zero collections so no garbage was produced because every element in the ring buffer was long lived.
On a really slow, in-order-execution processor w/ tiny caches and fairly awful front-side bus that's also all responsible for managing the network connection over USB?

The thing about Intel non-Atom level hardware is that Intel has spent a lot of money over the better part of two decades packing processors full of features that make all kinds of theoretically inefficient things run pretty fast.

This is not true of the Broadcom SoCs on a Raspberry Pi.

Good point. Go channels weren't nearly as efficient on non-x86 hardware. When I compiled my Go Disruptor port and ran it on my Nexus 5 mobile phone, I was getting about 9 million messages/sec.
And your Nexus 5 is much, much faster than a Raspberry Pi :-)
This is surprising, as this is exactly the type of application where you'd think Go would perform well.
Perhaps this article is the CS version of "The Onion"?
I'm happy about this article, not because of what it says, but because of the popularity it got. NodeJS is not as bad as people think and I'm really excited about when it will regain status in the "pros" community.
More like Go has reached peak Go in terms of hype and people are going back to the stack they know best. Go is absolutely not a silver bullet, its community hates "creative solutions" and at the end of the day it's not that enjoyable to write for a lot of people. Go ecosystem isn't that big either compared to JS.
(comment deleted)