Comparing and Copying Structs

Structs are values: assignment copies every field, and == compares field by field when every field type is comparable.

Syntaxa == b // allowed only when every field type is comparable

A struct in Go is a value, the same way a number is. The variable holds the fields themselves, not a reference to fields kept somewhere else. That single fact explains both how structs copy and how they compare.

Copying

Assignment copies every field into the new variable. After b := a, changing b.X leaves a.X alone; they are two separate values. Passing a struct to a function copies it too, so the function works on its own copy and cannot change the caller's value. Returning a struct copies it back out.

Comparing

== on two structs of the same type compares them field by field and is true only when every pair of fields is equal. != is the opposite. This is allowed only when every field type is comparable. Numbers, strings, booleans, pointers, channels, interfaces, arrays of comparable types, and structs whose fields are all comparable are comparable. Slices, maps and functions are not. A comparable struct can also be used as a map key, which is how a pair of coordinates becomes one key.

What trips people up

The copy is shallow. A slice or map field copies the small header that refers to the elements, not the elements, so the copy and the original share the same underlying data. Change an element through one and it changes for both. Separately, == on a struct that contains a slice, map or function field is a compile error, not a false result; use reflect.DeepEqual, or compare the fields you actually care about. A pointer field compares addresses, so two structs pointing at equal-looking but separate values are not equal.

Example

Example · go
package main

import "fmt"

type Point struct {
	X int
	Y int
}

type Tags struct {
	Names []string
}

func main() {
	a := Point{X: 1, Y: 2}
	b := Point{X: 1, Y: 2}

	// == compares field by field. Two separate values can still be equal.
	fmt.Println(a == b)

	// Assignment copies every field, so c is independent of a.
	c := a
	c.X = 99
	fmt.Println(a.X, c.X)

	// Tags holds a slice, so Tags values cannot be compared with ==.
	// Writing Tags{} == Tags{} would stop this program compiling.
	t1 := Tags{Names: []string{"go"}}

	// The copy gets its own header, pointing at the same elements.
	t2 := t1
	t2.Names[0] = "rust"
	fmt.Println(t1.Names[0])
}

// Output:
// true
// 1 99
// rust

When to use it

  • Use a two-field struct as a map key instead of joining the parts into a string.
  • Check a small value type such as a coordinate or a version for equality with ==.
  • Pass a struct by value to a function that must not change the caller's copy.
  • Compare structs containing slices in a test with reflect.DeepEqual.

More examples

A struct as a map key

A map key must be comparable, and a struct of comparable fields qualifies. This replaces tricks like building a string key of "1,2": the key stays typed and cannot be built wrongly. Reach for it for grid positions, a pair of IDs, or any compound identity.

Example · go
package main

import "fmt"

type Cell struct {
	Row int
	Col int
}

func main() {
	seen := map[Cell]string{}
	seen[Cell{Row: 1, Col: 2}] = "x"

	// Equal field values mean the same key, so this overwrites.
	seen[Cell{Row: 1, Col: 2}] = "y"
	fmt.Println(len(seen), seen[Cell{Row: 1, Col: 2}])

	_, ok := seen[Cell{Row: 9, Col: 9}]
	fmt.Println(ok)
}

// Output:
// 1 y
// false

Comparing structs that == cannot touch

reflect.DeepEqual walks both values and compares slice elements and map entries one by one, so it answers the question == refuses to. It is slower than == and it treats a nil slice and an empty slice as different, so keep it for tests and one-off checks rather than hot paths.

Example · go
package main

import (
	"fmt"
	"reflect"
)

type Config struct {
	Hosts []string
	Debug bool
}

func main() {
	a := Config{Hosts: []string{"a", "b"}}
	b := Config{Hosts: []string{"a", "b"}}

	// a == b would not compile, because Hosts is a slice.
	fmt.Println(reflect.DeepEqual(a, b))

	b.Hosts[1] = "c"
	fmt.Println(reflect.DeepEqual(a, b))
}

// Output:
// true
// false

A pointer field compares addresses

Pointers get their own lesson shortly: &a is the address where a is stored, and *x.Link is the value at that address. The point here is that == on a pointer field asks whether two structs point at the same value, not whether they point at equal values. Comparing records that hold pointers almost never means what you hoped.

Example · go
package main

import "fmt"

type Box struct {
	Name string
	Link *Box
}

func main() {
	a := Box{Name: "a"}
	x := Box{Name: "same", Link: &a}
	y := Box{Name: "same", Link: &a}

	// Both Link fields hold the same address, so x and y are equal.
	fmt.Println(x == y)

	b := Box{Name: "a"}
	z := Box{Name: "same", Link: &b}

	// b has the same fields as a but is stored elsewhere.
	fmt.Println(x == z, *x.Link == *z.Link)
}

// Output:
// true
// false true

Discussion

  • Be the first to comment on this lesson.