Dev Skills

Structs

How you make your own types by grouping values under one name.

Topic 15 of 98 · 5 min to read · 45 min to build
After this topic you can
  • Write a struct type and create values of it with named fields.
  • Predict the output after copying a struct and changing the copy.
  • Write a function that changes a struct through a pointer.

What a struct is

A struct is a type you define yourself by grouping other values together under one name. A user has an id, a name and an email; rather than passing three separate variables around, you define a User type that holds all three, and pass one thing.

Structs are how you model whatever your program is about. Go has no classes, so this is the main tool: a struct for the data, and methods (functions attached to that type) for the things it can do.

A struct groups fields

struct.go
type User struct {
ID int
Name string
Email string
}
u := User{ID: 1, Name: "sahdev", Email: "s@x.com"} // always name the fields
fmt.Println(u.Name)
You can write `User{1, "sahdev", "s@x.com"}`, but do not. Adding a field later breaks every positional literal, and swapping two fields of the same type (say `Name` and `Email`) silently swaps their values.

Printing a struct

print.go
u := User{ID: 1, Name: "sahdev", Email: "s@x.com"}
fmt.Printf("%v\n", u) // {1 sahdev s@x.com} — just the values
fmt.Printf("%+v\n", u) // {ID:1 Name:sahdev Email:s@x.com} — with field names
fmt.Printf("%#v\n", u) // main.User{ID:1, Name:"sahdev", Email:"s@x.com"}
fmt.Printf("%T\n", u) // main.User
`%+v` is the one you will use most when debugging: it shows which value belongs to which field.

Assignment copies. Always.

Go has no pass-by-reference. Assigning a struct copies it, and passing one to a function copies it too. When you pass a pointer, you are passing the pointer *by value*. The copy points at the same memory.

copy.go
a := User{Name: "first"}
b := a // a full copy — two independent structs
b.Name = "second"
fmt.Println(a.Name) // still "first"
p := &a // p is a pointer to a
p.Name = "changed" // Go dereferences automatically — no (*p).Name needed
fmt.Println(a.Name) // "changed"

Functions that take a struct

To work with a struct, write an ordinary function that takes it. Take a pointer (*User, from Pointers) when the function must change the original, because a plain User parameter is a copy.

funcs.go
func display(u User) string { return u.Name + " <" + u.Email + ">" }
func rename(u *User, n string) { u.Name = n } // pointer: changes the original
u := User{Name: "old", Email: "o@x.com"}
rename(&u, "new")
fmt.Println(display(u)) // new <o@x.com>
In **Methods and embedding** you will attach these functions to the type itself, so you can write `u.Rename("new")`.

Pointers to structs

This builds on Pointers. &User{...} makes a struct and gives you its address in one step; it is the usual way to get a *User. Go also dereferences struct fields for you: with p := &u, you write p.Name, and Go reads it as (*p).Name.

ptr-struct.go
p := &User{Name: "sahdev"} // a new User, and its address
fmt.Println(p.Name) // Go rewrites this as (*p).Name
p.Name = "changed" // and this too

A nil-able pointer field

A pointer field can be nil, so it tells "not sent" apart from "sent as empty". A PATCH endpoint needs that: {"title": ""} should clear the title, while {} should leave it alone. JSON shows how the tags work.

optional.go
type UpdateRequest struct {
Title *string `json:"title"` // nil = not provided at all
Body *string `json:"body"`
}
if req.Title != nil {
note.Title = *req.Title // they DID send it, even if it is ""
}

Embedding: a struct inside a struct

You can put one struct inside another with no field name. Its fields are then promoted, so they look like they belong to the outer type.

embed.go
type Base struct {
ID int
}
type Admin struct {
Base // embedded — no field name
Level int
}
a := Admin{Base: Base{ID: 1}, Level: 5}
fmt.Println(a.ID) // promoted field — same as a.Base.ID
This is composition, not inheritance. **Methods and embedding** shows that methods get promoted too.

Try it yourself

press Run — this executes on the Go Playground
package main
import "fmt"
type Base struct{ ID int }
type User struct {
Base // embedded: ID is promoted
Name string
}
func display(u User) string { return u.Name }
func rename(u *User, n string) { u.Name = n } // pointer: can mutate
func main() {
u := User{Base: Base{ID: 1}, Name: "first"}
fmt.Printf("%+v\n", u)
fmt.Println(display(u), u.ID)
rename(&u, "second")
fmt.Println("after rename:", u.Name)
copyOfU := u
rename(&copyOfU, "third")
fmt.Println("original is still:", u.Name) // assignment copied it
}
A copy and a pointer side by side, plus embedding.
The short version
  • A struct groups fields into one type. Always use field names in a literal.
  • Assignment and function calls copy a struct. Go has no pass-by-reference.
  • Taking a pointer (*User) lets a function change the original instead of a copy. &User{...} makes one, and p.Name dereferences for you.
  • A nil-able pointer field (*string) separates "not sent" from "sent as empty".
  • Embedding puts one struct inside another; its fields are promoted. Composition, not inheritance.
Remember: Everything in Go is a value and every assignment is a copy.
Build this before moving on
Build a Book struct (Title, Author, Pages), write func describe(b Book) string and func addPages(b *Book, n int), and print the book with %+v before and after.
Coding challenge · checked by tests
A Book has a Title and Pages. Write Describe(b Book) string, which returns the title and page count like "Go 101 (250 pages)", and AddPages(b *Book, n int), which adds n to the ORIGINAL book's pages. Example: a book with 250 pages after AddPages(&b, 10) has 260.

Tab indents. Press Esc, then Tab, to leave the editor.

Then try these in your own editor
  1. 1Copy a struct, change the copy, and print the original.
  2. 2Write rename(u User, n string) without a pointer, call it, and print the name. Then switch to *User.
  3. 3Embed a Base{ID int} in two different structs and print x.ID on both.
Check yourselfTake the Core language test