DEV Community

Cover image for JavaScript vs Go: A Deep Dive into Syntax and Philosophy
Maksym
Maksym

Posted on

JavaScript vs Go: A Deep Dive into Syntax and Philosophy

As developers, we often find ourselves jumping between languages, and the syntax differences can be jarring. JavaScript and Go represent two very different philosophies in programming language design. JavaScript evolved from a browser scripting language into a full-stack powerhouse, while Go was designed from scratch by Google to solve modern distributed systems problems.

Let's explore how these languages handle common programming tasks and what these differences reveal about their design philosophies.

Table of Contents

  1. Variable Declaration and Type Systems
  2. Functions and Their Flavors
  3. Error Handling: Exceptions vs Explicit Errors
  4. Asynchronous Programming and Concurrency
  5. Data Structures: Objects vs Structs
  6. Control Flow and Conditionals
  7. Loops and Iteration
  8. Interfaces and Polymorphism
  9. Package Management and Modules
  10. Philosophy and Real-World Implications

1. Variable Declaration and Type Systems

JavaScript: Dynamic and Flexible

JavaScript's type system is dynamic—variables can hold any type of value and change types at runtime:

// Multiple ways to declare variables
let username = "Mephesto";        // Block-scoped, reassignable
const MAX_RETRIES = 3;            // Block-scoped, immutable binding
var legacy = "avoid this";        // Function-scoped (pre-ES6)

// Dynamic typing
let data = 42;                    // number
data = "now I'm a string";        // totally fine
data = { key: "value" };          // also fine
data = [1, 2, 3];                 // no problem

// Type coercion happens implicitly
console.log("5" + 5);             // "55" (string)
console.log("5" - 5);             // 0 (number)
console.log(true + 1);            // 2
Enter fullscreen mode Exit fullscreen mode

Go: Explicit and Type-Safe

Go is statically typed—every variable has a specific type that's checked at compile time:

package main

import "fmt"

func main() {
    // Explicit type declaration
    var username string = "Mephesto"
    var count int = 42
    var isActive bool = true

    // Type inference with :=
    message := "Go infers this is a string"
    port := 8080  // inferred as int

    // Constants
    const MaxRetries = 3
    const Pi = 3.14159

    // Multiple declarations
    var (
        host string = "localhost"
        timeout int = 30
    )

    // This would be a compile error:
    // count = "string"  // cannot use "string" as int

    // Zero values (no undefined!)
    var emptyString string  // ""
    var emptyInt int        // 0
    var emptyBool bool      // false
    var emptySlice []int    // nil
}
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  • JavaScript lets you reassign variables to different types; Go doesn't
  • Go has zero values (default initialization), JavaScript has undefined
  • Go's := short declaration is convenient but only works inside functions
  • JavaScript has three declaration keywords (var, let, const); Go primarily uses var and :=

2. Functions and Their Flavors

JavaScript: First-Class and Flexible

JavaScript treats functions as first-class citizens—they're just values that can be passed around:

// Traditional function declaration
function calculateTotal(price, quantity) {
    return price * quantity;
}

// Function expression
const calculateDiscount = function(price, rate) {
    return price * (1 - rate);
};

// Arrow function (ES6+)
const calculateTax = (price, taxRate) => price * taxRate;

// Arrow function with block body
const processOrder = (order) => {
    const total = order.price * order.quantity;
    const tax = total * 0.2;
    return { total, tax };
};

// Default parameters
function greet(name = "Guest", greeting = "Hello") {
    return `${greeting}, ${name}!`;
}

// Rest parameters
function sum(...numbers) {
    return numbers.reduce((acc, n) => acc + n, 0);
}

// Destructuring parameters
function createUser({ name, email, age = 18 }) {
    return { name, email, age, createdAt: new Date() };
}

// Higher-order functions
function withLogging(fn) {
    return function(...args) {
        console.log(`Calling with:`, args);
        const result = fn(...args);
        console.log(`Result:`, result);
        return result;
    };
}

const loggedSum = withLogging(sum);
loggedSum(1, 2, 3); // Logs inputs and output
Enter fullscreen mode Exit fullscreen mode

Go: Typed and Structured

Go functions are more rigid but offer unique features like multiple return values:

package main

import (
    "errors"
    "fmt"
)

// Basic function with typed parameters and return
func calculateTotal(price float64, quantity int) float64 {
    return price * float64(quantity)
}

// Multiple return values (idiomatic for error handling)
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Named return values
func calculateStats(numbers []int) (sum int, avg float64, count int) {
    count = len(numbers)
    for _, n := range numbers {
        sum += n
    }
    if count > 0 {
        avg = float64(sum) / float64(count)
    }
    return // naked return uses named values
}

// Variadic functions (like JavaScript's rest parameters)
func sum(numbers ...int) int {
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

// Functions as values
func applyOperation(a, b int, op func(int, int) int) int {
    return op(a, b)
}

// Closures
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

// Methods (functions with receivers)
type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Pointer receivers for mutation
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func main() {
    // Using multiple return values
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result)

    // Variadic function
    total := sum(1, 2, 3, 4, 5)

    // Function as value
    multiply := func(a, b int) int { return a * b }
    result = applyOperation(5, 3, multiply)
}
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  • Go requires explicit types for all parameters and return values
  • Go's multiple return values eliminate the need for objects/tuples in many cases
  • JavaScript has more syntactic sugar (arrow functions, default params, destructuring)
  • Go's receiver functions replace JavaScript's method syntax
  • Both support closures and first-class functions, but Go's are more verbose

3. Error Handling: Exceptions vs Explicit Errors

This is where the philosophies diverge most dramatically.

JavaScript: Try-Catch Exceptions

JavaScript uses exceptions that can bubble up the call stack:

// Traditional try-catch
function readConfig(filename) {
    try {
        const data = fs.readFileSync(filename, 'utf8');
        return JSON.parse(data);
    } catch (error) {
        console.error(`Failed to read config: ${error.message}`);
        throw error; // Re-throw or handle
    } finally {
        console.log('Cleanup code runs regardless');
    }
}

// Async error handling with promises
async function fetchUserData(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const data = await response.json();
        return data;
    } catch (error) {
        if (error.name === 'NetworkError') {
            console.error('Network error:', error);
        } else if (error.name === 'SyntaxError') {
            console.error('Invalid JSON:', error);
        } else {
            console.error('Unknown error:', error);
        }
        throw error;
    }
}

// Promise-based error handling
fetchUserData(123)
    .then(user => console.log(user))
    .catch(error => console.error(error))
    .finally(() => console.log('Done'));

// Custom error types
class ValidationError extends Error {
    constructor(field, message) {
        super(message);
        this.name = 'ValidationError';
        this.field = field;
    }
}

function validateUser(user) {
    if (!user.email) {
        throw new ValidationError('email', 'Email is required');
    }
    if (user.age < 0) {
        throw new ValidationError('age', 'Age must be positive');
    }
}
Enter fullscreen mode Exit fullscreen mode

Go: Explicit Error Values

Go treats errors as values that must be explicitly checked:

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "os"
)

// Errors are just values
func readConfig(filename string) (map[string]interface{}, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, fmt.Errorf("failed to read config: %w", err)
    }

    var config map[string]interface{}
    if err := json.Unmarshal(data, &config); err != nil {
        return nil, fmt.Errorf("failed to parse JSON: %w", err)
    }

    return config, nil
}

// Multiple error checking
func processUser(userID int) error {
    user, err := fetchUser(userID)
    if err != nil {
        return fmt.Errorf("fetch failed: %w", err)
    }

    if err := validateUser(user); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }

    if err := saveUser(user); err != nil {
        return fmt.Errorf("save failed: %w", err)
    }

    return nil
}

// Custom error types
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}

func validateUser(user *User) error {
    if user.Email == "" {
        return &ValidationError{
            Field:   "email",
            Message: "email is required",
        }
    }

    if user.Age < 0 {
        return &ValidationError{
            Field:   "age",
            Message: "age must be positive",
        }
    }

    return nil
}

// Error wrapping and unwrapping (Go 1.13+)
func fetchAndProcess(id int) error {
    data, err := fetchFromAPI(id)
    if err != nil {
        // Wrap error with context
        return fmt.Errorf("processing user %d: %w", id, err)
    }

    return processData(data)
}

// Checking for specific errors
func handleError(err error) {
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        fmt.Printf("Validation failed on field: %s\n", validationErr.Field)
        return
    }

    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("File not found")
        return
    }

    fmt.Println("Unknown error:", err)
}

// Defer for cleanup (like finally)
func processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close() // Guaranteed to run

    // Process file...
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  • JavaScript errors bubble up automatically; Go errors must be explicitly returned and checked
  • Go's if err != nil pattern is ubiquitous—critics call it verbose, advocates call it explicit
  • Go's defer provides cleanup guarantees similar to JavaScript's finally
  • JavaScript can throw any value; Go errors implement the error interface
  • Go 1.13+ added error wrapping with %w, similar to error causes in JavaScript

The Great Debate:

This is perhaps the most controversial difference. JavaScript developers often find Go's error handling repetitive. Go developers argue it makes error paths visible and forces you to think about failure cases. Coming from Django/Python, you'll recognize this as the "explicit is better than implicit" philosophy taken to an extreme.


4. Asynchronous Programming and Concurrency

JavaScript: Async/Await and Promises

JavaScript is single-threaded with an event loop, using async/await for non-blocking operations:

// Promises
function fetchUser(id) {
    return fetch(`/api/users/${id}`)
        .then(response => response.json())
        .then(data => {
            console.log('User:', data);
            return data;
        })
        .catch(error => {
            console.error('Error:', error);
            throw error;
        });
}

// Async/await (syntactic sugar over promises)
async function getUserProfile(userId) {
    try {
        const user = await fetchUser(userId);
        const posts = await fetchUserPosts(userId);
        const followers = await fetchFollowers(userId);

        return {
            user,
            posts,
            followers
        };
    } catch (error) {
        console.error('Failed to get profile:', error);
        throw error;
    }
}

// Parallel execution
async function getMultipleUsers(ids) {
    // Sequential (slow)
    const users = [];
    for (const id of ids) {
        users.push(await fetchUser(id));
    }

    // Parallel (fast)
    const promises = ids.map(id => fetchUser(id));
    const usersParallel = await Promise.all(promises);

    return usersParallel;
}

// Race conditions
async function fetchWithTimeout(url, timeout = 5000) {
    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Timeout')), timeout);
    });

    return Promise.race([
        fetch(url),
        timeoutPromise
    ]);
}

// Promise combinators
async function complexFlow() {
    // All must succeed
    const [user, config, stats] = await Promise.all([
        fetchUser(1),
        fetchConfig(),
        fetchStats()
    ]);

    // First to succeed
    const fastestServer = await Promise.race([
        fetch('https://server1.com/data'),
        fetch('https://server2.com/data')
    ]);

    // All must settle (succeed or fail)
    const results = await Promise.allSettled([
        riskyOperation1(),
        riskyOperation2(),
        riskyOperation3()
    ]);

    results.forEach((result, index) => {
        if (result.status === 'fulfilled') {
            console.log(`Op ${index} succeeded:`, result.value);
        } else {
            console.log(`Op ${index} failed:`, result.reason);
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Go: Goroutines and Channels

Go has built-in concurrency primitives—goroutines (lightweight threads) and channels:

package main

import (
    "fmt"
    "sync"
    "time"
)

// Basic goroutine
func sayHello() {
    fmt.Println("Hello from goroutine")
}

func basicGoroutine() {
    go sayHello() // Runs concurrently

    time.Sleep(time.Second) // Wait for goroutine (bad practice, use sync)
}

// Channels for communication
func fetchUser(id int, ch chan<- User) {
    // Simulate API call
    time.Sleep(100 * time.Millisecond)
    user := User{ID: id, Name: fmt.Sprintf("User%d", id)}
    ch <- user // Send to channel
}

func useChannels() {
    userChan := make(chan User)

    go fetchUser(1, userChan)

    user := <-userChan // Receive from channel (blocks until data available)
    fmt.Println("Received:", user)
}

// Concurrent fetching (like Promise.all)
func fetchMultipleUsers(ids []int) []User {
    users := make([]User, len(ids))
    var wg sync.WaitGroup

    for i, id := range ids {
        wg.Add(1)
        go func(index, userID int) {
            defer wg.Done()
            users[index] = fetchUserSync(userID)
        }(i, id)
    }

    wg.Wait() // Wait for all goroutines
    return users
}

// Channel patterns
func channelPatterns() {
    // Buffered channel
    ch := make(chan int, 3) // Can hold 3 items without blocking
    ch <- 1
    ch <- 2
    ch <- 3
    // ch <- 4 // Would block until something is received

    // Select for multiple channels (like Promise.race)
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "from ch1"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "from ch2"
    }()

    select {
    case msg1 := <-ch1:
        fmt.Println("Received:", msg1)
    case msg2 := <-ch2:
        fmt.Println("Received:", msg2)
    case <-time.After(500 * time.Millisecond):
        fmt.Println("Timeout")
    }
}

// Worker pool pattern
func workerPool() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Start workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Send jobs
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)

    // Collect results
    for a := 1; a <= 9; a++ {
        <-results
    }
}

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, j)
        time.Sleep(time.Second)
        results <- j * 2
    }
}

// Context for cancellation (like AbortController)
import "context"

func fetchWithContext(ctx context.Context, id int) (User, error) {
    resultChan := make(chan User)
    errChan := make(chan error)

    go func() {
        // Simulate long operation
        time.Sleep(2 * time.Second)
        resultChan <- User{ID: id}
    }()

    select {
    case user := <-resultChan:
        return user, nil
    case err := <-errChan:
        return User{}, err
    case <-ctx.Done():
        return User{}, ctx.Err() // Cancelled or timed out
    }
}

func useContext() {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    user, err := fetchWithContext(ctx, 1)
    if err != nil {
        fmt.Println("Error:", err) // Will timeout
    }
}

// Mutex for shared state
type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  • JavaScript: single-threaded with async I/O; Go: multi-threaded with goroutines
  • JavaScript uses promises/async-await; Go uses channels and goroutines
  • Go's select is more powerful than Promise.race for complex coordination
  • JavaScript's event loop handles concurrency implicitly; Go makes it explicit
  • Go has built-in primitives (channels, mutexes); JavaScript relies on the event loop

Real-World Impact:

For CPU-bound tasks, Go's goroutines can utilize multiple cores natively. JavaScript would need worker threads (which are clunky) or child processes. For I/O-bound tasks (like your Django REST Framework apps), both work well, but Go's approach scales better for high-concurrency scenarios like handling thousands of WebSocket connections.


5. Data Structures: Objects vs Structs

JavaScript: Prototypal Objects and Classes

JavaScript is object-oriented with prototypal inheritance:

// Object literals
const user = {
    name: "Mephesto",
    email: "mephesto@example.com",
    age: 25,
    greet() {
        return `Hello, I'm ${this.name}`;
    }
};

// Dynamic properties
user.location = "Ukraine";
delete user.age;

// ES6 Classes (syntactic sugar over prototypes)
class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
        this.createdAt = new Date();
    }

    greet() {
        return `Hello, I'm ${this.name}`;
    }

    static fromJSON(json) {
        const data = JSON.parse(json);
        return new User(data.name, data.email);
    }

    get displayName() {
        return this.name.toUpperCase();
    }

    set displayName(value) {
        this.name = value.toLowerCase();
    }
}

// Inheritance
class AdminUser extends User {
    constructor(name, email, permissions) {
        super(name, email);
        this.permissions = permissions;
    }

    greet() {
        return `Hello, I'm ${this.name} (Admin)`;
    }

    hasPermission(perm) {
        return this.permissions.includes(perm);
    }
}

// Private fields (ES2022)
class BankAccount {
    #balance = 0;

    deposit(amount) {
        this.#balance += amount;
    }

    getBalance() {
        return this.#balance;
    }
}

// Mixing concerns
const withLogging = (BaseClass) => {
    return class extends BaseClass {
        log(message) {
            console.log(`[${this.constructor.name}] ${message}`);
        }
    };
};

const LoggedUser = withLogging(User);
Enter fullscreen mode Exit fullscreen mode

Go: Structs and Composition

Go uses structs with composition instead of inheritance:

package main

import (
    "encoding/json"
    "fmt"
    "strings"
    "time"
)

// Struct definition
type User struct {
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

// Constructor pattern (not built-in)
func NewUser(name, email string) *User {
    return &User{
        Name:      name,
        Email:     email,
        CreatedAt: time.Now(),
    }
}

// Methods with receivers
func (u User) Greet() string {
    return fmt.Sprintf("Hello, I'm %s", u.Name)
}

// Pointer receivers for mutation
func (u *User) UpdateEmail(email string) {
    u.Email = email
}

// "Getters" (not idiomatic to call them GetX)
func (u User) DisplayName() string {
    return strings.ToUpper(u.Name)
}

// Embedded structs (composition, not inheritance)
type AdminUser struct {
    User                    // Embedded field
    Permissions []string
}

func NewAdminUser(name, email string, perms []string) *AdminUser {
    return &AdminUser{
        User:        *NewUser(name, email),
        Permissions: perms,
    }
}

// Method overriding through embedding
func (a AdminUser) Greet() string {
    return fmt.Sprintf("Hello, I'm %s (Admin)", a.Name)
}

func (a AdminUser) HasPermission(perm string) bool {
    for _, p := range a.Permissions {
        if p == perm {
            return true
        }
    }
    return false
}

// Unexported fields (private)
type bankAccount struct {
    balance int
}

func NewBankAccount() *bankAccount {
    return &bankAccount{balance: 0}
}

func (b *bankAccount) Deposit(amount int) {
    b.balance += amount
}

func (b *bankAccount) Balance() int {
    return b.balance
}

// JSON marshaling/unmarshaling
func jsonExample() {
    user := NewUser("Mephesto", "m@example.com")

    // Marshal to JSON
    data, err := json.Marshal(user)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(data))

    // Unmarshal from JSON
    var newUser User
    if err := json.Unmarshal(data, &newUser); err != nil {
        panic(err)
    }
}

// Struct tags for metadata
type APIUser struct {
    ID        int       `json:"id" db:"user_id"`
    Username  string    `json:"username" db:"username" validate:"required"`
    Email     string    `json:"email,omitempty" db:"email"`
    CreatedAt time.Time `json:"created_at" db:"created_at"`
    password  string    // unexported, won't be marshaled
}

// Anonymous structs for one-off use
func anonymousStructs() {
    config := struct {
        Host string
        Port int
    }{
        Host: "localhost",
        Port: 8080,
    }

    fmt.Printf("Server: %s:%d\n", config.Host, config.Port)
}
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  • JavaScript has prototypal inheritance; Go has composition via embedding
  • JavaScript classes are syntactic sugar; Go structs are data containers with attached methods
  • Go doesn't have constructors—use factory functions by convention
  • JavaScript properties can be added/removed dynamically; Go structs are fixed at compile time
  • Go uses struct tags for metadata (like Django model field options)
  • Privacy in JavaScript uses # or closures; Go uses capitalization (Upper = public, lower = private)

6. Control Flow and Conditionals

JavaScript

// If-else
if (user.age >= 18) {
    console.log("Adult");
} else if (user.age >= 13) {
    console.log("Teenager");
} else {
    console.log("Child");
}

// Ternary operator
const status = user.isActive ? "Active" : "Inactive";

// Switch
switch (user.role) {
    case "admin":
        grantAdminAccess();
        break;
    case "moderator":
        grantModeratorAccess();
        break;
    default:
        grantUserAccess();
}

// Truthy/falsy
if (user.email) { // truthy check
    sendEmail(user.email);
}

// Nullish coalescing
const port = config.port ?? 8080; // Only for null/undefined

// Optional chaining
const city = user?.address?.city; // Won't throw if undefined

// Short-circuit evaluation
const name = user.name || "Anonymous";
const value = user.value && user.value.trim();
Enter fullscreen mode Exit fullscreen mode

Go

package main

import "fmt"

func controlFlow() {
    // If-else (no parentheses!)
    age := 25
    if age >= 18 {
        fmt.Println("Adult")
    } else if age >= 13 {
        fmt.Println("Teenager")
    } else {
        fmt.Println("Child")
    }

    // If with initialization
    if err := someOperation(); err != nil {
        fmt.Println("Error:", err)
        return
    }

    // No ternary operator! Use if-else
    var status string
    if user.IsActive {
        status = "Active"
    } else {
        status = "Inactive"
    }

    // Switch (no break needed!)
    switch role := user.Role; role {
    case "admin":
        grantAdminAccess()
    case "moderator":
        grantModeratorAccess()
    default:
        grantUserAccess()
    }

    // Switch without expression (like if-else chain)
    switch {
    case age < 13:
        fmt.Println("Child")
    case age < 18:
        fmt.Println("Teenager")
    default:
        fmt.Println("Adult")
    }

    // Switch with multiple cases
    switch day {
    case "Saturday", "Sunday":
        fmt.Println("Weekend")
    case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
        fmt.Println("Weekday")
    }

    // Fallthrough (explicit, unlike JavaScript)
    switch num {
    case 1:
        fmt.Println("One")
        fallthrough
    case 2:
        fmt.Println("Two or less")
    }

    // Type switch
    var i interface{} = "hello"
    switch v := i.(type) {
    case int:
        fmt.Printf("Integer: %d\n", v)
    case string:
        fmt.Printf("String: %s\n", v)
    default:
        fmt.Printf("Unknown type: %T\n", v)
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  • Go doesn't have ternary operators—use if-else
  • Go's switch doesn't fall through by default (needs explicit fallthrough)
  • Go's switch can operate on types and arbitrary conditions
  • JavaScript has truthy/falsy; Go requires explicit boolean expressions
  • Go's if can include an initialization statement

7. Loops and Iteration

JavaScript

// For loop
for (let i = 0; i < 10; i++) {
    console.log(i);
}

// For...of (values)
const numbers = [1, 2, 3, 4, 5];
for (const num of numbers) {
    console.log(num);
}

// For...in (keys/indices)
const user = { name: "John", age: 30 };
for (const key in user) {
    console.log(`${key}: ${user[key]}`);
}

// While
let count = 0;
while (count < 10) {
    console.log(count);
    count++;
}

// Do-while
do {
    console.log(count);
    count--;
} while (count > 0);

// Array methods (functional style)
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);

numbers.forEach((num, index) => {
    console.log(`${index}: ${num}`);
});

// Break and continue
for (let i = 0; i < 10; i++) {
    if (i === 3) continue;
    if (i === 7) break;
    console.log(i);
}

// Labeled loops
outer: for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
        if (i === 1 && j === 1) break outer;
        console.log(i, j);
    }
}
Enter fullscreen mode Exit fullscreen mode

Go

package main

import "fmt"

func loops() {
    // For loop (only loop keyword in Go!)
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }

    // While-style loop
    count := 0
    for count < 10 {
        fmt.Println(count)
        count++
    }

    // Infinite loop
    for {
        // Must have break somewhere
        if someCondition {
            break
        }
    }

    // Range over slice (like for...of)
    numbers := []int{1, 2, 3, 4, 5}
    for index, value := range numbers {
        fmt.Printf("%d: %d\n", index, value)
    }

    // Ignore index with _
    for _, value := range numbers {
        fmt.Println(value)
    }

    // Only index
    for index := range numbers {
        fmt.Println(index)
    }

    // Range over map
    user := map[string]interface{}{
        "name": "John",
        "age":  30,
    }
    for key, value := range user {
        fmt.Printf("%s: %v\n", key, value)
    }

    // Range over string (iterates over runes/characters)
    for index, char := range "Привіт" {
        fmt.Printf("%d: %c\n", index, char)
    }

    // Range over channel
    ch := make(chan int, 5)
    go func() {
        for i := 0; i < 5; i++ {
            ch <- i
        }
        close(ch)
    }()

    for value := range ch {
        fmt.Println(value)
    }

    // Break and continue
    for i := 0; i < 10; i++ {
        if i == 3 {
            continue
        }
        if i == 7 {
            break
        }
        fmt.Println(i)
    }

    // Labeled loops
outer:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if i == 1 && j == 1 {
                break outer
            }
            fmt.Println(i, j)
        }
    }
}

// No built-in map/filter/reduce, but easy to implement
func Map(slice []int, fn func(int) int) []int {
    result := make([]int, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

func Filter(slice []int, fn func(int) bool) []int {
    result := []int{}
    for _, v := range slice {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce(slice []int, fn func(int, int) int, initial int) int {
    result := initial
    for _, v := range slice {
        result = fn(result, v)
    }
    return result
}
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  • Go has only for (replaces while, do-while, foreach)
  • Go's range is like JavaScript's for...of but more powerful
  • JavaScript has rich array methods; Go requires manual implementation or libraries
  • Both support labeled breaks for nested loops
  • Go's range over strings gives you runes (Unicode code points), not bytes

8. Interfaces and Polymorphism

JavaScript: Duck Typing

JavaScript uses structural typing—if it looks like a duck and quacks like a duck:

// No explicit interface, just conventions
class FileStorage {
    save(data) {
        // Save to file
    }

    load() {
        // Load from file
    }
}

class DatabaseStorage {
    save(data) {
        // Save to database
    }

    load() {
        // Load from database
    }
}

// Works with anything that has save/load
function backup(storage, data) {
    storage.save(data);
}

backup(new FileStorage(), data);
backup(new DatabaseStorage(), data);

// TypeScript interfaces (compile-time only)
interface Storage {
    save(data: any): void;
    load(): any;
}

class CloudStorage implements Storage {
    save(data: any) { /* ... */ }
    load() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Go: Implicit Interfaces

Go has interfaces, but they're satisfied implicitly (structural typing):

package main

import "fmt"

// Interface definition
type Storage interface {
    Save(data []byte) error
    Load() ([]byte, error)
}

// Types implement interfaces implicitly
type FileStorage struct {
    path string
}

func (f *FileStorage) Save(data []byte) error {
    // Save to file
    fmt.Println("Saving to file:", f.path)
    return nil
}

func (f *FileStorage) Load() ([]byte, error) {
    // Load from file
    return []byte("file data"), nil
}

type DatabaseStorage struct {
    connStr string
}

func (d *DatabaseStorage) Save(data []byte) error {
    // Save to database
    fmt.Println("Saving to database")
    return nil
}

func (d *DatabaseStorage) Load() ([]byte, error) {
    // Load from database
    return []byte("db data"), nil
}

// Function accepts interface
func Backup(storage Storage, data []byte) error {
    return storage.Save(data)
}

func main() {
    file := &FileStorage{path: "/tmp/data"}
    db := &DatabaseStorage{connStr: "postgres://..."}

    Backup(file, []byte("data"))    // Works
    Backup(db, []byte("data"))      // Works
}

// Multiple interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader
    Writer
}

// Empty interface (like any in TypeScript)
func PrintAnything(v interface{}) {
    fmt.Println(v)
}

// Type assertions
func processValue(v interface{}) {
    // Type assertion
    if str, ok := v.(string); ok {
        fmt.Println("String:", str)
    }

    // Type switch
    switch val := v.(type) {
    case int:
        fmt.Println("Int:", val)
    case string:
        fmt.Println("String:", val)
    case Storage:
        fmt.Println("Storage implementation")
    default:
        fmt.Println("Unknown type")
    }
}

// Common standard interfaces
import "io"

// io.Reader is everywhere in Go
func ProcessReader(r io.Reader) {
    data := make([]byte, 100)
    n, err := r.Read(data)
    // Process data
}

// Works with files, network connections, buffers, etc.
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  • Both use structural typing (duck typing), but Go makes it explicit with the interface keyword
  • JavaScript has no runtime interfaces; TypeScript interfaces are compile-time only
  • Go's interface satisfaction is implicit—no implements keyword needed
  • Go's empty interface interface{} is like JavaScript's any
  • Go has powerful standard interfaces (io.Reader, io.Writer, etc.)

9. Package Management and Modules

JavaScript: NPM and ES Modules

// package.json
{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "express": "^4.18.0",
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "jest": "^29.0.0"
  }
}

// Importing
import express from 'express';
import { debounce } from 'lodash';
import * as utils from './utils.js';

// Named exports
export function helper() { }
export const CONSTANT = 42;

// Default export
export default class MyClass { }

// Re-exporting
export { something } from './other.js';
export * from './module.js';

// Dynamic imports
const module = await import('./dynamic.js');

// CommonJS (older style)
const fs = require('fs');
module.exports = { helper };
Enter fullscreen mode Exit fullscreen mode

Go: Go Modules

// go.mod
module github.com/mephesto/myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.0
    github.com/lib/pq v1.10.7
)

// Importing
package main

import (
    "fmt"                    // Standard library
    "net/http"              // Standard library

    "github.com/gin-gonic/gin"           // External package
    "github.com/mephesto/myapp/internal" // Internal package
)

// Exporting (capitalization!)
package utils

// Exported (public)
func Helper() {
    // ...
}

const PublicConstant = 42

type PublicStruct struct {
    PublicField  string
    privateField string  // Not exported
}

// Not exported (private)
func helper() {
    // ...
}

// Package initialization
func init() {
    // Runs when package is imported
    fmt.Println("Package initialized")
}

// Internal packages (Go specific)
// github.com/mephesto/myapp/internal can only be imported
// by packages under github.com/mephesto/myapp
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  • JavaScript uses package.json; Go uses go.mod
  • JavaScript has explicit export/import; Go uses capitalization for visibility
  • Go's import creates a namespace; JavaScript can destructure imports
  • Go has internal/ package convention for truly private code
  • JavaScript has dynamic imports; Go imports are static (compile-time)
  • Go's standard library is more comprehensive

10. Philosophy and Real-World Implications

JavaScript Philosophy

Flexibility and Expression

JavaScript embraces multiple paradigms—object-oriented, functional, imperative. You can solve problems in many ways:

// Object-oriented
class UserService {
    findUser(id) { }
}

// Functional
const findUser = (id) => fetch(`/users/${id}`);

// Prototype-based
function UserService() { }
UserService.prototype.findUser = function(id) { };
Enter fullscreen mode Exit fullscreen mode

Dynamic and Forgiving

// Type coercion
"5" - 3;        // 2
"5" + 3;        // "53"
[] + {};        // "[object Object]"
{} + [];        // 0 (in some contexts)

// Undefined behavior
const obj = {};
obj.deeply.nested.property; // TypeError
obj?.deeply?.nested?.property; // undefined (with optional chaining)
Enter fullscreen mode Exit fullscreen mode

Ecosystem: Fast-Moving

The JavaScript ecosystem moves quickly. New frameworks appear constantly. This is both a strength (innovation) and weakness (fatigue).

Go Philosophy

Simplicity and Clarity

Go deliberately limits features. There's often "one way" to do things:

// One loop construct
for i := 0; i < 10; i++ { }

// No ternary operator
// No generics (until Go 1.18)
// No inheritance
// No overloading
Enter fullscreen mode Exit fullscreen mode

Explicit and Strict

// No implicit conversions
var i int = 42
var f float64 = i  // Compile error
var f float64 = float64(i)  // Must be explicit

// Unused imports are errors
import "fmt"  // If not used, won't compile

// All errors must be handled
result, err := doSomething()
// Ignoring err is bad practice
Enter fullscreen mode Exit fullscreen mode

Ecosystem: Stability-Focused

Go values backwards compatibility. The language changes slowly. Standard library is comprehensive. Fewer but more stable third-party packages.

When to Choose Which?

Choose JavaScript when:

  • Building web frontends (React, Vue, Angular)
  • Rapid prototyping with flexibility
  • Rich ecosystem of libraries needed
  • Team familiar with dynamic typing
  • Quick iteration more important than compile-time safety

Choose Go when:

  • Building microservices or APIs
  • Performance and concurrency critical
  • System-level programming
  • Deployment simplicity matters (single binary)
  • Long-term maintainability priority
  • Team values explicit error handling

Practical Example: Web API

JavaScript (Express):

const express = require('express');
const app = express();

app.get('/users/:id', async (req, res) => {
    try {
        const user = await db.findUser(req.params.id);
        res.json(user);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Go (Gin):

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    r.GET("/users/:id", func(c *gin.Context) {
        id := c.Param("id")

        user, err := db.FindUser(id)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{
                "error": err.Error(),
            })
            return
        }

        c.JSON(http.StatusOK, user)
    })

    r.Run(":3000")
}
Enter fullscreen mode Exit fullscreen mode

Key Observations:

  • JavaScript: More concise, async/await cleaner, dynamic
  • Go: More verbose, explicit error handling, typed parameters
  • JavaScript: Dependency on framework conventions
  • Go: Strong standard library, explicit control flow

Conclusion

JavaScript and Go represent different philosophies:

JavaScript says: "Be flexible, move fast, express yourself freely." It's the language of rapid iteration, rich ecosystems, and creative solutions. Perfect for web development where requirements change rapidly.

Go says: "Be explicit, be simple, be maintainable." It's the language of clarity, performance, and long-term reliability. Perfect for backend services where stability matters.

Neither is "better"—they solve different problems. Coming from Django and Python, you'll appreciate Go's explicitness and find JavaScript's dynamism both liberating and occasionally frustrating.

The best developers understand both paradigms and choose the right tool for the job. JavaScript for dynamic, user-facing applications. Go for reliable, concurrent backend services.

What matters is understanding the trade-offs and using each language's strengths effectively.


Want to dive deeper? Check out my other posts on language comparisons, or follow along as I continue exploring "What's wrong with..." various programming paradigms.

Top comments (0)