DEV Community

jorin
jorin

Posted on • Updated on • Originally published at jorin.me

Why Go for Node.js Developers

The following is not an attempt to convince anyone that one technology is better than the other, instead I like to explore the strong points of each so we can better choose the appropriate tool for a given task.

I have been working with Node.js for more than five years now and in the last year I have been using Go to build various things - bigger projects and also various open source tools.

At this point I like to document my thought process for choosing between these language for solving a given task.

This article might be the most useful for people that, like me, have used Node.js in the past and now hear a lot of talk about the success everyone has with switching to Go.

Just to be clear about it, we are talking about server-side technologies here. Some people do actually use Go in the browser, but this is not what this article is about.

Also note that even if this article or other reasons convince you that you are better off using a different technology for what you are trying to do, it is never a good idea to rewrite your whole system at once. Find components that can be easily decoupled and make changes incrementally.

Another thing to keep in mind is to don't take "the right tool for the job" to an extreme. Don't underestimate the complexity of working with multiple ecosystems at once. Be careful about introducing new technology in your system. Complexity always comes with a cost.


All this being said, let's talk about Go.

There are certain issues that you might run into when using Node.js, which you can solve by using Go. There are other issues Go won't solve for you. There is no silver bullet.


You might want to have a look at Go if you run into one the following issues:

  • Your software needs to run on hardware with little available memory or your Node application uses more memory than acceptable in other ways.

Let's compare the memory usage of these two small programs, the first in JavaScript, the second in Go:

setTimeout(() => {}, 100000)
Enter fullscreen mode Exit fullscreen mode
package main
import "time"
func main() { time.Sleep(100 * time.Second) }
Enter fullscreen mode Exit fullscreen mode

On my Laptop the JavaScript process uses 8.6MB while the Go one uses 380KB. The difference is not really surprising since Go is compiled to machine code upfront and has a really minimal runtime, but it is something you need to be aware of for certain kind of software.

  • The application needs to start up as fast as possible because it restarts frequently or you are shipping CLI tools or something like that.

While Node.js has an excellent startup time compared to many other runtimes, it can't keep up with Go:

console.log('hello')
Enter fullscreen mode Exit fullscreen mode
package main
import "fmt"
func main() { fmt.Println("hello") }
Enter fullscreen mode Exit fullscreen mode

When running these two programs with the time command, the node version takes around 120ms to run while running the compiled Go program takes 10ms.

  • The work a service is doing is computing intensive and CPU-bound.

Node.js is often praised for its performance for web applications compared to other environments such as Python or Ruby. That performance comes from the asynchronous programming model of JavaScript runtimes. By utilizing an event loop together with asynchronous functions a single Process can performance many tasks concurrently. However that only applies to tasks that are IO-bound — meaning tasks that are slow because they have to wait for the network or the disk. These kind of tasks are very common in web applications since they often need to get information from or to other resources such as files on disk, databases or third-party services.

If your performance is constrained by raw computing power, Go might be an interesting alternative. Through its static type system and its direct compilation to machine code, its performance can be better optimised and it is faster than any JavaScript engine in many scenarios.

Additionally Go can run code in parallel. While Node.js has a great concurrency model, it does not support parallel execution. A Node.js process always runs in a single thread. Go can utilize all CPUs the machine provides and Go comes with simple concurrency primitives built into the language. By using Goroutines and channels one has a simple way to orchestrate a parallel system without depending on mutexes and manual resources locking.

If your problem is CPU-bound and maybe even parallizable, Go should be able to give you great performance gains over Node.js.

In the extreme case Go will perform N times better — with N being the number of cores your program can make use of. But keep in mind that in many cases you can scale Node by simply running more processes. Scaling on a process level versus a thread level comes with a certain overhead, but unless you are also constrained in one of the above mentioned restrictions, it might not be an issue for you. The simplest way to coordinate multiple processes is using Nodes's cluster module. I also encourage you to have a look at other technologies such as ZeroMQ though.

  • The Deployment of your application is limited by not having additional dependencies available on the machine or by file size the deployment is allowed to use.

Node.js is required to be installed on the host machine. Additionally all files need to be copied and dependencies installed on the machine using npm install. Dependencies often contain native C libraries and must be installed on the host itself instead upfront.

In Go the whole program and all dependencies can be compiled into a single, statically linked binary. The binaries can be cross-compiled from any platform.

The size of a Linux binary for the above hello Go program is 1.2MB.

In case a system is using Docker containers, the file size savings can be even more severe:

Building the Node version using the following Dockerfile results in an image of 676MB.

FROM node
WORKDIR /usr/src/app
COPY index.js .
CMD ["node", "index.js"]
Enter fullscreen mode Exit fullscreen mode

An image for the Go binary using the following Dockerfile results in an image of 1.23MB.

FROM scratch
COPY hello /
ENTRYPOINT ["/hello"]
Enter fullscreen mode Exit fullscreen mode

Note that if you have many containers running and you use the same base image for them, it is reused and the disk space is only used once.

There are also lightweight alternative containers for running Node — node:slim at 230MB and node:alpine at 67.5MB. They come with their own caveats though.

Go containers can only be this small if you don't have any external dependencies. Otherwise you might also need an Alpine or Debian image for Go and will end up at a similar images size. Also keep in mind, that to create a small Go container you need a more complex build process since you need to create the binary first and then copy it into a container.


There are many other soft factors on which people base their decision of switching to Go:

  • Go has one paradigm for error handling compared to 3+ ways in JavaScript.
  • Go has convenient tools for testing, documenting and formatting code built into the default toolchain.
  • Static typing allows for tight editor integration including autocompletion, inline docs, go to definiton, renaming symbols, …

In my opinion non of these arguments can justify rewriting an exiting codebase and it might be more benificial to invest in improving your coding guidelines in JavaScript, using tools like prettier and writing proper documentation and tests which is equally possible in JavaScript.


If any of the above arguments convinced you, that Go might be a more suitable tool for the problem you are trying to solve, keep in mind that there are other languages that share many characteristics with Go. If your problem is extremely performance critical, a possibly even more suited solution might be a language such as Rust or C. Go still comes with a runtime and uses a garbage collection with can pause your program at any time. The main reason why you would look at Go instead of Rust is because the barrier to getting started is way lower. Go is a way simpler language with way less concepts to keep in your head. It is extremely quick for people to get started and be productive.


When not to use Go

If none of the above points are of concern to what you are trying to achive, you might also use any other language than Go. There is no good reason for you to throw away all your work and rewrite it in another language.

In fact I would argue that you might actually be more productive sticking to Node. JavaScript and its ecosystem come with a lot of powerful tools and abstractions, which allow you to think more about your problem domain instead of the details of the technical implementation.

Being able to load your code in a REPL and try it out and inspect your data live, allows you to explore ideas really fast. If you write automated tests - as you should in any case - you will also catch issues static typing can catch for you.

Which of these two programs would you prefer to to write, read and reason about?

This:

const toInts = strings => strings.map(s => parseInt(s, 10))
console.log(toInts(['1', '2']))
Enter fullscreen mode Exit fullscreen mode

Or this:

package main

import (
    "fmt"
    "strconv"
)

func toInts(strings []string) ([]int64, error) {
  var res []int64

  for i, s := range strings {
    r, err := strconv.ParseInt(s, 10, 64)
    if err != nil {
      return res, fmt.Errorf("failed parsing element at index '%d': %v", i, err)
    }
    res = append(res, r)
  }

  return res, nil
}

func main() {
  fmt.Println(toInts([]string{"1", "2"}))
}
Enter fullscreen mode Exit fullscreen mode

At this point if you feel like going deeper into a debate of static vs. dynamic languages, I recommend you this interesting article.


As you can see, there is no right answer. It depends on your problem. And even then, there might not be an obvious winner.

That being said, it is never a bad idea to explore a new language and its way of thinking. If you like to have a look at Go, I recommend you to checkout this comparison here:
Go for JavaScript Developers

Top comments (19)

Collapse
 
orask profile image
Oras Al-Kubaisi

Good read, thank you for taking the time to write it and link to different sources. I totally agree with the point of there is no right or wrong answer and its totally dependant on the software and priorities.

Collapse
 
orkon profile image
Alex Rudenko

Have you tried the tools which compile a node program into a binary? i.e. which package Node and your program into a single executable? For example, github.com/zeit/pkg It does not solve the size problem (about 45 Mb, in my case) and the startup time. But it might be easier to distribute.

Collapse
 
jorinvo profile image
jorin

Yes, pkg is great! Good that you mention it!

Collapse
 
hrmny profile image
Leah

In rust you could use

let to_ints = |s_vec| s_vec.iter().map(|s| s.parse::<i64>().unwrap_or_default(0)).collect::<Vec<_>>();
println!("{?:}", to_ints(vec!["1","2"]));
Collapse
 
bgadrian profile image
Adrian B.G.

I agree and I also took the same path. Writing node apps and realized that Go is a good complementary language, I see them playing well together as in keeping the most business logic in JS and the fast ops in Go.

Collapse
 
sammyisa profile image
Sammy Israwi • Edited

On my Laptop the JavaScript process uses 8.6MB while the Go one uses 380KB

Quick question, how can you tell how much memory each process uses? And why do you think Node uses so much more memory in this case?

Collapse
 
jorinvo profile image
jorin • Edited

Easiest way to see the resource usage of a process is to open Activity Monitor on Mac or Task Manager on Windows.

The memory usage of Node.js is surprising low if you think about how much the environment is doing for you. The runtime translates your code to actual machine code on the fly and while your program is running, it is still being analyzed and optimized so that hot code paths become more efficient and so on.
Everything you have with a compiled language like Go in the separate build step, the VM in a language like JavaScript is doing for you at runtime.

Hope this helps a bit. If you are interested there are many interesting talks and articles about the internals of JavaScript (and other) engines, for example this one: youtube.com/watch?v=p-iiEDtpy6I

Collapse
 
philmod profile image
Philippe Modard

You should use an alpine version of the node docker base image, it will be way lighter. Maybe github.com/mhart/alpine-node

Collapse
 
jorinvo profile image
jorin

Yes, I mentioned that above already. You only need to be aware of all the gotcha that come with not using a full blown distro like Debian or Ubuntu.

Collapse
 
philmod profile image
Philippe Modard

What are these gotchas?

Thread Thread
 
jorinvo profile image
jorin

I do like alpine and recommend everyone giving it a try, but if people already have a complicated setup running on another system, it will be a lot of work to port it. You need to find out how to install all the dependencies you are using, deal with differing versions and varying configuration files. And there will be hard to figure out little differences when you - for example - need to compile some C dependency and can't figure out why it's not working with the C toolchain on that system.
Still, give it a try, maybe you don't have any problems with your setup. Docker image size is also not such a big problem if you use a lot of the same images and they can be reused.

Collapse
 
jorinvo profile image
jorin

More interesting thoughts on which images you want to choose: derickbailey.com/2017/05/31/how-a-...

Collapse
 
coolgoose profile image
Alexandru Bucur

Technically, you can have 'threads' in node.js like github.com/andywer/threads.js or github.com/audreyt/node-webworker-...

Now imho, if you want to do an experiment regarding performance and test something else than node, you can try kotlin since it has decent JS interop kotlinlang.org/docs/reference/js-i...

Collapse
 
lluismf profile image
Lluís Josep Martínez

Does Go have an IDE comparable to those of NET/Java ecosystems?

Collapse
 
jorinvo profile image
jorin • Edited

If you like IDEs, Goland by JetBrains is pretty good. jetbrains.com/go

But Editor Integration is also good with Go. Many people like to use Visual Studio Code. A lot of Go users are using Vim too.

Collapse
 
equan profile image
Equan P.

i think there is a project called Nexus.js now that use threads. Maybe you ever compare it with Go? Great article by the way.

Collapse
 
jorinvo profile image
jorin

Thanks, Nexus looks interesting! Let's see how it goes. Seems to be still pretty new.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
jorinvo profile image
jorin

It's pretty efficient, but it has to pause the program a bit. In all but the most extreme cases you won't notice the break through.
They even parallelize GC.