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
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
- Variable Declaration and Type Systems
- Functions and Their Flavors
- Error Handling: Exceptions vs Explicit Errors
- Asynchronous Programming and Concurrency
- Data Structures: Objects vs Structs
- Control Flow and Conditionals
- Loops and Iteration
- Interfaces and Polymorphism
- Package Management and Modules
- 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
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
}
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 usesvarand:=
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
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)
}
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');
}
}
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
}
Key Differences:
- JavaScript errors bubble up automatically; Go errors must be explicitly returned and checked
- Go’s
if err != nilpattern is ubiquitous—critics call it verbose, advocates call it explicit - Go’s
deferprovides cleanup guarantees similar to JavaScript’sfinally - JavaScript can throw any value; Go errors implement the
errorinterface - 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);
}
});
}
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
}
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
selectis more powerful thanPromise.racefor 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);
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)
}
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();
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)
}
}
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
ifcan 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);
}
}
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
}
Key Differences:
- Go has only
for(replaces while, do-while, foreach) - Go’s
rangeis like JavaScript’sfor...ofbut 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() { /* ... */ }
}
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.
Key Differences:
- Both use structural typing (duck typing), but Go makes it explicit with the
interfacekeyword - JavaScript has no runtime interfaces; TypeScript interfaces are compile-time only
- Go’s interface satisfaction is implicit—no
implementskeyword needed - Go’s empty interface
interface{}is like JavaScript’sany - 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 };
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
Key Differences:
- JavaScript uses
package.json; Go usesgo.mod - JavaScript has explicit
export/import; Go uses capitalization for visibility - Go’s
importcreates 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) { };
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)
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
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
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);
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")
}
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.