PidokuInfra

Values, Types and Zero Values

Foundations Beginner 45 min Difficulty 2/5 Topic 02 of 05

Prerequisites 01

The idea in one minute#

In Go, everything is a value, and assignment copies the value. Passing an argument copies it. Returning it copies it. There are no hidden references.

Some values contain a pointer — slices, maps, channels, strings, interfaces — so copying them copies a small header that still points at the same underlying data. That single rule, “the copy is always shallow and always of the value itself”, explains every sharing surprise in Go.

Every type also has a zero value — 0, "", false, nil — and every variable starts there. There is no uninitialized memory.

An analogy#

Photocopying a page. If the page holds the recipe, you now have two independent recipes. If the page holds only “the recipe is in the blue folder on shelf 3”, you have two notes pointing at one folder — change the folder and both readers see it.

A picture#

flowchart LR
  subgraph COPY["b := a"]
    direction TB
    A1["a = [3]int{1,2,3}<br/>24 bytes of data"] -->|"copies all 24 bytes"| B1["b = [3]int{1,2,3}<br/>independent"]
    S1["s = []int{1,2,3}<br/>header: ptr, len, cap"] -->|"copies the 24-byte header"| S2["t = header copy"]
    S1 --> BK[("backing array<br/>1, 2, 3")]
    S2 --> BK
  end
  class A1,B1 compute
  class S1,S2 queue
  class BK memory

How it really works#

The built-in types#

KindTypesSize (64-bit)Zero value
Booleanbool1 bytefalse
Integersint8/16/32/64, uint8/16/32/64as named0
int, uint, uintptr8 bytes (machine word)0
Aliasesbyte = uint8, rune = int321, 40
Floatsfloat32, float644, 80
Complexcomplex64, complex1288, 160
Stringstring16-byte header (pointer, length)""
Pointer*T8nil
Array[N]TN × size of Tevery element zero
Slice[]T24-byte header (pointer, length, capacity)nil
Map, channel, functionmap[K]V, chan T, func(...)8 (a pointer)nil
Interfaceany, error, …16 (two words)nil
Structstruct{...}its fields plus paddingevery field zero

Module II opens up each of the composite ones.

Declaring#

Go
var n int            // zero value: 0
var s string = "go"  // explicit type
x := 42              // short form, inside functions only; type inferred (int)
var a, b = 1, "two"  // several at once
const Pi = 3.14159   // constant; untyped until used

Untyped constants have arbitrary precision and take a type from context: const big = 1 << 40 is fine, and var f float32 = Pi works without a conversion.

Go never converts between types implicitly. int32 plus int64 is a compile error; write int64(a) + b. This is deliberate: every widening, narrowing and sign change is visible.

Copies: the rule and its consequences#

TypeAssignment copiesAfterwards the two values
Numbers, boolThe valueAre independent
Array [N]TAll N elementsAre independent
StructEvery fieldAre independent — unless a field is one of the rows below
Pointer *TThe addressPoint at the same variable
SliceThe header (pointer, len, cap)Share the backing array
Map, channelThe pointerAre the same map or channel
StringThe headerShare bytes, which is safe: strings are immutable
InterfaceTwo words (type, data)Usually share what the data word points to

This is also why you pass a pointer when a function must modify its argument: the function receives a copy, and a copy of a pointer still reaches the original.

Zero values you can use#

Go’s standard library is designed so that zero values are useful: a sync.Mutex is unlocked, a bytes.Buffer is empty and ready, a nil slice can be appended to, len of a nil map is 0. The one trap: writing to a nil map panics — maps must be made with make or a literal.

Defining your own types#

Go
type Celsius float64          // a new type with float64's representation
type Tokens []int             // a named slice type
type ID = string              // an alias: ID *is* string

A defined type is distinct from its underlying type — Celsius and float64 do not mix without a conversion — which lets the compiler catch unit mistakes for free.

Code#

Go
// values.go — sizes, zero values, and what an assignment actually copies.
package main

import (
	"fmt"
	"unsafe"
)

type Point struct{ X, Y float64 }

func main() {
	fmt.Println("sizes in bytes")
	fmt.Println("  bool      ", unsafe.Sizeof(true))
	fmt.Println("  int       ", unsafe.Sizeof(int(0)))
	fmt.Println("  float32   ", unsafe.Sizeof(float32(0)))
	fmt.Println("  string    ", unsafe.Sizeof(""), "(header only, whatever the length)")
	fmt.Println("  []int     ", unsafe.Sizeof([]int{}), "(header only)")
	fmt.Println("  [3]int    ", unsafe.Sizeof([3]int{}), "(the data itself)")
	fmt.Println("  map       ", unsafe.Sizeof(map[string]int{}), "(a pointer)")
	fmt.Println("  any       ", unsafe.Sizeof(any(nil)), "(two words)")
	fmt.Println("  Point     ", unsafe.Sizeof(Point{}))

	var (
		i int
		s string
		p *Point
		m map[string]int
		e error
	)
	fmt.Printf("\nzero values: %d %q %v %v %v  (len of nil map: %d)\n", i, s, p, m, e, len(m))

	// Array: the copy is independent.
	a := [3]int{1, 2, 3}
	b := a
	b[0] = 99
	fmt.Println("\narray  a:", a, " b:", b, "  ← independent")

	// Slice: the header is copied, the backing array is shared.
	sl := []int{1, 2, 3}
	tl := sl
	tl[0] = 99
	fmt.Println("slice  s:", sl, " t:", tl, "  ← shared")

	// Struct: copied field by field.
	p1 := Point{1, 2}
	p2 := p1
	p2.X = 99
	fmt.Println("struct p1:", p1, " p2:", p2, "  ← independent")

	// Pointer: the address is copied.
	q := &p1
	q.X = -1
	fmt.Println("via pointer, p1 is now", p1)

	// A function receives a copy.
	double := func(pt Point) { pt.X *= 2 }
	doubleP := func(pt *Point) { pt.X *= 2 }
	double(p1)
	fmt.Println("after double(p1):  ", p1, " ← unchanged")
	doubleP(&p1)
	fmt.Println("after doubleP(&p1):", p1, " ← changed")
}

Remember this#

  • Assignment, argument passing and return all copy the value.
  • Slices, maps, channels, strings and interfaces are small headers that point elsewhere: the header is copied, the data is shared.
  • Every variable starts at its type’s zero value; design types so the zero value is usable.
  • No implicit conversions between numeric types.

Try it#

  1. Run values.go. Add a struct with a slice field, copy it, and change an element through the copy. Which parts were independent?
  2. Declare var m map[string]int and write to it. Read the panic message, then fix it two ways.
  3. Define type Meters float64 and type Feet float64. What does the compiler say when you add one to the other?

Check yourself#

  1. What does assigning one slice to another copy?
  2. Why does Go have no implicit numeric conversions?
  3. Which zero value cannot be written to?

↑↓ navigate↵ openesc close