Go's Value Philosophy: Part 1 - Why Everything Is a Value, Not an Object
Deep dive into Go's value-oriented design philosophy and how it differs from Python's everything-is-an-object and Java's everything-is-a-class approaches. Understand how this fundamental choice affects memory, concurrency, and performance.
- tags
- #Go #Golang #Values #Objects #Memory-Model #Philosophy #Python #Java #Comparison #Concurrency #Stack-Allocation #Heap-Allocation #Programming-Paradigms #Type-Systems #Performance #Mental-Models
- categories
- Programming Go
- published
- reading time
- 31 minutes
📚 Series: Go Value Philosophy
- Go's Value Philosophy: Part 1 - Why Everything Is a Value, Not an Object (current)
- Go's Value Philosophy: Part 2 - Escape Analysis and Performance
- Go's Value Philosophy: Part 3 - Zero Values: Go's Valid-by-Default Philosophy
You’ve heard the mantras:
- Python: “Everything is an object”
- Java: “Everything is a class”
- Go: “Everything is a value”
These Are Design Philosophies, Not Marketing Slogans
These statements describe fundamental design choices that shape every line of code you write. Understanding what “everything is a value” means in Go reveals why Go’s concurrency model works, why it’s fast, and why it feels different from object-oriented languages.
This post explores the mental model behind values, contrasts it with objects and classes, and shows how Go’s value philosophy enables safe concurrency and predictable performance.
Three Mental Models for Programming
Python: Everything Is an Object
In Python, even the integer 5 is a heap-allocated object. This means values are stored as objects with three characteristics:
1. Identity - A memory address that identifies the object (though multiple variables may reference the same object)
2. Methods - Functions you can call on the value (like .bit_length() on integers)
3. Reference semantics - Assignment copies references, not data (multiple variables can point to the same object)
| |
The identity caveat - integer interning and constant folding:
Python optimizes small integers by pre-creating objects for values from -5 to 256 (this is called integer interning - a general optimization technique where runtimes share immutable values to reduce memory usage). All variables referencing these values point to the same pre-allocated object. Additionally, the Python compiler performs constant folding - when it sees literal values in the same compilation unit, it often reuses the same object even for larger integers.
| |
Objects have identity separate from value:
For mutable types like lists, Python always creates distinct objects. Two lists with identical contents occupy different memory locations and have different identities.
| |
All assignments are reference assignments:
When you assign one variable to another in Python, you’re copying the reference (pointer) to the object, not the object itself. Both variables point to the same object in memory, so changes through one variable affect the other.
| |
Java: Everything Is a Class
Java’s famous boilerplate verbosity comes from organizing all code into classes. Even the main() entry point requires a class wrapper - you can’t write a function without wrapping it in a class first.
| |
Class hierarchies define structure:
| |
Go: Everything Is a Value
Go represents data as values that are copied by default, have no hidden metadata, and don’t inherit from anything.
| |
Values have no identity:
Only references have identity. In Python, objects have identity (memory address) because everything is a reference. In Go, values are just data with no identity separate from their contents.
| |
When you need identity in Go, use explicit pointers:
Identity means “unique location in memory” - does this data structure occupy its own distinct memory address? In Go, pointers provide this concept explicitly.
| |
Identity Equivalence Across Languages
Go’s pointer equality (p1 == p2) is equivalent to:
- Python’s
isoperator:a is b - Python’s identity comparison:
id(a) == id(b) - Java’s reference equality:
a == b(for objects)
All check the same thing: “Do these references point to the same memory location?”
The difference: Python/Java check identity by default. Go requires explicit pointers to get identity semantics.
Explicit pointers for sharing:
| |
The Core Distinction:
- Python: Assignment copies references (shares objects)
- Java: Assignment copies references for objects, values for primitives
- Go: Assignment copies values; use explicit pointers for sharing
This affects everything: concurrency safety, memory layout, performance characteristics, and how you reason about code.
The Unifying Concept: References vs Values
Behind the philosophical differences (“everything is an object” vs “everything is a class” vs “everything is a value”) lies a fundamental choice about what gets copied when you assign a variable.
The Real Question Every Language Answers
When you write b = a, what actually gets copied?
Option 1: Copy the reference (pointer)
- Result: Both variables point to the same data in memory
- Mutations through
baffecta(they share state) - Memory overhead: object headers, garbage collection tracking
- Languages: Python (always), Java (for objects), C# (for classes)
Option 2: Copy the value (data)
- Result: Both variables have independent copies of the data
- Mutations to
bdon’t affecta(no sharing) - Memory overhead: minimal (just the data itself)
- Languages: Go (by default), Java (for primitives), C (structs)
How Languages Present This Choice
Three Philosophies, One Choice: What Gets Copied?
Python’s “everything is an object” = References by default
x = 5creates a reference to an integer object (heap-allocated)- Assignment copies references (shared state by default)
- Explicit
copy.copy()needed for independent copies
Java’s “everything is a class” = Split model
- Objects are references, primitives are values
- Creates friction: boxing/unboxing, different semantics for
intvsInteger - Designed for performance: primitives avoid heap overhead
Go’s “everything is a value” = Values by default
p2 = p1copies the data (independent copies by default)- Explicit pointers (
*Point) for references (shared state when needed) - Makes sharing visible in the code through
&and*
The pattern: All three support both references and values. They differ in which is implicit (easy) and which requires explicit syntax (intentional).
The spectrum:
Reference-heavy ←────────────────────────→ Value-heavy
Python Java Go C/Rust
(always refs) (split) (values (raw values
+ pointers) + unsafe)
Implicit sharing ←──────────────────→ Explicit sharing
Dynamic dispatch ←──────────────────→ Static dispatch
Heap by default ←──────────────────→ Stack preferred
High overhead ←──────────────────→ Zero overhead
Languages exist on a spectrum from “references by default” to “values by default.” Moving right trades convenience (implicit sharing) for performance (stack allocation) and explicitness (visible sharing).
Why This Matters
The reference-vs-value choice determines:
Concurrency safety: Values don’t need synchronization (independent copies). References require locks or channels when shared between goroutines/threads.
Performance characteristics: Values can live on the stack (fast allocation/deallocation). References typically require heap allocation and garbage collection.
Mental model: With references, you reason about object identity and shared state. With values, you reason about data flow and transformations.
API design: Languages with default references encourage mutation (modify shared state). Languages with default values encourage immutability (return modified copies).
The key insight: Python, Java, and Go all support both references and values. The difference is which one is the default and which requires explicit syntax. Go inverts the common pattern by making values implicit and references explicit.
Objects Are Not Just Pointers to Structs
A common misconception: “Objects are just structs with a pointer, right?” Not quite. Objects carry metadata that Go values (even pointer-based ones) don’t have.
Python object in memory:
Variable on stack: [pointer]
↓
Heap-allocated object: [ref_count | type_pointer | __dict__ | data]
(8 bytes) (8 bytes) (48+ bytes) (varies)
Every Python object has:
- Reference count (for garbage collection)
- Type pointer (links to class definition)
- Attribute dictionary (stores instance attributes)
- Then finally the actual data
Java object in memory:
Variable on stack: [reference]
↓
Heap-allocated object: [mark_word | class_pointer | data]
(8 bytes) (8 bytes) (varies)
Every Java object has:
- Mark word (GC info, lock state, hash code)
- Class pointer (links to class metadata)
- Then the actual data
Go value (stack-allocated):
Variable on stack: [data]
(just the data, no metadata, no pointer)
Go pointer:
Variable on stack: [pointer]
↓
Heap-allocated struct: [data]
(just the data, no metadata!)
The crucial difference: Go pointers point directly to data with zero metadata overhead. Python/Java references point to structures that wrap the data in metadata.
Size comparison for storing two integers:
| |
What this means:
When Go uses pointers, you get reference semantics (shared state, identity) without object overhead. The pointer references raw data, not a metadata-wrapped object. This is why Go can use pointers liberally for large structs without the memory overhead that Python/Java objects carry.
What Is an Object Really? Class vs Object
Understanding the implementation difference between classes and objects clarifies what “everything is an object” actually costs.
Class (compile-time + runtime metadata):
- Template defining field layout and method locations
- Method table (vtable): function pointers for dynamic dispatch
- Type information for runtime reflection
- One per type - all instances share the same class metadata
Object (runtime instance):
- Header pointing to its class
- Instance data (the actual field values)
- Many per class - each instantiation creates a new object
Python example:
| |
Method call mechanism (vtable dispatch):
| |
What is a vtable? A vtable (virtual method table) is an array of function pointers stored in the class metadata. Every method in the class has an entry in the vtable pointing to its implementation. When you call a method on an object, the runtime follows the object’s class pointer, looks up the method in that class’s vtable, and calls the function it points to.
Why vtables exist - polymorphism:
| |
This indirection (object → class → vtable → function) enables polymorphism but costs performance: pointer dereferences and cache misses.
Java example:
| |
Go - no classes at all:
| |
Go avoids vtables for concrete types:
| |
The key difference: Go uses vtables only when you ask for polymorphism (interfaces). Python/Java use vtables always (every method call on every object).
Performance Implications Summary
| Aspect | Python/Java (Objects) | Go (Values) | Go (Interfaces) |
|---|---|---|---|
| Class metadata | Stored at runtime | Compile-time only | Stored for interface types |
| Method dispatch | Dynamic (vtable) | Static (direct call) | Dynamic (interface table) |
| Instance header | Required (16+ bytes) | None (0 bytes) | Interface wrapper (16 bytes) |
| Method call cost | ~5-10ns (vtable lookup) | ~1ns (direct call) | ~2-3ns (interface dispatch) |
| Memory overhead | High (headers + metadata) | Zero | Only when using interfaces |
What “everything is an object/value” means in practice:
Python/Java:
- Every instance has runtime header → class metadata → vtable
- Every method call: pointer dereference + vtable lookup + indirect call
- Performance cost paid whether you need polymorphism or not
Go values:
- No runtime type information, no headers, no vtables
- Method calls resolved at compile time → direct function calls
- Zero overhead for the common case (concrete types)
Go interfaces (opt-in objects):
- Explicit syntax (
var a Animal = dog) wraps value in interface - Interface contains type pointer + value pointer
- Method calls use dynamic dispatch through interface table
- Pay for polymorphism only when you explicitly ask for it
Connection to Pass-by-Value vs Pass-by-Reference
This same choice applies to function parameters. When you pass an argument to a function, what gets passed?
Pass-by-value: The function receives a copy of the data
| |
Pass-by-reference: The function receives a reference to the original data
| |
How languages handle function calls:
Python: Technically “pass-by-value of references.” Since everything is already a reference, you pass a copy of the reference. The function can mutate the object but can’t change which object the caller’s variable references.
| |
For practical purposes, Python behaves like pass-by-reference since you can mutate objects through the reference you receive.
Java: Pass-by-value, but for objects the “value” is a reference (confusing!). You’re copying the reference, not the object.
| |
Go: Pass-by-value (always). Functions receive copies unless you explicitly pass pointers.
| |
The assignment semantics (reference vs value) determine the default parameter passing behavior. Languages with reference semantics naturally pass references to functions. Go’s value semantics mean everything is copied unless you explicitly use pointers.
The Primitive vs Object Question
This raises an important question: Is everything in Go a “primitive” since everything behaves like a value?
Python: No primitives at all. Everything is a reference to a heap-allocated object with identity.
| |
Java: Explicit split between primitives and objects.
| |
Java’s primitive/object split creates complexity: boxing/unboxing, different semantics, performance tradeoffs.
Go: No primitive/object distinction. Everything follows value semantics, but you’re not limited to simple types.
| |
The key insight:
- Python: Everything is an object (reference semantics everywhere)
- Java: Split model (primitives are values, objects are references)
- Go: Everything behaves like values by default (uniform semantics, explicit pointers for references)
Go doesn’t need a primitive type system because value semantics work for complex types too. A struct with 10 fields behaves just like an integer - copied on assignment, no identity, stack-allocatable. Java needed primitives for performance (avoiding heap allocation), but Go achieves this through escape analysis instead.
What Does “Value” Mean?
Values vs Objects: The Technical Difference
Values:
- Copied on assignment
- No identity separate from content
- No hidden metadata
- Stack-allocated when possible
- No inheritance hierarchy
Objects:
- Shared by reference on assignment
- Have identity (
id()in Python,hashCode()in Java) - Carry metadata (type, reference count, vtable pointer)
- Heap-allocated
- Part of class hierarchies
Memory Model: Values
When you create a value in Go, it exists as raw bytes in memory:
| |
Y: 8 bytes"] end subgraph python["Python Object (80+ bytes)"] pyheader["Object Header: 16 bytes
Type Pointer: 8 bytes
Dict: 48 bytes"] pydata["x ref → int(1): 28 bytes
y ref → int(2): 28 bytes"] pyheader -.-> pydata end style go fill:#3A4C43,stroke:#6b7280,color:#f0f0f0 style python fill:#4C3A3C,stroke:#6b7280,color:#f0f0f0 style godata fill:#66bb6a,stroke:#1b5e20,color:#fff style pyheader fill:#ef5350,stroke:#b71c1c,color:#fff style pydata fill:#ef5350,stroke:#b71c1c,color:#fff
Copy operation is memcpy:
| |
Memory Model: Objects
When you create an object in Python, it’s a heap-allocated structure with metadata:
| |
Assignment copies references:
| |
Why This Matters: Concurrency
Go’s value semantics make concurrency safer:
| |
Python’s object semantics require synchronization:
| |
Receivers vs Methods: Go’s Approach
Go doesn’t have methods in the OOP sense. It has receivers - functions associated with types.
The Terminology Matters
Python/Java methods:
- Bound to class hierarchy
- Implicit
self/thisparameter (the object) - Dynamic dispatch through vtables
- Can override parent methods
Go receivers:
- Bound to any user-defined type
- Explicit receiver parameter (value or pointer)
- Static dispatch (unless through interface)
- No inheritance, no override
Receiver Example
| |
Key distinction: The receiver receives the VALUE (or pointer to value), not an object with hidden state.
Value Receivers vs Pointer Receivers
| |
When to use each:
| Receiver Type | Use When | Example |
|---|---|---|
Value (t T) | Small types, no mutation needed | func (t Temperature) Celsius() |
Pointer (t *T) | Large types, mutation needed | func (c *Counter) Increment() |
(count = 1) Copy-->>Original: copy discarded Note over Original: count still 0 Original->>Ptr: c.IncrementPtr() - passes pointer Note over Ptr: count++ on original
(via pointer) Ptr-->>Original: modifies original Note over Original: count = 1
Common Mistake: Value Receivers Don’t Mutate
| |
Built-In Types: No Receivers Allowed
Go doesn’t allow adding receivers to built-in types:
| |
But you can wrap built-in types:
| |
Python allows methods on everything:
| |
This reflects the philosophical difference: Python’s integers are objects with behavior; Go’s integers are values you can wrap to add behavior.
Performance Implications
Stack vs Heap Allocation
Go values prefer the stack:
| |
Python objects require heap allocation:
| |
Memory Overhead Comparison
Go struct (16 bytes):
[X: 8 bytes][Y: 8 bytes]
Total: 16 bytes
Python object (80+ bytes):
Object header: 16 bytes
Type pointer: 8 bytes
Dictionary: 48+ bytes (for attributes)
Attribute pointers: 16 bytes (x and y references)
Integer objects: 28 bytes each (x=1, y=2)
Total: 80+ bytes
Copy Performance
| Operation | Go (Value) | Python (Object) |
|---|---|---|
| Create | Stack alloc (fast) | Heap alloc + GC tracking (slow) |
| Copy | memcpy (cheap) | Reference copy (cheap), deep copy (expensive) |
| Access | Direct (no indirection) | Pointer dereference (indirection) |
| Mutation | Safe (copy) | Requires synchronization (shared) |
Benchmark: 1 million struct copies
| |
| |
Concurrency: Values Enable Safety
The Problem with Shared Objects
Python requires locks for shared state:
| |
Go’s Value Solution
Each goroutine gets its own copy:
| |
When sharing IS needed, use channels or mutexes explicitly:
| |
Go’s Philosophy: Make Sharing Explicit
- Default: Values are copied (safe, no synchronization needed)
- Sharing: Use explicit pointers, channels, or mutexes
- Visibility: The code shows where data is shared vs copied
Result: Concurrency bugs are easier to spot because sharing is explicit.
Interfaces: When Values Become Object-Like
Go interfaces create a hybrid: when a value is placed in an interface, it gains object-like behavior with type information.
Interface Values
| |
Under the hood, an interface value is:
| |
Dynamic Dispatch Through Interfaces
| |
But note: Outside interfaces, they’re pure values with no dynamic behavior.
| |
For more on Go’s interface system, see: Go Interfaces: The Type System Feature You Implement By Accident
Method Chaining: Why It’s Rare in Go
Method chaining (fluent interfaces) is common in OOP languages but rare in Go because of value semantics.
Method Chaining in Python/Java
| |
| |
Go: Value Semantics Break Chaining
| |
Error Handling Breaks Chaining
Go’s explicit error handling makes chaining awkward:
| |
Idiomatic Go prefers explicit error checking:
| |
When chaining DOES appear:
| |
Comparison Table
| Aspect | Go (Values) | Python (Objects) | Java (Classes) |
|---|---|---|---|
| Assignment | Copies value | Copies reference | Copies reference (objects), value (primitives) |
| Identity | No identity | id() function | hashCode() method |
| Metadata | No metadata | Object header, type pointer, refcount | Object header, class pointer |
| Allocation | Stack-preferred | Always heap | Heap for objects, stack for primitives |
| Copy cost | Cheap (memcpy) | Cheap (reference), expensive (deep copy) | Cheap (reference) |
| Concurrency | Safe by default (copies) | Requires synchronization | Requires synchronization |
| Memory overhead | Zero overhead | High (header + dict) | Moderate (header) |
| Method dispatch | Static (direct call) | Dynamic (object lookup) | Dynamic (vtable) |
| Polymorphism | Interfaces only | Inheritance + duck typing | Inheritance + interfaces |
| Mutation | Requires pointer | Mutates shared object | Mutates shared object |
When Value Semantics Matter Most
1. High-Frequency Data Structures
Go’s values shine:
| |
2. Concurrent Processing
Safe parallelism without locks:
| |
3. Functional Patterns
Immutability by default:
| |
Putting It Together
Go’s “everything is a value” philosophy creates a programming model where data is copied by default. Assignment copies values, function arguments receive copies, and there’s no hidden sharing. When sharing is needed, it’s explicit through pointers, channels, or mutexes. This makes performance predictable through stack allocation and cheap copies, while keeping concurrency safer since each goroutine gets its own copies by default.
The memory model stays simple: values are just bytes with no object headers or reference counting. This contrasts sharply with Python’s heap-allocated objects with identity and Java’s class hierarchies with inheritance.
The trade-offs:
Python’s objects provide rich introspection and dynamic behavior at the cost of memory overhead and synchronization complexity. Java’s classes offer strong typing and clear structure but demand verbose boilerplate and explicit interfaces. Go’s values deliver simplicity and safe concurrency but require explicit copying and forego inheritance entirely.
The mental model you choose shapes how you think about your program. Go’s value philosophy encourages thinking about data flow (values moving through functions) rather than object graphs (references connecting objects).
Further Reading
Go Philosophy:
Related Posts:
📚 Series: Go Value Philosophy
- Go's Value Philosophy: Part 1 - Why Everything Is a Value, Not an Object (current)
- Go's Value Philosophy: Part 2 - Escape Analysis and Performance
- Go's Value Philosophy: Part 3 - Zero Values: Go's Valid-by-Default Philosophy