Go · 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.