DEV Community

Dixon Osure
Dixon Osure

Posted on

Discovering JavaScript Functions Through the Lens of Go

I recently started learning JavaScript, and I didn't come in blind. I had Go in my head — how it structures things, what it cares about, what it refuses to let you do. And honestly, that made learning JS a strange experience. Not hard, just... revealing.

Every time JS did something different from Go, I found myself asking why. Why does it work that way? What problem is JS solving that Go solves differently? I started learning two things at once — JavaScript, and a deeper appreciation of choices Go made that I'd taken for granted.

This is that journey, through functions.


Go Wants to Know Everything Upfront. JS Doesn't.

The very first thing I noticed: Go demands you declare your types. Every parameter, every return value — the compiler needs to know.

func greet(name string) string {
    return "Hello, " + name
}
Enter fullscreen mode Exit fullscreen mode

JavaScript has no such requirement:

function greet(name) {
  return "Hello, " + name;
}
Enter fullscreen mode Exit fullscreen mode

At first I kept thinking — but what type is name? What if someone passes a number? And the answer is: JavaScript doesn't stop them. It'll just run, coerce the value, and do its best. greet(42) gives you "Hello, 42" without complaint.

This isn't sloppiness — it's a deliberate tradeoff. JS was built to be flexible and forgiving, especially in a browser environment where you don't control the input. Go was built for systems where you really need to know what's going into each function. Two different problems, two different philosophies.

The practical consequence: JavaScript errors tend to show up at runtime, not compile time. You lose the safety net Go's compiler gives you, but you gain a kind of freedom to move fast and figure things out as you go.


JS Solves the "Sensible Default" Problem Natively

In Go, there's no such thing as a default parameter value. If you want a fallback, you handle it yourself:

func greet(name string) string {
    if name == "" {
        name = "stranger"
    }
    return "Hello, " + name
}
Enter fullscreen mode Exit fullscreen mode

The first time I saw JavaScript's version of this, I genuinely paused:

function greet(name = "stranger") {
  return "Hello, " + name;
}

greet();         // "Hello, stranger"
greet("Alice");  // "Hello, Alice"
Enter fullscreen mode Exit fullscreen mode

The default lives right in the signature. It's part of the function's contract — readable, visible, impossible to miss.

What I found interesting is that Go's approach isn't just a missing feature — it reflects a philosophy. Go wants function signatures to be unambiguous. A function either gets a value or it doesn't; you deal with it explicitly. JavaScript's approach says: let the function itself handle graceful fallbacks so the caller doesn't have to think about it.

Neither is wrong. They're just solving the same problem with different priorities.


How Each Language Handles Errors — This One Really Made Me Think

This was the biggest conceptual shift for me, and it changed how I understood both languages.

In Go, errors are just values. A function that can fail returns two things — the result and an error — and you check both:

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}

result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}
Enter fullscreen mode Exit fullscreen mode

Every function call that can fail makes that failure visible and forces you to deal with it on the spot. You can't accidentally ignore an error.

JavaScript takes a completely different approach — errors are thrown and caught:

function divide(a, b) {
  if (b === 0) throw new Error("Cannot divide by zero");
  return a / b;
}

try {
  const result = divide(10, 0);
} catch (err) {
  console.error(err.message);
}
Enter fullscreen mode Exit fullscreen mode

What struck me is how this changes the shape of the code. In Go, error handling is part of the normal flow. In JavaScript, the structure is separated into try and catch blocks. The function itself returns just one thing; errors travel on a different channel entirely.

The risk with JavaScript's model is that it's easy to forget the try/catch. An unhandled thrown error can bubble all the way up and crash your program in a place far removed from where the problem happened. Go makes that much harder to do by accident.

But I came to see the beauty in JavaScript's model too — especially for deeply nested async code, where passing errors as return values would get messy fast. Exceptions let errors escape cleanly without having to be explicitly threaded through every layer.


Functions as Values — JS Leans Into This Much Harder Than Go

Both Go and JavaScript treat functions as first-class values. You can assign them to variables, pass them around, return them from other functions. I knew this from Go:

double := func(n int) int {
    return n * 2
}
Enter fullscreen mode Exit fullscreen mode

But JavaScript doesn't just allow this — the whole language is built around it. Arrays have built-in methods that take functions as arguments:

const nums = [1, 2, 3, 4, 5];

nums.map(n => n * 2);          // [2, 4, 6, 8, 10]
nums.filter(n => n % 2 === 0); // [2, 4]
nums.reduce((sum, n) => sum + n, 0); // 15
Enter fullscreen mode Exit fullscreen mode

Coming from Go, this was eye-opening. Go has no built-in map or filter — you write loops. JavaScript treats these as fundamental operations on collections, built right into the language.

And those little n => n * 2 things? Those are arrow functions — JavaScript's shorthand for inline functions. You'll see them everywhere:

// These all do the same thing:
function double(n) { return n * 2; }
const double = function(n) { return n * 2; };
const double = (n) => n * 2;
const double = n => n * 2; // parentheses optional for single param
Enter fullscreen mode Exit fullscreen mode

Go has anonymous function literals, but JavaScript went further — arrow functions exist partly for brevity, and partly to solve a specific problem with how this works (more on that below).


Closures Are the Same Idea, But JS Uses Them More Aggressively

Closures — functions that remember the environment they were created in — work the same way in both languages:

func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}
Enter fullscreen mode Exit fullscreen mode
function makeCounter() {
  let count = 0;
  return () => ++count;
}

const counter = makeCounter();
counter(); // 1
counter(); // 2
Enter fullscreen mode Exit fullscreen mode

Identical concept. But JavaScript leans on closures much more heavily than Go does — partly because JS doesn't have Go's explicit struct-based encapsulation, so closures often are the encapsulation. Private state in JavaScript is frequently just a variable in a closure that nobody outside can touch.

One thing I learned the hard way: JavaScript also has var (older style), and var does not behave like := in Go. var ignores block scope — a loop variable declared with var is the same variable across all iterations of the loop, which causes bizarre closure bugs. Always use let or const. Treat var like it doesn't exist.


The this Problem — Nothing Like It in Go

In Go, methods have explicit receivers. You always know exactly what a method is operating on:

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}
Enter fullscreen mode Exit fullscreen mode

JavaScript uses this instead:

class Circle {
  constructor(radius) { this.radius = radius; }
  area() { return Math.PI * this.radius ** 2; }
}
Enter fullscreen mode Exit fullscreen mode

That works fine when you call c.area() directly. But JavaScript's this is dynamic — it changes depending on how the function is called, not where it was defined. The moment you pass a method as a callback, this can become something unexpected:

const c = new Circle(5);

// This breaks — `this` is no longer the circle inside the callback
setTimeout(c.area, 1000);

// This works — arrow function captures `this` from the surrounding scope
setTimeout(() => c.area(), 1000);
Enter fullscreen mode Exit fullscreen mode

This was the most genuinely confusing thing I encountered coming from Go. In Go, the receiver is explicit and never changes. In JavaScript, this shifts depending on call context, and you have to know the rules to avoid getting burned.

Arrow functions were invented partly to solve this — they don't have their own this, they inherit it from wherever they were created. That's why you'll see arrow functions used as callbacks so often in JavaScript codebases.


What Go Taught Me About JavaScript

Going through all this, something clicked for me: Go and JavaScript aren't just different syntax for the same ideas. They reflect genuinely different beliefs about what a programming language should optimize for.

Go optimizes for clarity and correctness — types, explicit errors, predictable receivers. JavaScript optimizes for flexibility and expressiveness — dynamic types, exceptions that escape cleanly, this that adapts to context, functions that compose in powerful ways.

Neither got it wrong. They made different bets.

Knowing Go made me a more thoughtful JavaScript learner, because I kept asking why instead of just accepting the syntax. And every time I found an answer, I understood both languages a little better.


Resources:

Top comments (0)