A struct groups related values into one type: a user with a name, an email and an age, or a point with an X and a Y. After this lesson you’ll be able to define your own struct types, create and change struct values, work with pointers to structs, and write the small constructor functions Go programmers use everywhere.
Defining a struct type
You define a struct with type Name struct { … } and list its fields, each with a name and a type. Then you create values of it with a struct literal:
package main
import "fmt"
type User struct {
Name string
Email string
Age int
}
func main() {
u := User{Name: "Ana", Email: "[email protected]", Age: 31}
fmt.Println(u)
fmt.Println(u.Name, u.Age)
u.Age++
fmt.Printf("%+v\n", u)
}Code language: Go (go)
{Ana [email protected] 31}
Ana 31
{Name:Ana Email:[email protected] Age:32}Code language: plaintext (plaintext)
You read and write fields with a dot: u.Name, u.Age++. fmt.Println prints just the values in braces; the %+v verb adds the field names, which is what I use when debugging.
Struct types are usually defined at the top level of a file, outside any function, so the whole package can use them. Fields of the same type can share a line (X, Y int), and gofmt lines up the types for you.
Struct literals: name your fields
In a literal you can name the fields, as above, or list the values in order: User{"Ana", "[email protected]", 31}. The positional form works, but it breaks (or worse, silently mixes up values) as soon as someone adds or reorders fields. I always name the fields, and so does almost every Go codebase.
With named fields the compiler also catches typos:
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
p := Point{X: 1, Z: 2}
fmt.Println(p)
}Code language: Go (go)
./main.go:10:19: unknown field Z in struct literal of type PointCode language: plaintext (plaintext)
Zero values
Any field you leave out of a literal gets its zero value, and a struct declared with var has all fields at their zero values. There’s no “uninitialized” struct in Go:
package main
import "fmt"
type User struct {
Name string
Email string
Age int
}
func main() {
var empty User
fmt.Printf("%+v\n", empty)
ben := User{Name: "Ben"}
fmt.Printf("%+v\n", ben)
fmt.Println(ben.Email == "")
}Code language: Go (go)
{Name: Email: Age:0}
{Name:Ben Email: Age:0}
trueCode language: plaintext (plaintext)
Good Go types are designed so that the zero value is already useful. You’ll see that idea again when you meet types like strings.Builder and sync.Mutex, which work without any setup.
Structs are values
Like arrays (and unlike slices and maps), a struct is a plain value. Assigning it or passing it to a function copies all of its fields:
package main
import "fmt"
type User struct {
Name string
Age int
}
func birthday(u User) {
u.Age++
fmt.Println("inside:", u.Age)
}
func main() {
ana := User{Name: "Ana", Age: 31}
copyOfAna := ana
copyOfAna.Name = "Not Ana"
birthday(ana)
fmt.Println(ana.Name, ana.Age)
}Code language: Go (go)
inside: 32
Ana 31Code language: plaintext (plaintext)
birthday changed its own copy, and ana is still 31. Renaming copyOfAna didn’t touch ana either. To let a function change the caller’s struct, pass a pointer.
Pointers to structs
You met pointers in Variables, types and pointers: &x takes the address of x, and *p follows the pointer. With structs there’s a convenience: you can use the dot directly on a pointer, and Go follows it for you. p.Age means (*p).Age.
package main
import "fmt"
type User struct {
Name string
Age int
}
func birthday(u *User) {
u.Age++ // same as (*u).Age++
}
func main() {
ana := User{Name: "Ana", Age: 31}
birthday(&ana)
fmt.Println(ana.Age)
ben := &User{Name: "Ben", Age: 25}
birthday(ben)
fmt.Println(ben.Name, ben.Age)
fmt.Println(ben)
}Code language: Go (go)
32
Ben 26
&{Ben 26}Code language: plaintext (plaintext)
&User{…} creates a struct and gives you a pointer to it in one step, which is very common. When you print a pointer to a struct, fmt shows the struct with a & in front.
When should you use a pointer? Use one when a function needs to change the struct, or when the struct is large and copying it on every call would be wasteful. For small structs that are only read, passing the value is simpler and perfectly fine. The Methods lesson turns this into a clear rule of thumb.
Constructor functions
Go has no constructors in the language. Instead, when creating a value needs defaults or validation, you write an ordinary function, by convention named NewTypeName:
package main
import (
"errors"
"fmt"
"time"
)
type Server struct {
Host string
Port int
Timeout time.Duration
}
func NewServer(host string) (*Server, error) {
if host == "" {
return nil, errors.New("host is required")
}
return &Server{Host: host, Port: 8080, Timeout: 30 * time.Second}, nil
}
func main() {
s, err := NewServer("localhost")
if err != nil {
fmt.Println("error:", err)
return
}
s.Port = 9000
fmt.Printf("%+v\n", *s)
_, err = NewServer("")
fmt.Println("error:", err)
}Code language: Go (go)
{Host:localhost Port:9000 Timeout:30s}
error: host is requiredCode language: plaintext (plaintext)
Nothing about NewServer is special: it’s a function that returns a *Server and an error, exactly like the functions in Errors. Returning a pointer is common for constructors, but returning a plain Server is fine too, especially for small types. If the zero value works and there’s nothing to check, skip the constructor and let callers write a literal.
Anonymous structs
Sometimes you need a struct just once and a named type would be overkill. You can write the struct type right where you use it:
package main
import "fmt"
func main() {
point := struct {
X, Y int
}{X: 3, Y: 4}
fmt.Println(point.X, point.Y)
cases := []struct {
input string
want int
}{
{input: "go", want: 2},
{input: "gopher", want: 6},
}
for _, c := range cases {
fmt.Println(c.input, len(c.input) == c.want)
}
}Code language: Go (go)
3 4
go true
gopher trueCode language: plaintext (plaintext)
The second form, a slice of anonymous structs, is exactly how Go programmers write table-driven tests. You’ll use it a lot in Testing.
Comparing structs
Two struct values are equal with == when all their fields are equal:
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
a := Point{X: 1, Y: 2}
b := Point{X: 1, Y: 2}
c := Point{X: 2, Y: 1}
fmt.Println(a == b, a == c)
visited := map[Point]bool{a: true}
fmt.Println(visited[b], visited[c])
}Code language: Go (go)
true false
true falseCode language: plaintext (plaintext)
Because they’re comparable, structs like Point can even be map keys (see Maps). That only works if every field is comparable. A struct with a slice field can’t use ==:
package main
import "fmt"
type Post struct {
Title string
Tags []string
}
func main() {
a := Post{Title: "Hi"}
b := Post{Title: "Hi"}
fmt.Println(a == b)
}Code language: Go (go)
./main.go:13:14: invalid operation: a == b (struct containing []string cannot be compared)Code language: plaintext (plaintext)
For those, compare the fields yourself (using slices.Equal for the slice).
Embedding one struct in another
A struct can contain another struct as a normal field. It can also embed one by listing the type without a field name. The embedded struct’s fields are then promoted: you can reach them directly.
package main
import "fmt"
type Address struct {
City string
Country string
}
type Customer struct {
Name string
Address
}
func main() {
c := Customer{
Name: "Ana",
Address: Address{City: "Lisbon", Country: "Portugal"},
}
fmt.Println(c.City)
fmt.Println(c.Address.Country)
fmt.Printf("%+v\n", c)
}Code language: Go (go)
Lisbon
Portugal
{Name:Ana Address:{City:Lisbon Country:Portugal}}Code language: plaintext (plaintext)
c.City is shorthand for c.Address.City; the field is still called Address (after its type), and both spellings work. Embedding isn’t inheritance: a Customer is not an Address, it just has one and lets you skip a step. It becomes more interesting with methods, which are promoted the same way. Use it lightly; a named field is often clearer.
Two things you’ll see in real code. First, field names that start with a capital letter (Name) are visible to other packages, and lowercase ones (name) are private to their own package. This matters once your code spans packages; Packages and modules covers it.
Second, a field can carry a tag, a string in backquotes after the type:
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
}Code language: Go (go)
Tags don’t change how the struct behaves in your code. They’re notes that other packages can read; here they tell the JSON encoder which key names to use. You’ll put them to work in Working with JSON.
Practice
1. Bookshelf. Define a Book struct with Title, Author and Pages. Put three books in a slice, then print the total number of pages and the title of the longest book.
Show a solution
package main
import "fmt"
type Book struct {
Title string
Author string
Pages int
}
func main() {
books := []Book{
{Title: "Gardening for Gophers", Author: "M. Burrow", Pages: 380},
{Title: "The Long Loop", Author: "A. Range", Pages: 491},
{Title: "Small Interfaces", Author: "I. Reader", Pages: 264},
}
total := 0
longest := books[0]
for _, b := range books {
total += b.Pages
if b.Pages > longest.Pages {
longest = b
}
}
fmt.Println("total pages:", total)
fmt.Println("longest:", longest.Title)
}Code language: Go (go)
total pages: 1135
longest: The Long LoopCode language: plaintext (plaintext)(Inside a slice literal you can leave out the type name before each {…}; Go knows they’re Books.)
2. Move every point. Write func move(p *Point, dx, dy int). Then make a slice of three points and move each of them by (1, 1). Careful: for _, p := range points gives you a copy of each element. Print the slice to check that the points really moved.
Show a solution
package main
import "fmt"
type Point struct {
X, Y int
}
func move(p *Point, dx, dy int) {
p.X += dx
p.Y += dy
}
func main() {
points := []Point{{X: 0, Y: 0}, {X: 5, Y: 2}, {X: -1, Y: 3}}
for i := range points {
move(&points[i], 1, 1)
}
fmt.Println(points)
}Code language: Go (go)
[{1 1} {6 3} {0 4}]Code language: plaintext (plaintext)Taking the address of the loop variable, move(&p, 1, 1) inside for _, p := range points, compiles fine but only moves the copy. Using the index and &points[i] points at the real element in the slice.
Key takeaways
- Define structs with
type Name struct { … }and create them with literals that name their fields. - Missing fields get their zero values; aim for types whose zero value is useful.
- Structs are copied on assignment and when passed to functions. Pass a pointer when the function must change the struct;
p.Fieldfollows the pointer for you. - A constructor is just a function named
NewXthat sets defaults or validates input. - Structs compare with
==(and work as map keys) when all their fields are comparable. - Embedding promotes the inner struct’s fields; it’s a shortcut, not inheritance.