Skip to content

Value vs Reference Semantics

JavaScript: everything object is a reference

Section titled “JavaScript: everything object is a reference”

In JavaScript and TypeScript, all non-primitive values (objects, arrays, functions) are passed by reference. Assigning an object to a new variable does not copy it — both variables point at the same data.

// TypeScript — objects always share
const a = { x: 1 };
const b = a; // b points to the SAME object
b.x = 99;
console.log(a.x); // 99 — a was also mutated

In Go, the rules are more nuanced: some types copy, some share. Knowing which is which prevents subtle bugs.

Assigning a struct or fixed-size array to a new variable produces a full copy. Modifying the copy does not touch the original.

TypeScript
// TypeScript — objects always share (reference)
interface Point { x: number; y: number; }
const a: Point = { x: 1, y: 2 };
const b = a; // b is the SAME object
b.x = 99;
console.log(a.x); // 99 — mutated!
// Arrays also share:
const arr1 = [1, 2, 3];
const arr2 = arr1;
arr2[0] = 99;
console.log(arr1[0]); // 99
Go
// Go — structs copy on assign
type Point struct{ X, Y int }
a := Point{X: 1, Y: 2}
b := a // b is a full COPY
b.X = 99
fmt.Println(a.X) // 1 — a is unchanged
// Fixed-size arrays also copy
arr1 := [3]int{1, 2, 3}
arr2 := arr1 // copy
arr2[0] = 99
fmt.Println(arr1[0]) // 1 — unchanged

Slices and maps hold an internal pointer to their underlying data. Assigning them to a new variable shares that pointer — both variables refer to the same underlying array or hash table.

TypeScript
// TypeScript — arrays (like all objects) are references
const s1 = [1, 2, 3];
const s2 = s1; // same array
s2[0] = 99;
console.log(s1[0]); // 99
// Maps too:
const m1 = new Map([["a", 1]]);
const m2 = m1;
m2.set("a", 99);
console.log(m1.get("a")); // 99
Go
// Go — slices share the underlying array
s1 := []int{1, 2, 3}
s2 := s1 // s2 shares s1's underlying array
s2[0] = 99
fmt.Println(s1[0]) // 99 — shared!
// Maps always share
m1 := map[string]int{"a": 1}
m2 := m1
m2["a"] = 99
fmt.Println(m1["a"]) // 99 — shared!
// To get a true copy, use copy() for slices:
s3 := make([]int, len(s1))
copy(s3, s1)
s3[0] = 0
fmt.Println(s1[0]) // 99 — s1 unchanged

The same rules apply when passing values to functions. Struct arguments arrive as copies; slice/map arguments share the header.

TypeScript
// TypeScript — objects always pass by reference
function mutate(obj: { x: number }) {
obj.x = 99; // mutates caller's object
}
const p = { x: 1 };
mutate(p);
console.log(p.x); // 99
Go
// Go — struct passes as copy; mutation doesn't affect caller
func mutateStruct(p Point) {
p.X = 99 // modifies the LOCAL copy only
}
// Slice passes its header (pointer + len + cap) — shares data
func mutateSlice(s []int) {
s[0] = 99 // modifies the shared underlying array
}
func main() {
pt := Point{X: 1}
mutateStruct(pt)
fmt.Println(pt.X) // 1 — unchanged
sl := []int{1, 2, 3}
mutateSlice(sl)
fmt.Println(sl[0]) // 99 — shared data modified
}
package main
import "fmt"
type Point struct{ X, Y int }
func tryMutateStruct(p Point) {
p.X = 999
}
func tryMutateSlice(s []int) {
s[0] = 999
}
func main() {
// Struct: value copy
pt := Point{X: 1, Y: 2}
tryMutateStruct(pt)
fmt.Println("struct after func:", pt.X) // 1 — unchanged
// Slice: reference semantics
sl := []int{1, 2, 3}
tryMutateSlice(sl)
fmt.Println("slice after func:", sl[0]) // 999 — mutated
// Array (fixed size): value copy
arr := [3]int{10, 20, 30}
arr2 := arr
arr2[0] = 999
fmt.Println("array original:", arr[0]) // 10 — unchanged
// Explicit deep copy of slice
original := []int{1, 2, 3}
clone := make([]int, len(original))
copy(clone, original)
clone[0] = 999
fmt.Println("original after clone mutation:", original[0]) // 1
}
You assign a Go struct to a new variable and modify a field. What happens to the original?
Which built-in function creates a true deep copy of a slice?
You pass a map to a function and the function adds a key. What does the caller see?
What does json.Marshal produce for a nil slice in Go?