Skip to content

Structs

A struct is a typed collection of named fields glued together into one value. It is Go's primary tool for modelling "a thing with several attributes" — a point, a user, an HTTP request. There is no class, no inheritance, no constructor keyword: a struct is just data laid out in memory, and you build behaviour around it separately (with functions and methods).

You almost always give a struct a name using the type keyword from custom types:

type Point struct {
    X int
    Y int
}

That declares a new type Point whose values carry two int fields, X and Y. Fields of the same type can share a line:

type Point struct {
    X, Y int
}

Creating struct values

There are several ways to make a Point, and the difference matters.

Keyed literal — name the fields. This is the form you should reach for almost always: it is order-independent and survives someone adding a new field later.

p := Point{X: 1, Y: 2}
fmt.Println(p)        // output: {1 2}

Positional literal — values in field-declaration order, no names. Fragile: it breaks the moment the struct gains or reorders a field, and it requires a value for every field.

p := Point{1, 2}      // ok, but tied to declaration order

Zero value — declare without initialising and every field gets its type's zero value (0, "", nil, false, …). A struct has no separate "uninitialised" state; the zero struct is a complete, usable value.

var p Point
fmt.Println(p)        // output: {0 0}

You can omit fields in a keyed literal; the ones you leave out take their zero value:

p := Point{Y: 5}
fmt.Println(p)        // output: {0 5}

From Python: there is no __init__. The zero value is your default constructor. When zero isn't a sensible default, the convention is a plain function named NewPoint(...) Point — a regular function, not special syntax.

Reading and writing fields

Dot notation, and fields are addressable, so you can assign to them directly:

p := Point{X: 1, Y: 2}
p.X = 10
fmt.Println(p.X + p.Y)   // output: 12

Structs are value types

Assigning a struct, passing it to a function, or returning it copies every field. The copy is independent of the original.

a := Point{X: 1, Y: 2}
b := a            // full copy
b.X = 99
fmt.Println(a.X)  // output: 1  — a is untouched

This is the single most important thing to internalise. If you want a function to mutate the caller's struct, pass a pointer:

func moveRight(p *Point) {
    p.X++         // p.X is shorthand for (*p).X — Go auto-dereferences
}

a := Point{X: 1, Y: 2}
moveRight(&a)
fmt.Println(a.X)  // output: 2

Note p.X on a *Point: Go automatically dereferences a struct pointer for field access, so you never write (*p).X. See pointers for the underlying rule.

Comparing structs

A struct is comparable with == if all of its fields are comparable. The comparison is field-by-field.

p := Point{1, 2}
q := Point{1, 2}
fmt.Println(p == q)   // output: true

This also makes comparable structs usable as map keys. But if a struct contains a non-comparable field — a slice, a map, or a function — the whole struct becomes non-comparable and == is a compile error:

type Bag struct {
    items []int
}
b1 := Bag{}
b2 := Bag{}
_ = b1 == b2          // compile error: struct containing []int cannot be compared

Nested and embedded structs

A field can itself be a struct:

type Line struct {
    Start Point
    End   Point
}

l := Line{
    Start: Point{0, 0},
    End:   Point{3, 4},
}
fmt.Println(l.End.Y)   // output: 4

If you declare a field with no name — just a type — that field is embedded, and its fields are promoted so you can reach them directly:

type Circle struct {
    Point      // embedded: no field name, just the type
    Radius int
}

c := Circle{Point: Point{X: 1, Y: 2}, Radius: 5}
fmt.Println(c.X)        // output: 1  — promoted from the embedded Point
fmt.Println(c.Point.Y)  // output: 2  — the explicit path still works

A key in a composite literal may be any valid field selector for the struct type, not just a top-level field name. Promoted fields therefore work as keys directly, so you can fill an embedded struct without naming it:

type Base struct{ ID int }
type Meta struct{ Tag string }

type User struct {
    Base
    Meta
    Name string
}

u := User{ID: 7, Tag: "admin", Name: "ada"}
fmt.Printf("%+v\n", u)
// output: {Base:{ID:7} Meta:{Tag:admin} Name:ada}

Promotion reaches through as many levels as it takes, so a field embedded two deep is still a usable key:

type Inner struct{ Deep int }
type Mid struct{ Inner }
type Outer struct {
    Mid
    Name string
}

o := Outer{Deep: 5, Name: "x"}
fmt.Println(o.Deep)   // output: 5

Two rules keep this unambiguous. You may not set an embedded field and one of its promoted fields in the same literal — the two would fight over the same memory:

o := Outer{Mid: Mid{}, Deep: 2}
// compile error: cannot specify promoted field Deep and enclosing embedded field Mid

And a name promoted from two embedded types at the same depth is not a valid selector at all, so it is not a valid key either:

type A struct{ X int }
type B struct{ X int }
type C struct {
    A
    B
}

c := C{X: 1}   // compile error: unknown field X in struct literal of type C

Embedding is Go's composition mechanism — it stands in for the data side of what other languages do with inheritance. The method side of embedding (method promotion) is covered in methods.

Anonymous structs

You can create a struct value without ever declaring a named type. Handy for a one-off grouping — a table-test row, a quick JSON shape — where a top-level type would be noise.

config := struct {
    Host string
    Port int
}{
    Host: "localhost",
    Port: 8080,
}
fmt.Println(config.Host, config.Port)   // output: localhost 8080

Struct tags

Each field may carry a tag: a raw string literal after the type. Tags are metadata — the compiler ignores them, but libraries read them at runtime via reflection. The canonical use is controlling how encoding/json names fields:

type User struct {
    Name  string `json:"name"`
    Email string `json:"email,omitempty"`
}

u := User{Name: "Ada"}
b, _ := json.Marshal(u)
fmt.Println(string(b))   // output: {"name":"Ada"}

Here Email is dropped because of omitempty and its zero (empty) value. Without tags the keys would be "Name" and "Email" — the Go field names. Tags are conventionally backtick-quoted key:"value" pairs; multiple keys are space-separated.

A tag is just a string

Nothing about a tag is special to the compiler. It is one string stored alongside the field, and any package can ask it for a key:

type Item struct {
    Name string `json:"name" xml:"item-name"`
}

f, _ := reflect.TypeOf(Item{}).FieldByName("Name")
fmt.Printf("%q %q %q\n", f.Tag.Get("json"), f.Tag.Get("xml"), f.Tag.Get("nope"))
// output: "name" "item-name" ""

An unknown key comes back empty, so two libraries can read the same field without knowing about each other. encoding/json reads json:, encoding/xml reads xml:, and neither notices the other:

j, _ := json.Marshal(Item{Name: "bolt"})
fmt.Println(string(j))   // output: {"name":"bolt"}

x, _ := xml.Marshal(Item{Name: "bolt"})
fmt.Println(string(x))   // output: <Item><item-name>bolt</item-name></Item>

The flip side is that a misspelled tag is silent. jsom:"name" compiles happily, and encoding/json just falls back to the field name:

type Typo struct {
    Name string `jsom:"name"`
}

b, _ := json.Marshal(Typo{Name: "bolt"})
fmt.Println(string(b))   // output: {"Name":"bolt"}

- skips a field, and tags survive embedding

A tag value of - tells the reading library to leave the field out altogether. That is different from omitempty, which drops the field only when it holds its zero value. Unexported fields are never marshalled at all, tag or no tag. And when a struct is embedded, its fields are promoted with their tags attached:

type Point struct {
    X int `json:"x"`
    Y int `json:"y"`
}

type Shape struct {
    Point                     // X and Y are promoted, tags and all
    Label  string `json:"label"`
    secret string             // unexported: never marshalled
    Cache  []byte `json:"-"`  // exported, but explicitly skipped
}

s := Shape{Point: Point{X: 1, Y: 2}, Label: "corner"}
b, _ := json.Marshal(s)
fmt.Println(string(b))   // output: {"x":1,"y":2,"label":"corner"}

x and y land at the top level of the output rather than nested under a Point key — promotion is as flat here as it is for field access.

From Python: no decorator is doing this. A tag is inert data, and the library you hand the struct to is what gives it meaning — which is why one struct can describe its JSON shape and its XML shape at once.

The empty struct struct{}

A struct with no fields occupies zero bytes. It carries no data — it is used purely as a signal. The two common uses are a set (a map whose values you don't care about) and a channel that signals "an event happened" without sending a payload:

seen := map[string]struct{}{}
seen["go"] = struct{}{}
_, ok := seen["go"]
fmt.Println(ok)          // output: true

struct{}{} reads oddly at first: the inner struct{} is the type (empty struct), the outer {} is the literal (a value of that type).

Quick reference

Form Meaning
type T struct { X, Y int } declare a named struct type
T{X: 1, Y: 2} keyed literal (preferred)
T{1, 2} positional literal (order-bound)
var t T zero value — all fields zeroed
t.X field access (auto-derefs through a *T)
a == b field-by-field, only if all fields comparable
embedded field (type, no name) promotes the inner fields
`json:"name"` field tag, read by libraries via reflection
struct{}{} the zero-byte empty struct value

Sources