Skip to main content
Go · lesson 11 of 25
Go beginner Lesson 11 of 25

Structs in Go

Go structs run for real: literals and zero values, value vs pointer receivers, constructors, embedding and promotion, JSON tags, comparison, and padding.

By SkillByExample · Updated

A struct groups named fields into one type. Methods attach behaviour to it, embedding composes types, and tags tell packages like encoding/json how to treat each field. Each example below is a runnable program.

Defining and creating structs

Fields not given in a literal get their zero values.

package main

import "fmt"

type User struct {
	ID    int
	Name  string
	Email string
	Admin bool
}

func main() {
	var zero User
	named := User{Name: "Alice", ID: 1}
	positional := User{2, "Bob", "bob@example.com", true}
	p := &User{ID: 3, Name: "Carol"}
	p.Email = "carol@example.com" // no -> needed: Go dereferences for you

	fmt.Printf("%+v\n%+v\n%+v\n%+v\n", zero, named, positional, *p)
}
$ go run literals.go
{ID:0 Name: Email: Admin:false}
{ID:1 Name:Alice Email: Admin:false}
{ID:2 Name:Bob Email:bob@example.com Admin:true}
{ID:3 Name:Carol Email:carol@example.com Admin:false}

Named-field literals can list fields in any order and leave some out; positional literals must give every field in order and break when a field is added, so prefer names outside of tiny local types. p.Email works on a pointer because Go automatically dereferences p for field access.

Value vs pointer receivers

A method receives a copy of its receiver unless the receiver is a pointer.

package main

import "fmt"

type Counter struct{ n int }

func (c Counter) IncValue()    { c.n++ } // modifies a copy
func (c *Counter) IncPointer() { c.n++ } // modifies the original
func (c Counter) Value() int   { return c.n }

func main() {
	var c Counter
	c.IncValue()
	c.IncValue()
	fmt.Println("after IncValue x2:", c.Value())
	c.IncPointer() // Go takes &c automatically
	c.IncPointer()
	fmt.Println("after IncPointer x2:", c.Value())
}
$ go run receivers.go
after IncValue x2: 0
after IncPointer x2: 2

IncValue incremented a copy that was discarded on return, so the count stayed 0 — the method compiled and ran without any warning. Use a pointer receiver whenever a method changes the struct; c.IncPointer() on an addressable variable is shorthand for (&c).IncPointer().

Constructors

Go has no constructors; a NewX function that validates and returns a pointer is the convention.

package main

import (
	"errors"
	"fmt"
)

type Account struct {
	owner   string
	balance float64
}

func NewAccount(owner string, opening float64) (*Account, error) {
	if owner == "" {
		return nil, errors.New("owner required")
	}
	return &Account{owner: owner, balance: opening}, nil
}

func (a *Account) Deposit(x float64) { a.balance += x }
func (a *Account) Balance() float64  { return a.balance }

func main() {
	acc, err := NewAccount("ann", 100)
	fmt.Println(err)
	acc.Deposit(25.5)
	fmt.Println(acc.Balance())

	_, err = NewAccount("", 0)
	fmt.Println(err)
}
$ go run ctor.go
<nil>
125.5
owner required

Lower-case fields are unexported: code in other packages can’t set balance directly, so NewAccount and the methods are the only way to create or change an Account. Returning a pointer to a local variable is safe in Go — the compiler moves it to the heap when it escapes (the memory lesson shows that decision).

Embedding and promotion

Embedding a type (a field with no name) promotes its fields and methods to the outer struct.

package main

import "fmt"

type Animal struct{ Name string }

func (a Animal) Speak() string    { return a.Name + " makes a sound" }
func (a Animal) Describe() string { return "I am " + a.Name + ": " + a.Speak() }

type Dog struct {
	Animal
	Breed string
}

func (d Dog) Speak() string { return d.Name + " barks" }

func main() {
	d := Dog{Animal{"Rex"}, "Labrador"}
	fmt.Println(d.Name, d.Breed)
	fmt.Println(d.Speak())
	fmt.Println(d.Animal.Speak())
	fmt.Println(d.Describe())
}
$ go run embed.go
Rex Labrador
Rex barks
Rex makes a sound
I am Rex: Rex makes a sound

Dog.Speak shadows the promoted Animal.Speak, and the inner one is still reachable as d.Animal.Speak(). The last line is the difference from inheritance: Describe is Animal’s method, and inside it a.Speak() calls Animal.Speak, not Dog’s. There are no virtual methods. When you need behaviour that varies by type, use an interface.

Anonymous structs

A struct type can be declared inline for one-off data, such as test tables or a JSON payload.

package main

import "fmt"

func main() {
	point := struct{ X, Y int }{3, 4}
	fmt.Printf("%+v\n", point)

	tests := []struct {
		in   string
		want int
	}{
		{"go", 2},
		{"gopher", 6},
	}
	for _, tt := range tests {
		fmt.Println(tt.in, len(tt.in) == tt.want)
	}
}
$ go run anon.go
{X:3 Y:4}
go true
gopher true

The []struct{...}{...} table is the backbone of Go’s table-driven tests, shown in the testing lesson.

Struct tags and JSON

Tags are string metadata on fields; encoding/json reads the json key.

package main

import (
	"encoding/json"
	"fmt"
)

type Product struct {
	ID       int     `json:"id"`
	Name     string  `json:"name"`
	Price    float64 `json:"price,omitempty"`
	Secret   string  `json:"-"`
	internal string
}

func main() {
	p := Product{ID: 1, Name: "Widget", Secret: "s3cret", internal: "x"}
	b, _ := json.Marshal(p)
	fmt.Println(string(b))

	var back Product
	err := json.Unmarshal([]byte(`{"id":2,"name":"Gadget","price":9.5,"extra":true}`), &back)
	fmt.Printf("%+v %v\n", back, err)
}
$ go run tags.go
{"id":1,"name":"Widget"}
{ID:2 Name:Gadget Price:9.5 Secret: internal:} <nil>

omitempty dropped the zero price, json:"-" hid Secret, and the unexported internal is never encoded — encoding/json can’t see lower-case fields at all. On the way in, the unknown extra key is silently ignored; use a json.Decoder with DisallowUnknownFields if that should be an error.

Comparing structs

Structs are comparable with == when all their fields are.

package main

import "fmt"

type Point struct{ X, Y int }

func main() {
	fmt.Println(Point{1, 2} == Point{1, 2}, Point{1, 2} == Point{2, 1})

	seen := map[Point]bool{{0, 0}: true} // comparable structs work as map keys
	fmt.Println(seen[Point{0, 0}], seen[Point{1, 1}])
}
$ go run compare.go
true false
true false

Equality is field by field, and a comparable struct can be a map key — handy for grid coordinates or composite keys. Add one slice field and that stops working:

package main

import "fmt"

type Tagged struct {
	Name string
	Tags []string
}

func main() {
	a, b := Tagged{"x", nil}, Tagged{"x", nil}
	fmt.Println(a == b)
}
$ go run comparefail.go
# command-line-arguments
./comparefail.go:12:14: invalid operation: a == b (struct containing []string cannot be compared)

Slices aren’t comparable, so neither is a struct containing one, and the error appears at compile time rather than run time. Compare such types with reflect.DeepEqual or a hand-written Equal method.

Field order and size

The compiler aligns fields, so field order changes a struct’s size.

package main

import (
	"fmt"
	"unsafe"
)

type Loose struct {
	A bool
	B int64
	C bool
}

type Tight struct {
	B int64
	A bool
	C bool
}

func main() {
	fmt.Println(unsafe.Sizeof(Loose{}), unsafe.Sizeof(Tight{}))
}
$ go run padding.go
24 16

int64 must sit on an 8-byte boundary, so Loose pads 7 bytes after A and 7 more after C. Grouping the small fields together saves 8 bytes per value. It only matters for types you hold millions of, but it’s free to get right.

Practice

1. Add a Withdraw(x float64) error method to Account in ctor.go that refuses to overdraw.

Solution
package main

import "fmt"

type Account struct{ balance float64 }

func (a *Account) Withdraw(x float64) error {
	if x > a.balance {
		return fmt.Errorf("insufficient funds: have %.2f, need %.2f", a.balance, x)
	}
	a.balance -= x
	return nil
}

func main() {
	a := &Account{balance: 50}
	for _, amt := range []float64{20, 40} {
		err := a.Withdraw(amt)
		fmt.Println(err, a.balance)
	}
}
$ go run p1.go
<nil> 30
insufficient funds: have 30.00, need 40.00 30

It must be a pointer receiver, or the deduction would apply to a copy.

2. Give Dog in embed.go its own Describe method that calls d.Speak(), so d.Describe() reports the bark.

Solution
package main

import "fmt"

type Animal struct{ Name string }

func (a Animal) Speak() string { return a.Name + " makes a sound" }

type Dog struct{ Animal }

func (d Dog) Speak() string    { return d.Name + " barks" }
func (d Dog) Describe() string { return "I am " + d.Name + ": " + d.Speak() }

func main() {
	fmt.Println(Dog{Animal{"Rex"}}.Describe())
}
$ go run p2.go
I am Rex: Rex barks

Inside Dog.Describe, the receiver is a Dog, so d.Speak() resolves to Dog.Speak.

3. Decode {"id":7,"name":"Lamp","price":12.5} into a Product, then marshal it back with json.MarshalIndent.

Solution
package main

import (
	"encoding/json"
	"fmt"
)

type Product struct {
	ID    int     `json:"id"`
	Name  string  `json:"name"`
	Price float64 `json:"price,omitempty"`
}

func main() {
	var p Product
	if err := json.Unmarshal([]byte(`{"id":7,"name":"Lamp","price":12.5}`), &p); err != nil {
		panic(err)
	}
	out, _ := json.MarshalIndent(p, "", "  ")
	fmt.Println(string(out))
}
$ go run p3.go
{
  "id": 7,
  "name": "Lamp",
  "price": 12.5
}

Unmarshal needs a pointer (&p) so it can fill the struct in.

4. Reorder the fields of struct { a bool; b int32; c bool; d int64; e bool } to make it as small as possible, and print both sizes.

Solution
package main

import (
	"fmt"
	"unsafe"
)

type Before struct {
	a bool
	b int32
	c bool
	d int64
	e bool
}

type After struct {
	d       int64
	b       int32
	a, c, e bool
}

func main() {
	fmt.Println(unsafe.Sizeof(Before{}), unsafe.Sizeof(After{}))
}
$ go run p4.go
32 16

Largest alignment first: 8 + 4 + 3 bytes is 15, rounded up to a multiple of 8.

Frequently Asked Questions

Does Go have classes?
No. Go uses structs for data and attaches behaviour with methods, functions declared with a receiver of the struct type. There is no inheritance; types are composed by embedding, and polymorphism comes from interfaces.
Should a method use a value receiver or a pointer receiver?
Use a pointer receiver when the method must modify the struct, when the struct is large, or when it contains something that must not be copied, such as a sync.Mutex. Use a value receiver for small, immutable values. Keep all of a type's methods consistent where possible.
What is struct embedding in Go?
Embedding declares a field with only a type name, such as Animal inside Dog. The embedded type's fields and methods are promoted, so dog.Name and dog.Speak() work. It is composition, not inheritance: the embedded method still receives the inner Animal, not the Dog.
What are struct tags used for?
A tag is a string literal after a field, such as `json:"name,omitempty"`, read at run time through reflection. encoding/json uses it to choose the JSON key, omit empty values, or skip a field with json:"-". Unexported fields are never encoded, whatever their tag.
Can Go structs be compared with ==?
Yes, if every field is comparable; two structs are equal when all corresponding fields are equal. A struct containing a slice, map or function field cannot be compared with == and fails to compile; use reflect.DeepEqual or a hand-written Equal method instead.