Defining a struct
A struct is a named collection of fields. Each field has its own name and type, and together they make up one value of the struct's type. You use a struct whenever you have several pieces of related data that belong together.
If you have ever tracked a user's information in a real program, you have already felt the problem structs solve:
// Bad: four loose variables that have to travel together everywhere.
var userID int
var userName string
var userEmail string
var userActive bool
func saveUser(id int, name string, email string, active bool) { ... }
Every function that touches the user has to take four parameters. Every time you add a field (age, lastLogin, role), every signature changes. A struct fixes this by giving the whole bundle one name:
type User struct {
ID int
Name string
Email string
Active bool
}
func saveUser(u User) { ... }
Now User is one value that travels as a unit. Adding a field means updating the struct definition and the code that cares about the new field; everything else keeps working unchanged.
Declaring a struct type
The syntax is type Name struct { ... }, with one field per line inside the braces:
type Point struct {
X int
Y int
}
type Point struct { ... } creates a new type called Point. X int and Y int are its fields: names paired with types, same as function parameters. When the field types match, you can group them:
type Point struct {
X, Y int
}
Both declarations produce the same type. The grouped form is shorter when you have several fields of the same type and is the style you will see most often.
Accessing fields
You reach into a struct with the dot operator, .:
var p Point
p.X = 10
p.Y = 20
fmt.Println(p.X, p.Y) // 10 20
fmt.Println(p) // {10 20}
p.X reads or writes the X field. fmt.Println(p) prints the struct itself, using the default format {field1 field2 ...} (space-separated, in declaration order).
Zero values
The zero value of a struct is a struct with every field at its own zero value. For a Point, that is X: 0, Y: 0. You get it automatically with a plain var:
var p Point
fmt.Println(p) // {0 0}
Many Go types are designed so their zero value is immediately useful. bytes.Buffer, sync.Mutex, and strings.Builder all work the moment you declare them, without an explicit initialisation step. When you design your own structs, aim for the same property where you can: it makes the type pleasant to use.
Exported versus unexported fields
The capitalisation rule you met in the Packages lesson applies to struct fields too:
type User struct {
ID int // exported: visible to other packages
Name string // exported
email string // unexported: visible only inside this package
}
ID and Name are exported (uppercase first letter). Any package that imports yours can read and write them directly. email is unexported (lowercase). Only code in the same package can touch it. This is how Go controls access to struct internals: by the name of the field, not with a separate public or private keyword.
Struct tags (awareness)
Every field in a struct can carry a tag: a string in backticks after the type, used by libraries like encoding/json to control how the field is serialised. You will see them everywhere in real Go code:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
Tags do not change how the struct works inside your own program; they are metadata a library reads at runtime via reflection. json:"name" tells encoding/json to use "name" as the JSON key for the Name field, omitempty tells it to skip the field when the value is zero, and so on. The full treatment lives in the Standard library chapter (encoding/json) and the Idioms chapter (struct tags in practice). For now, knowing that the backtick annotation is called a struct tag and that its meaning depends on whoever is reading it is enough.
The empty struct struct{}
Go has a type called struct{}: a struct with no fields at all. A value of that type occupies zero bytes of memory. It sounds useless, but it is an idiom in two specific places.
The first is as the value type of a set. Go has no built-in set type; the standard pattern is a map whose values you do not care about, and struct{} is the smallest possible "do not care" value:
seen := map[string]struct{}{}
seen["alice"] = struct{}{}
seen["bob"] = struct{}{}
if _, ok := seen["alice"]; ok {
fmt.Println("alice was seen")
}
The map's keys are the set's contents; the values are placeholders that take up no memory per entry. map[string]bool also works but stores a byte per entry and invites confusion between "present but false" and "absent". map[K]struct{} is the unambiguous form.
The second place is as a signalling channel element, covered in the Concurrency chapter. chan struct{} is used to notify one goroutine that something happened, with no data attached; the empty struct is exactly "nothing" by value.
You rarely declare struct{} outside these two idioms. When you see it in the wild, it is almost always one of them.
Generic structs (forward-pointer)
Structs can also take type parameters, the same way functions can (Stack[T any], LinkedList[T any], Map[K comparable, V any]). Generics have their own chapter later in the course; the point to notice here is that every rule in this chapter (literals, pointer vs value, embedding, comparability) carries over unchanged to the generic case. A Stack[int] is still a struct.
When you sit down to write a Go program, the first question is usually "what are the things this program talks about?" Users, orders, requests, articles, transactions: whatever the program's subject is, each one becomes a struct type. Functions then operate on those structs, and packages group related structs and functions together. You will see this pattern in the standard library (http.Request, os.FileInfo, time.Time), in third-party libraries, and in every production Go codebase. Get into the habit of asking "does this need to be its own struct?" as soon as you notice two or three loose variables moving together.
The starter declares nothing. In main:
- Define a struct type
Bookwith three fields:Title(string),Author(string), andPages(int). - Declare a variable of type
Bookwithout initialising it. Print it. You should see{ 0}(both string fields empty, pages 0). - Assign
Title,Author, andPagesthrough the dot operator. - Print the struct again.
Expected output:
{ 0}
{The Go Programming Language Alan A. A. Donovan 380}
(Between "The Go Programming Language" and "Alan A. A. Donovan" there is one space, because fmt prints struct fields space-separated. The output has no line break between the title, author, and page count; it is one {...} line.)
{ 0}
{The Go Programming Language Alan A. A. Donovan 380}