Why I moved from Nodejs to Go

Over the past 8 years, I have been extensively working with Nodejs on the backend developing various types of I/O intensive web applications. I have developed tens of small scale and large scale services that are handling 1000's of operations in no time and are surfacing applications across various product domains.

I was fascinated by the handling of files in nodejs. Allowing to pipe streams and manipulate chunks is extremely powerful. So far, no language has shown the ease and the speed with which these operations are carried out. Sometime back I decided to give GO a go simply because have been hearing really good stuff about it from fellow engineers. The results, to my surprise were drastically different. I knew GO will perform better but as I put the system under load, it became clear, GO simply beats nodejs by a fair margin.

Let's try to build a CPU intensive program and compare how both perform. Consider a factorial program. Before moving forward, heres what a factorial is -

In mathematic the factorial of a non-negative integer denoted by n! is the product of all positive integers less than or equal to n. The factorial of n also equals the product of n with the next smaller factorial:

n!=n×(n−1)×(n−2)×(n−3)×⋯×3×2×1=n×(n−1)!

For example, 5! = 5×4! = 5×4×3×2×1 = 120.
The value of 0! is 1.

Heres how it can be written in GO (assuming recursion is understood.)

package main

import (
	"fmt"
	"time"
)

func computeFactorial(n int) int {
	if n <= 1 {
		return 1
	}
	return n * computeFactorial(n-1)
}

func main() {
	start := time.Now()
	result := computeFactorial(20)
	elapsed := time.Since(start)
	fmt.Printf("Result: %d\n", result)
	fmt.Printf("Time taken: %s\n", elapsed)
}

In this example, we calculate the factorial of a number (20) using a recursive function. Go's efficient concurrency model and compiled nature allow it to handle CPU-bound tasks like this more efficiently. On a 6 Core Machine, heres the output -

Result: 2432902008176640000
Time taken: 207ns

NodeJS Example -

function computeFactorial(n) {
	if (n <= 1) {
		return 1;
	}
	return n * computeFactorial(n - 1);
}

const start = process.hrtime.bigint();
const result = computeFactorial(20);
const elapsed = process.hrtime.bigint() - start;
console.log("Result:", result);
console.log("Time taken:", elapsed, "ns");

In Nodejs, the same factorial computation is performed using a recursive function. Here is the result on a 6core machine -

Result: 2432902008176640000
Time taken: 17309n ns

However, JavaScript's single-threaded event loop and interpreted nature can result in slower performance compared to Go for CPU-bound tasks.

The difference in performance becomes more noticeable as the computational complexity increases or when running multiple parallel computations. Go's ability to leverage multiple cores efficiently and its compiled nature provide an advantage for CPU-bound tasks, making it a favorable choice in such scenarios.

It is so evident from the above, for a very simple computation use case, GO is not just easy, but is also pretty optimum. I have compiled some common reasons on why GO is so much better than nodejs -

  1. Performance: Go is known for its high performance and efficiency. It compiles to machine code, which allows it to execute faster than interpreted languages like JavaScript, which is used in Node.js. Go's lightweight goroutines and built-in concurrency features also make it highly scalable and efficient in handling concurrent tasks.
  2. Concurrency and Parallelism: Go has excellent support for concurrency and parallelism. Goroutines and channels in Go enable concurrent programming with ease. It allows developers to efficiently handle multiple requests simultaneously, making it well-suited for building scalable and high-performance backend systems.
  3. Static Typing: Go is a statically typed language, meaning it performs type checking at compile-time. This helps catch errors early in the development process, making it easier to write reliable and maintainable code. In contrast, Node.js uses JavaScript, which is dynamically typed, allowing more flexibility but potentially leading to runtime errors.
  4. Strong Standard Library: Go comes with a rich standard library that provides a wide range of functionality out of the box. It includes packages for networking, encryption, HTTP, file handling, and more. This allows developers to rely on the standard library without having to rely heavily on external dependencies.
  5. Concurrency Safety: Go has built-in features like goroutines and channels, which make it easy to write concurrent code that is less prone to race conditions and other common concurrency issues. Node.js, on the other hand, requires additional effort and the use of external libraries to achieve similar levels of concurrency safety.
  6. Deployment and Execution: Go compiles to a single binary that can be easily deployed and executed on various platforms without requiring the installation of any additional runtime dependencies. Node.js applications, on the other hand, require the Node.js runtime environment to be installed on the target server, which adds complexity to deployment.

More practical examples to follow soon.