A Place on the Grid
The Coord rule
worldd announces itself and exits because it has no way to say where
anything is. The first server rule is small and permanent: a place is one
value: the column and the row travel together, or not at all.
The world is a grid: columns running east, rows running south, and every future inhabitant of The Hollow standing on exactly one square of it. A place on that grid is a column number and a row number.
Keeping the numbers loose makes every grid function grow a pair of parameters:
x int, y int. A function that moves something takes four integers, two
for where it is and two for where it goes.
Nothing stops you from passing the row where the column belongs, and the compiler cannot
object, because an int is an int. The call
move(7, 12, 12, 7) compiles as happily backwards as forwards.
In a simulation that runs for days unattended, a swapped pair does not crash anything. It quietly puts a creature on the wrong square and lets the world carry on being subtly wrong.
Go's tool for binding several values into one is the struct: a type you define by listing named fields, each with its own type. Once a place is a single value, a function that moves something takes two arguments, a from and a to, and passing them backwards is a mistake you can see.
Struct fields are built from Go's basic types, and this book leans on four of them.
int is a whole number, positive or negative; grid columns and rows are
ints. float64 is a number with a fractional part, like a
height of 2.5 meters.
bool is true or false. string
is text.
Every type also has a zero value, the value a variable holds before you
assign anything: 0 for numbers, false for
bool, "" for strings. Go never leaves a variable
undefined, a guarantee that matters in a program meant to run unattended.
The type also needs a home. The layout from last chapter reserved
internal/sim for the simulation itself, and this is the moment it earns
its keep.
cmd/worldd is the command: it starts things and prints things. The world's
own vocabulary, starting today with what a place is, belongs to the sim
package, where the command can use it but does not own it.
The Coord struct
// internal/sim/coord.go
// Package sim holds the simulation's own types. Nothing in here
// prints, listens, or knows that a command called worldd exists.
package sim
// Coord is a place on the world grid: column X, row Y.
type Coord struct {
X int
Y int
}
// cmd/worldd/main.go
package main
import (
"fmt"
"theworld/internal/sim"
)
func main() {
var origin sim.Coord
spring := sim.Coord{X: 12, Y: 7}
fmt.Println("origin:", origin)
fmt.Println("spring:", spring)
fmt.Println("spring column:", spring.X)
}
$ go run ./cmd/worldd
origin: {0 0}
spring: {12 7}
spring column: 12
type Coord struct { ... } teaches Go a new noun. From that line on,
Coord is as real to the compiler as int: you can declare
variables of it, pass it, return it.
The import path theworld/internal/sim is the module name from last
chapter plus the directory. Inside main, the package's names arrive
with a sim. prefix.
Capitalized names like Coord and X are the ones a package
makes visible outside itself.
var origin sim.Coord got the zero value with no assignment anywhere.
A struct's zero value is every field at its own zero, so an unset place is
{0 0}, the top-left corner, never garbage.
The struct literal sim.Coord{X: 12, Y: 7} builds a value with
the fields named, so the reader of the call site knows which 12 is which. The dot in
spring.X reaches into the value for one field.
A Coord that only sits there is still a pair of boxes. The server will
constantly step from one place to a neighbor and measure how far apart two places are.
Those questions should live on the type, so that every part of the program asks the same code the same question. That is what a method is: a function with a home.
// internal/sim/coord.go — add below the type
// Offset returns the coordinate dx columns and dy rows away.
func (c Coord) Offset(dx, dy int) Coord {
return Coord{X: c.X + dx, Y: c.Y + dy}
}
// Dist returns the number of grid steps between two coordinates,
// counting only moves along a row or a column.
func (c Coord) Dist(o Coord) int {
dx := c.X - o.X
if dx < 0 {
dx = -dx
}
dy := c.Y - o.Y
if dy < 0 {
dy = -dy
}
return dx + dy
}
// cmd/worldd/main.go — the new body of main
var origin sim.Coord
spring := sim.Coord{X: 12, Y: 7}
east := spring.Offset(1, 0)
fmt.Println("east of spring:", east)
fmt.Println("origin to spring:", origin.Dist(spring), "steps")
fmt.Println("spring to origin:", spring.Dist(origin), "steps")
$ go run ./cmd/worldd
east of spring: {13 7}
origin to spring: 19 steps
spring to origin: 19 steps
The part in parentheses before the name, (c Coord), is the
receiver. It declares that Offset belongs to
Coord, and inside the method c is the coordinate the call
happened on.
You call it with the same dot that reads a field. spring.Offset(1, 0)
asks the spring for the square one column east.
Dist counts steps the way something walking the grid would, no
diagonals: from the origin to the spring is 12 columns plus 7 rows, 19 steps. The
two if blocks flip negative differences positive, so the answer comes
out the same from either end, as the run confirms.
Offset never touches c. It builds and returns a new
Coord, and spring is still {12 7} afterwards.
That choice turns into this chapter's worked failure.
The Cell struct
A coordinate names a square. A Cell is what the square holds.
For now, before terrain proper arrives, a cell needs three things: where it is, how high its ground sits, and how deep the water standing on it runs. One of those fields is a struct, and Go does not blink: a field's type can be any type, including one you defined a page ago.
// internal/sim/cell.go
package sim
// Cell is one square of ground: where it sits, how high the ground
// is, and how deep the water standing on it runs. Heights and depths
// are in meters.
type Cell struct {
At Coord
Elevation float64
Water float64
}
// Wet reports whether any water stands on the cell.
func (c Cell) Wet() bool {
return c.Water > 0
}
// Surface returns the height of the cell's top: ground plus water.
func (c Cell) Surface() float64 {
return c.Elevation + c.Water
}
// cmd/worldd/main.go — the new body of main
spring := sim.Coord{X: 12, Y: 7}
pool := sim.Cell{At: spring, Elevation: 1.0, Water: 0.4}
bank := sim.Cell{At: spring.Offset(1, 0), Elevation: 2.5}
fmt.Println("pool:", pool.At, "wet:", pool.Wet(), "surface:", pool.Surface())
fmt.Println("bank:", bank.At, "wet:", bank.Wet(), "surface:", bank.Surface())
$ go run ./cmd/worldd
pool: {12 7} wet: true surface: 1.4
bank: {13 7} wet: false surface: 2.5
The bank literal names only two of the three fields, and
Water quietly takes its zero: dry, exactly as a field left unsaid should
be.
Both methods answer questions any part of the server might ask, and both answer them
in one place. If the definition of wet ever grows a threshold, the code that changes
is one line in sim, not a comparison copied through the codebase.
That is the case for methods in one sentence: the type carries its own vocabulary, so the rest of the program asks instead of assuming.
Figure 2.1 — the pool at column 12, row 7, and the single struct value that carries everything the server knows about that square.
Pointer receivers
Wet and Surface read a cell. A server also needs to change
one: rain falls, and the water on a square deepens.
Following the pattern of every method so far gives an obvious next method. It is wrong in a way no error message reports.
// internal/sim/cell.go — the obvious attempt
// Flood adds depth meters of standing water to the cell.
func (c Cell) Flood(depth float64) {
c.Water += depth
}
// cmd/worldd/main.go — the new body of main
spring := sim.Coord{X: 12, Y: 7}
bank := sim.Cell{At: spring.Offset(1, 0), Elevation: 2.5}
fmt.Println("before:", "wet:", bank.Wet(), "water:", bank.Water)
bank.Flood(0.3)
fmt.Println("after: ", "wet:", bank.Wet(), "water:", bank.Water)
$ go run ./cmd/worldd
before: wet: false water: 0
after: wet: false water: 0
It compiles. go vet ./... passes. The line c.Water += depth
runs, and adds 0.3 to something.
Reason from the symptom: the addition definitely happened, so there must have been a
second Cell for it to happen to. There was.
Go passes copies. Calling a function with a struct hands the function a duplicate of
it, and a receiver written (c Cell) is a parameter like any other.
c was a copy of bank, the flood soaked the copy, and the
copy was thrown away when the method returned.
Stage 2's Offset had the same receiver and never noticed, because it
never tried to change c. It returned a new value instead. A reading
method on a copy is correct. A writing method on a copy is a no-op with good
intentions.
// internal/sim/cell.go — the fix: *Cell, not Cell
// Flood adds depth meters of standing water to the cell.
func (c *Cell) Flood(depth float64) {
c.Water += depth
}
$ go run ./cmd/worldd
before: wet: false water: 0
after: wet: true water: 0.3
One character changed. *Cell means the method receives not a copy of
the cell but its address: a pointer, a value that says where the original
lives in memory.
Through the pointer, c.Water += depth reaches the actual
bank, and the run shows the water arriving and staying.
The call site did not change at all. bank.Flood(0.3) still reads the
same, because Go sees a pointer receiver and passes the address for you.
The receiver declaration is where the choice lives, and it is a one-word answer to one question: does this method change the thing it belongs to? No: value receiver. Yes: pointer receiver.
Why copies are the default
Everything in this chapter rests on one property of Go: assignment and argument passing
copy the value, whatever the value is. An int copies, and so does a struct
of two ints, and so does a struct holding another struct.
Copies make code easy to reason about. No function can change your variables behind your back unless you handed it their address on purpose.
The cost is the trap you walked out of: a method that wants to mutate must say so, with
a pointer receiver. The * in the declaration makes that statement in
public. When you read func (c *Cell) Flood six volumes from now, it will
still tell you at a glance that flooding changes the cell.
The same property explains why Coord can stay copy-friendly forever. A
coordinate is a fact, not a thing with a life: moving something should produce the new
place, not edit the old one.
Offset returning a fresh value means the spring's coordinates cannot be
corrupted by whoever borrowed them.
Small immutable facts get value receivers. Stateful residents of the world, of which
Cell is the first and creatures will be louder, get pointer receivers on
anything that changes them. The whole server stays readable through that split.
That is what behavior beside data buys in practice. The two types you wrote are not passive records with helper functions scattered around them; they are the beginnings of a vocabulary.
Code elsewhere in the server will say cell.Wet() and
place.Dist(target), sentences with the noun and the verb attached. The
definitions of those verbs sit in one file, next to the fields they read, where a change
is one edit.
Checkpoint
- Define a struct type, build one with a named-field literal, and predict
its zero value field by field, including
{{0 0} 0 0}for a struct inside a struct. - Explain what the two loose
ints design costs, and what binding them intoCoordmakes impossible. - Write a method, name its receiver, and say precisely what
cis insideOffsetwhenspring.Offset(1, 0)runs. - Compute
Distby hand for any two coordinates: 12 columns plus 7 rows made the run print 19 steps. - Handed a method that compiles, runs, and changes nothing, check the receiver before anything else, and narrate where the lost mutation went.
- Choose between
(c Cell)and(c *Cell)for a new method and defend the choice in one sentence.
Exercise 1 — a coordinate that knows its limits. Give
Coord a method In(w, h int) bool that reports whether
the coordinate lies on a grid w columns wide and h rows
tall. Print spring.In(64, 64) and
sim.Coord{X: 64, Y: 7}.In(64, 64), and decide before running which
is false.
Four comparisons joined with &&:
c.X >= 0 && c.X < w && c.Y >= 0 &&
c.Y < h. The prints come out true then
false: on a 64-wide grid the columns run 0 through 63, so
X: 64 is the first column that does not exist. Off-by-one
boundaries like this are exactly where a method earns its keep, because the
<-versus-<= decision gets made once.
Exercise 2 — drain, but never below dry. Write
Drain(depth float64) on Cell: it removes up to
depth meters of standing water and refuses to go negative. Start
a cell at Water: 0.4, drain 1.0, and print the result. Which
receiver does it need?
Pointer receiver, because draining changes the cell: subtract, then clamp with
if c.Water < 0 { c.Water = 0 }. The print reads
pool water: 0, not -0.6. Write it with
(c Cell) first if you want the failure box again on your own
terms: the print stays 0.4, and this time you can name the missing character.
Exercise 3 — predict the zero world. Declare
var blank sim.Cell and write down, before running, exactly what
fmt.Println(blank) prints, field by field. Then run it.
{{0 0} 0 0}: the inner braces are the zero Coord
inside At, then Elevation and Water at
0. A blank cell is a dry square at the origin on the valley floor: every field
defined, none of them meaningful yet. Distinguishing "zero" from "never set" is
a real problem this server will meet again, and the first defense is knowing
the zero values cold.