Structs

A struct groups related values into one type: a User with a name, email and join date, or a Point with X and Y. Structs are Go's main way to model data — there are no classes. This lesson covers defining struct types, creating values, accessing and comparing fields, anonymous structs, struct tags, and defining your own types in general.

Defining and creating structs

GO
package main

import "fmt"

type Book struct {
	Title  string
	Author string
	Pages  int
	OnLoan bool
}

func main() {
	b1 := Book{Title: "Dune", Author: "Frank Herbert", Pages: 412}
	b2 := Book{"Emma", "Jane Austen", 474, true} // positional: all fields, in order
	var b3 Book                                  // every field at its zero value

	fmt.Println(b1)
	fmt.Printf("%+v\n", b2)
	fmt.Printf("%+v\n", b3)

	b3.Title = "Untitled"
	b3.Pages = b1.Pages + b2.Pages
	fmt.Println(b3.Title, b3.Pages, b1.Author)
}
Output
{Dune Frank Herbert 412 false}
{Title:Emma Author:Jane Austen Pages:474 OnLoan:true}
{Title: Author: Pages:0 OnLoan:false}
Untitled 886 Frank Herbert

Prefer the Field: value form: it can leave out fields (they get zero values), reads clearly, and keeps working if fields are added or reordered later. The positional form must list every field and breaks when the struct changes.

Structs are values

Like arrays, structs are copied when assigned or passed to a function. To let a function modify a struct, pass a pointer (next lesson):

GO
package main

import "fmt"

type Point struct{ X, Y int }

func moveRight(p Point) {
	p.X += 10 // changes the copy
}

func main() {
	a := Point{1, 2}
	b := a
	b.X = 99
	moveRight(a)
	fmt.Println(a, b)
	fmt.Println(a == Point{1, 2}, a == b)
}
Output
{1 2} {99 2}
true false

Structs whose fields are all comparable can be compared with ==, field by field, and used as map keys.

Nested structs

GO
package main

import "fmt"

type Address struct {
	Street, City, Postcode string
}

type Customer struct {
	Name     string
	Email    string
	Shipping Address
	Tags     []string
}

func main() {
	c := Customer{
		Name:  "Ana Lima",
		Email: "ana@example.com",
		Shipping: Address{
			Street: "12 Rua Augusta",
			City:   "Lisbon",
		},
		Tags: []string{"vip"},
	}
	c.Shipping.Postcode = "1100-053"
	fmt.Println(c.Name, "ships to", c.Shipping.City, c.Shipping.Postcode)
	fmt.Printf("%+v\n", c.Shipping)
}
Output
Ana Lima ships to Lisbon 1100-053
{Street:12 Rua Augusta City:Lisbon Postcode:1100-053}

A composite literal that spans several lines needs a comma after the last element too — gofmt keeps it there.

Constructor functions

Go has no constructors. When a struct needs validation or defaults, write a function — by convention named New… — that builds and returns it:

GO
package main

import (
	"errors"
	"fmt"
	"strings"
)

type Account struct {
	Owner   string
	Balance int64 // cents
	Plan    string
}

func NewAccount(owner string) (*Account, error) {
	owner = strings.TrimSpace(owner)
	if owner == "" {
		return nil, errors.New("owner is required")
	}
	return &Account{Owner: owner, Plan: "basic"}, nil
}

func main() {
	acc, err := NewAccount("  Ben ")
	fmt.Printf("%+v %v\n", *acc, err)

	_, err = NewAccount("")
	fmt.Println(err)
}
Output
{Owner:Ben Balance:0 Plan:basic} <nil>
owner is required

Returning a pointer (&Account{…}) is common for structs that will be modified or are large. If the zero value of your struct is already useful, you may not need a constructor at all.

Anonymous structs

For one-off groupings — test cases, a JSON shape used in one place — you can declare a struct type inline without naming it:

GO
package main

import "fmt"

func main() {
	tests := []struct {
		input string
		want  int
	}{
		{"a", 1},
		{"hello", 5},
		{"", 0},
	}
	for _, tc := range tests {
		fmt.Println(tc.input, len(tc.input) == tc.want)
	}

	point := struct{ X, Y float64 }{1.5, -2}
	fmt.Println(point)
}
Output
a true
hello true
 true
{1.5 -2}

Struct tags

A field can carry a tag: a string of metadata that libraries read. The most common are JSON tags, which set the field's name in JSON (lesson 59):

GO
package main

import (
	"encoding/json"
	"fmt"
)

type Product struct {
	ID       int     `json:"id"`
	Name     string  `json:"name"`
	Price    float64 `json:"price"`
	Internal string  `json:"-"`                  // never encoded
	Discount float64 `json:"discount,omitempty"` // left out when zero
}

func main() {
	p := Product{ID: 7, Name: "Lamp", Price: 34.5, Internal: "secret"}
	data, _ := json.Marshal(p)
	fmt.Println(string(data))
}
Output
{"id":7,"name":"Lamp","price":34.5}

Exported and unexported fields

Field names follow the same rule as everything else in Go: a capital letter makes a field visible to other packages, a lowercase one keeps it private to its own package. Packages such as encoding/json can only see exported fields, which is why the fields above are capitalised.

Defining your own types

type can create a new type from any existing type, not just structs. The new type has the same underlying representation but is distinct, and you can add methods to it (lesson 38). A type alias, written with =, is just another name for the same type:

GO
package main

import "fmt"

type UserID int64    // a new, distinct type
type Celsius float64 // another one
type Tags = []string // an alias: exactly []string

func lookup(id UserID) string { return fmt.Sprint("user #", id) }

func main() {
	var id UserID = 42
	fmt.Println(lookup(id))

	var n int64 = 42
	// lookup(n) would not compile: int64 is not UserID
	fmt.Println(lookup(UserID(n)))

	var t Tags = []string{"a", "b"}
	var s []string = t // fine: Tags and []string are the same type
	fmt.Println(s, Celsius(21.5))
}
Output
user #42
user #42
[a b] 21.5

Try it yourself

Model a shopping cart line with a struct and calculate the cart total. Then add a Discount field to the struct.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
Notebook   4 x  3.50 =  14.00
Pen       10 x  1.20 =  12.00
Backpack   1 x 39.90 =  39.90
Total                     65.90
Next: pointers.