Skip to main content
Back to Blog

Choose a Scripting Language. Choose Go.

or Zig.

9 min read
gopythonnodejszig

Many in the development world constantly push for Python or Node.js for virtually everything, suggesting that using a “compiled” language, or, heaven forbid, attempting to optimize anything “prematurely” is the root of all evil. What a farce!

This craze for “easiness” and the immediate availability of packages has directly led to the heavily bloated ecosystems of PyPI and NPM. This, in turn, has resulted in an overwhelming amount of security issues due to supply chain attacks and massive runtime costs (both memory and compute). Developer cost increases due to the reliance on extra tooling for linting and rule enforcement that isn’t built into the languages themselves, and lengthy package installation times in CI/CD pipelines.

If you choose today to write a Node or Python project using their native tooling (npm or pip), it simply won’t be fast. And the code? Slow, prone to bugs, and difficult to maintain.

Another farce is the claim that Python and Node are inherently easier to use than compiled languages.

  • Python’s Package Manager Nightmare: If you want to solve the inherent problems of Python and pip, you need to use newer tooling like uv. While uv is undeniably awesome (precisely because it’s built with a compiled language!), now you have the burden of knowing the potential drawbacks of using an alternate package manager.

  • Node’s Module Mess: The story is even worse with Node.js and npm. Developers must choose between CJS (CommonJS) and ESM (ECMAScript Modules). Switching between these standards often necessitates changing your code (e.g. require vs. import!). A CJS library cannot load an ESM library, forcing developers to constantly be aware of module compatibility. Sometimes, libraries don’t fully support either CJS or ESM, leading developers to bundle their entire application using tools like esbuild into a single JavaScript file to remove all require or import statements entirely. Of course, you should also minify your JS code for optimization, so I guess you still have a compilation phase after all.

    And we haven’t even mentioned TypeScript yet—which should be mandatory for any serious project. TypeScript adds another compilation step on top of bundling, so now you’re compiling code that provides zero runtime performance benefit. You pay the compilation tax without the speed dividend. Your workflow looks roughly like this:

    graph TB
    
    subgraph "Development"
        A[Code .ts] --> B[npm install]
        B --> C[npx tsc]
        C --> D[node dist/rectangle.js]
    end
    
    subgraph "Production"
        A --> E[npm install]
        E --> F[npx tsc --noEmit]
        F --> G[npx esbuild rectangle.ts --bundle --minify]
        G --> H[node rectangle.min.cjs]
    end

    Compare this to Go:

    graph TB
    
    subgraph "Development"
        A[Code .go] --> B[go run rectangle.go]
    end
    
    subgraph "Production"
        A --> C[go build -o rectangle]
        C --> D[./rectangle]
    end

    Yeah, very simple indeed.

On the horizon, there is Go. When compared to giants like C++ or Rust, Go is a terrible compiled language: it’s slower, has bloated memory consumption, and a weak multithreading model (multiplexing OS threads to goroutines is suboptimal for CPU cache).

But compared to Python and Node? Go is infinitely nicer to work with for simple scripts. If a project is non-critical, requires strict, streamlined development, and yet allows for a fast pace, it should be in Go. The rest can be done in C++ or Rust.

Go offers several distinct advantages:

  1. Extremely Fast Compilation: Go compiles so quickly that you can use the command go run ./cmd/mycli, and it feels like using Python. You could even alias goo="go run" if you desire a single command execution.
  2. Superior Package Management: go mod is a better package manager: fast, safer, and far more integrated with built-in linters and static analysis tools than either NPM or pip/uv.
  3. Simple Workflow, Static Binary: The workflow is simple: code \rightarrow go build ./cmd/mycli \rightarrow done. You get a static binary that is far less bloated, fast enough for most purposes, and decisively faster than Python.
  4. High-Quality Standard Library: While Go’s standard library is weak in some areas, its quality is significantly higher in the aspects that matter most for modern development (e.g., net/http).

A simple benchmark—a bubble sort on a 10,00010,000 array of float32\text{float}32 (fixed, randomly pre-generated)—shows how Go simply outperforms the others in compute-heavy scenarios. The bubblesort is so simple that JavaScript’s V8 likely compiles it to similar assembly as Go, yet the results speak for themselves.

CPU-Heavy Workload (Bubble Sort - Compiled Binary)

BenchmarkCommandMean Time (± σ)
Go./bubblesort30.6 ms±1.6 ms30.6 \text{ ms} \pm 1.6 \text{ ms}
Node.jsnode bubblesort.js64.6 ms±2.5 ms64.6 \text{ ms} \pm 2.5 \text{ ms}
Node.js (minified)node bubblesort.min.js64.1 ms±3.1 ms64.1 \text{ ms} \pm 3.1 \text{ ms}
Pythonpython3 bubblesort.py1.897 s±0.014 s1.897 \text{ s} \pm 0.014 \text{ s}

Summary: Go ran 2.1x faster than Node.js and 62x faster than Python.

“But what about compilation time?” you might ask. Let’s include a cold build scenario—deleting the binary before each run—to measure the true cost of Go’s compilation step against interpreted languages:

Cold Build (Bubble Sort - Including Compilation)

BenchmarkCommandMean Time (± σ)
Gogo build && ./bubblesort77.5 ms±3.6 ms77.5 \text{ ms} \pm 3.6 \text{ ms}
Node.jsnode bubblesort.js63.3 ms±1.8 ms63.3 \text{ ms} \pm 1.8 \text{ ms}
Node.js (bundled)npx esbuild ... && node bubblesort.min.js169.3 ms±2.9 ms169.3 \text{ ms} \pm 2.9 \text{ ms}
Pythonpython3 bubblesort.py1.933 s±0.035 s1.933 \text{ s} \pm 0.035 \text{ s}

Node.js direct execution (plain JavaScript, no TypeScript) beats Go by a small margin. However, Go still outperforms the typical Node workflow that includes bundling—and absolutely demolishes Python. This means for any compute-heavy project—CI scripts, data processing, tooling—Go remains a superior choice.

You might ask, “What about typical I/O-bound applications?” The following benchmarks compare a CLI application using common argparser and YAML parsing libraries—representative of real-world tooling.

CLI (Compiled Binary)

BenchmarkCommandMean Time (± σ)
Go./rectangle test.yaml1.1 ms±0.1 ms1.1 \text{ ms} \pm 0.1 \text{ ms}
Node.js (minified)node rectangle.min.js test.yaml19.6 ms±1.1 ms19.6 \text{ ms} \pm 1.1 \text{ ms}
Pythonpython3 rectangle.py test.yaml19.9 ms±1.5 ms19.9 \text{ ms} \pm 1.5 \text{ ms}
Node.jsnode rectangle.js test.yaml23.2 ms±2.1 ms23.2 \text{ ms} \pm 2.1 \text{ ms}

Summary: Go ran 17-20x faster than all interpreted alternatives. Despite claims of I/O bottlenecks leveling the playing field, Go dominates—likely due to both library quality and interpreter initialization overhead exceeding the actual workload.

CLI Cold Build (Including Compilation)

BenchmarkCommandMean Time (± σ)
Pythonpython3 rectangle.py test.yaml19.6 ms±1.2 ms19.6 \text{ ms} \pm 1.2 \text{ ms}
Node.jsnode rectangle.js test.yaml24.1 ms±2.7 ms24.1 \text{ ms} \pm 2.7 \text{ ms}
Gogo build && ./rectangle test.yaml107.1 ms±4.9 ms107.1 \text{ ms} \pm 4.9 \text{ ms}
Node.js (bundled)npx esbuild ... && node rectangle.min.js test.yaml128.3 ms±2.7 ms128.3 \text{ ms} \pm 2.7 \text{ ms}

In this worst-case scenario (cold build for a trivial workload), Python wins. Go’s compilation overhead shows—yet it remains decent and still beats the typical Node.js bundling workflow.

Python’s cold-start “win” is misleading. Yes, ~20ms beats Go’s 107ms for trivial workloads—but this encourages a dangerous pattern: writing slow glue code around fast native libraries (NumPy, etc.). You end up optimizing the wrong thing. The hotpath gets abstracted into C bindings while your actual logic—the part you control—runs at 62x slower. Why not write in a language where your code is also fast? Go compiles quickly, has excellent libraries, and the syntax learning curve is trivial compared to the ongoing cost of Python’s performance ceiling.

To be fair, bundling does help Node.js—minification improves runtime slightly. But step back: you’re now maintaining a TypeScript → JavaScript → bundled/minified JavaScript pipeline, complete with tsconfig, esbuild config, and build scripts. After all that, you still have JIT compilation, an embedded runtime, and interpreter overhead. Go gives you a single go build command, produces a static binary, and runs faster. Same compilation step count, vastly simpler toolchain, better result.

CLI Cold Build with TypeScript (The Real Node.js Workflow)

Let’s be honest: nobody in their right mind would use JavaScript without typing anymore. TypeScript is the de facto standard—but it requires compilation. Unlike Go, this compilation provides zero runtime performance benefit. You pay the compilation tax without the speed dividend.

BenchmarkCommandMean Time (± σ)
Pythonpython3 rectangle.py test.yaml20.1 ms±1.1 ms20.1 \text{ ms} \pm 1.1 \text{ ms}
Gogo build && ./rectangle test.yaml107.5 ms±7.9 ms107.5 \text{ ms} \pm 7.9 \text{ ms}
TypeScriptnpx tsc && node dist/rectangle.js test.yaml414.8 ms±7.8 ms414.8 \text{ ms} \pm 7.8 \text{ ms}
TypeScript (bundled)npx tsc --noEmit && npx esbuild ... && node rectangle.min.cjs test.yaml505.5 ms±6.0 ms505.5 \text{ ms} \pm 6.0 \text{ ms}

Summary: Go obliterates TypeScript—running 3.9x faster than the basic TypeScript workflow and 4.7x faster than the bundled version. Python demolishes both, running 20x faster than TypeScript. The Node.js ecosystem’s compilation overhead is staggering, yet delivers none of the runtime benefits that Go’s compilation provides.

The TypeScript Paradox: You adopt TypeScript because you need types. But if you need types, why not use a language that gives you types and performance? TypeScript’s 400-500ms cold start vs Go’s 107ms is indefensible. You’re paying the compilation tax with zero runtime dividend.