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
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) }
{Dune Frank Herbert 412 false}
{Title:Emma Author:Jane Austen Pages:474 OnLoan:true}
{Title: Author: Pages:0 OnLoan:false}
Untitled 886 Frank HerbertPrefer 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):
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) }
{1 2} {99 2}
true falseStructs whose fields are all comparable can be compared with ==, field by field, and used as map keys.
Nested structs
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) }
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:
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) }
{Owner:Ben Balance:0 Plan:basic} <nil>
owner is requiredReturning 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:
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) }
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):
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)) }
{"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:
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)) }
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.
Notebook 4 x 3.50 = 14.00 Pen 10 x 1.20 = 12.00 Backpack 1 x 39.90 = 39.90 Total 65.90