---
title: Structs
source: 'https://academia.sh/en/courses/go-fundamentals/structs'
course: 'Go Fundamentals'
language: en
updated: '2026-09-13T05:08:44+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Reads unwritten fields' zero value and compares positional notation's fragility with keyed notation"
    - "Measures that a struct's comparability depends on its fields' comparability"
    - 'Shows that embedding carries unwritten names to the outer type and that the receiver is the embedded value'
---

# Structs

The zero value of unwritten fields, the fragility carried by fields being writable positionally, and embedding carrying unwritten names to the outer type are measured: an embedded type's methods can be called without ever being written on the outer type. The limiting measure shows that embedding is not inheritance: when an embedded type's method is called, the receiver is the embedded value itself, not the outer struct.

A map's key was an indivisible value: a single string or integer, carrying a single piece of
information. A struct is a composite type that holds several fields together, with each field
accessed by its name. It shares one question with a map — what happens if fields are not
given a value — but a struct has two more questions of its own: does the fields' **order**
carry meaning, and what happens when a struct is **embedded** inside another struct?

These two questions sit on an axis separate from the "copy or sharing" question established
in the array and slice lessons. The Arrays and Slices lesson measured how much of a value gets
copied; a struct is also a value and follows the same rule. But a struct's own question is not
about copying, it is about the **naming** of fields: a field's name, its position, and its
source — whether it was written — carry three separate pieces of information, independent of
each other.

## The Zero Value of Unwritten Fields

When a struct variable is declared without an initial value, each of its fields takes its own
type's zero value — a string field gets an empty string, an integer field gets zero, a boolean
field gets `false`. This is one of the cases falling into the **defined** class of the Zero
Values lesson's binding 8/4/3 distribution; it is mentioned here in a single sentence, the
whole distribution is not rebuilt.

```go
// zero.go — the zero value of unwritten struct fields
package main

import "fmt"

type Record struct {
	Name   string
	Number int
	Active bool
}

func main() {
	var empty Record
	fmt.Printf("empty record: %q %d %t\n", empty.Name, empty.Number, empty.Active)
}
```

```
empty record: "" 0 false
```

The three fields carry the zero value of three separate types, and all three come from a
single line, the notation `var empty Record`. No field was given a value explicitly; because
the struct itself knows which field is which type, it fills in each one's zero value itself.
This means a struct variable does not need a separate constructor call to be built —
`var empty Record` on its own produces a usable value, leaving no field in a "not yet built"
state.

This filling-in also works for embedded types: if a struct embeds another struct, the embedded
field itself also takes its own zero value.

```go
// embedded-zero.go — an embedded type's zero value is also filled in automatically
package main

import "fmt"

type Base struct {
	Name string
}

type Extended struct {
	Base
	Password string
}

func main() {
	var g Extended
	fmt.Printf("empty extended: Name=%q Password=%q\n", g.Name, g.Password)
}
```

```
empty extended: Name="" Password=""
```

`g.Name` here is `Base`'s field, `g.Password` is `Extended`'s own field; both came from a
single line, `var g Extended`. Zero-value filling holds for nested structs too, because the
rule is the same at every level: every field takes its own type's zero value, even when the
field itself is another struct.

A struct is also a value, just like an array, and is copied entirely when assigned — every
struct example seen throughout this lesson stands under this rule. After `copy := source` is
written, a change made to `copy` does not affect `source` at all, because the two variables
now carry two independent copies. This behavior will gain its real importance in the Pointers
lesson: a function that wants to change a struct cannot change anything at the caller when it
is given a copy.

## The Fragility of Fields Being Writable Positionally

A struct literal can be written in two forms: without writing field names, purely by **order**
(positional), or by matching each value to its own field name (keyed). Positional notation is
short, but it rests on an assumption — the assumption that the field arrangement at the time
it was written will stay the same as the struct's **current** definition. When this assumption
breaks, the compiler gives no warning at all, as long as the two fields are the same type.

```go
// ordering.go — positional notation depends on field order, keyed notation does not
package main

import "fmt"

type AddressV1 struct {
	City    string
	Country string
}

type AddressV2 struct { // field order changed: Country moved to the front
	Country string
	City    string
}

func main() {
	v1 := AddressV1{"Ankara", "Turkey"}
	v2 := AddressV2{"Ankara", "Turkey"} // code was not updated, values written for V1
	fmt.Println("V1 positional:", "City=", v1.City, " Country=", v1.Country)
	fmt.Println("V2 positional:", "Country=", v2.Country, " City=", v2.City)
	v2Correct := AddressV2{City: "Ankara", Country: "Turkey"}
	fmt.Println("V2 keyed     :", "Country=", v2Correct.Country, " City=", v2Correct.City)
}
```

```
V1 positional: City= Ankara  Country= Turkey
V2 positional: Country= Ankara  City= Turkey
V2 keyed     : Country= Turkey  City= Ankara
```

`AddressV2` carries the same two fields as `AddressV1`, only its order has changed: `Country`
has been moved to the front. The line `AddressV2{"Ankara", "Turkey"}` was left **completely
unupdated** — it still assumes the old order (city first, then country). The compiler rejects
nothing here, because both fields are of type `string`; positional notation only looks at
position, it never asks for the name. The result: `Ankara` is now written into the `Country`
field, `Turkey` into the `City` field — the values have swapped places. Keyed notation puts
the same two values into the correct field no matter what order they are written in;
`v2Correct` is entirely independent of field order.

This fragility becomes visible only when field order **changes**, and the compiler never
catches it as long as the two fields are the same type — none of the four rejected cases
(unused variable, unused import, an unwritten return, implicit type conversion) covers this
situation. Had the fields' types been **different**, positional notation would not compile
and the error would appear at compile time; the type staying the same postpones the error to
run time, or even to never being noticed at all.

**CT11.** — `AddressV1` and `AddressV2` are deliberately built here with two fields, both of
the same type; the goal is to show the narrowest case the compiler cannot catch. Had a third
field of a different type been added, positional notation would not compile on that field, and
the fragility would not be visible.

This situation arises in a real codebase when a struct's field order is changed **later**:
the struct's definition is updated, but every line that builds it with positional notation is
not updated automatically. Because the compiler gives no warning, this becomes a defect class
that is hard to notice without running the code and checking the result by eye — keyed
notation removes this class entirely, because which value goes into which field stands in the
notation itself, entirely independent of field order.

## Comparability Is Inherited From the Fields

The Arrays and Slices lesson measured that an array can be compared with `==` and a slice
cannot, and left it in a single sentence that this would carry over to structs too. Whether a
struct is comparable is not a rule of its own, it is the sum of its **fields'** comparability.

**CT12.** — This measurement is run here with a compiler call, and only the class "compiled"
or "did not compile" is printed; both type definitions carry a single field that looks the
same, and only that field's type determines the difference.

```go
// comparison.go — a struct's comparability is inherited from its fields
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

func compiles(source string) bool {
	dir, err := os.MkdirTemp("", "trial")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

func main() {
	arrayField := `package trial
	type K struct { Labels [2]string }
	func F() bool {
		a := K{[2]string{"x", "y"}}
		b := K{[2]string{"x", "y"}}
		return a == b
	}`
	sliceField := `package trial
	type K struct { Labels []string }
	func F() bool {
		a := K{[]string{"x", "y"}}
		b := K{[]string{"x", "y"}}
		return a == b
	}`
	fmt.Println("array-field-only struct:", compiles(arrayField))
	fmt.Println("slice-field struct     :", compiles(sliceField))
}
```

```
array-field-only struct: true
slice-field struct     : false
```

Both structs carry a single field; both appear to hold the same information (two labels). The
difference is in the field's type: one is an array, the other a slice. The array-field struct
can be compared with `==` because the array itself is comparable; the slice-field struct
cannot be compared because a slice is not comparable, and this property passes to the struct
**exactly as is**. A struct does not define a comparison rule of its own — it inherits its
fields' rule.

## Embedding: Unwritten Names Carried to the Outer Type

A struct can contain, instead of a named field, a field that carries only another type's
name; this is called **embedding**. The embedded type's fields and methods become directly
callable through the outer type, without ever being written on the outer type. This lets
several behaviors be carried to the outer type at once with a single field; in the measurement
below, `Base`'s two methods appear on the `Extended` side without ever being written there.

```go
// embedding.go — an embedded type's names are carried to the outer type without being written
package main

import "fmt"

type Base struct {
	Name string
}

func (t Base) Greet() string {
	return "Hello, " + t.Name
}

func (t Base) WhoIsSpeaking() string {
	return fmt.Sprintf("%T says: %s", t, t.Name)
}

type Extended struct {
	Base
	Password string
}

func (g Extended) Verify(s string) bool {
	return g.Password == s
}

func main() {
	g := Extended{Base: Base{Name: "Alice"}, Password: "secret"}
	fmt.Println("embedded field direct  :", g.Name)
	fmt.Println("embedded method direct :", g.Greet())
	fmt.Println("own method             :", g.Verify("secret"))
}
```

```
embedded field direct  : Alice
embedded method direct : Hello, Alice
own method             : true
```

In the `Extended` struct there is no field written as `Name`, no method written as `Greet`
either — both belong to `Base`. But `g.Name` and `g.Greet()` work, as if `Extended` had
defined both itself. `Base`'s two methods (`Greet`, `WhoIsSpeaking`) were carried to `Extended`
**without being written**; the one method `Extended` writes for itself is `Verify`. An
embedded type's names appear as though they were the embedding type's own names — unlike a
key in a map, access here happens not by name, but directly by **field selection**.

**CT13.** — The number of methods carried over is two here (`Greet`, `WhoIsSpeaking`); the
number depends on how many methods were written on `Base`, and is independent of how many
methods the embedding type writes itself. Even if `Extended` wrote zero methods, both would
still be visible.

Embedding also works with a struct embedding **more than one** type, and the carried names
stack up; if two embedded types have a field or method with the same name, this lesson does
not test that collision, but the compiler rejects direct access from the outer type in such a
collision — which embedded type is meant has to be written explicitly. This lesson measures
only a **single** level of embedding; one embedded type whose name and methods do not collide.

## Limiting Measure: Embedding Is Not Inheritance

Embedding resembles inheritance at the level of call notation, but it is not the same thing.
The difference lies in **who the receiver is** inside the called method.

```go
// receiver.go — when an embedded method is called, the receiver is the embedded value itself
package main

import "fmt"

type Base struct {
	Name string
}

func (t Base) WhoIsSpeaking() string {
	return fmt.Sprintf("%T says: %s", t, t.Name)
}

type Extended struct {
	Base
	Password string
}

func main() {
	g := Extended{Base: Base{Name: "Alice"}, Password: "secret"}
	fmt.Println("method naming the receiver:", g.WhoIsSpeaking())
}
```

```
method naming the receiver: main.Base says: Alice
```

The call `g.WhoIsSpeaking()` was made through `Extended`, but the type printed inside the
method with `%T` is `main.Base` — not `main.Extended`. This proves that the receiver is not
`g` itself, it is the embedded `Base` value inside `g`. In languages with inheritance, a
superclass method called through a subclass can see the **full identity** of the object
calling it; in Go, an embedded method can never step outside its own type — none of `Base`'s
methods know that `Extended` exists, because at the moment `Base` was defined, a type called
`Extended` did not exist yet. Embedding is the compiler **automatically expanding** the
notation `g.WhoIsSpeaking()` into `g.Base.WhoIsSpeaking()`; it is not a new call form, it is a
shortened notation for an existing call.

This becomes even more evident when the outer type defines a field with the **same name** as
the embedded type.

```go
// shadow.go — the outer type's same-named field shadows the embedded one, but the method is unaffected
package main

import "fmt"

type Base struct {
	Name string
}

func (t Base) Greet() string {
	return "Base says: Hello " + t.Name
}

type Extended struct {
	Base
	Name string // same-named field on the outer type, shadows the embedded one
}

func main() {
	g := Extended{Base: Base{Name: "EmbeddedName"}, Name: "OuterName"}
	fmt.Println("g.Name (direct)       :", g.Name)
	fmt.Println("g.Base.Name (explicit):", g.Base.Name)
	fmt.Println("g.Greet() (embedded)  :", g.Greet())
}
```

```
g.Name (direct)       : OuterName
g.Base.Name (explicit): EmbeddedName
g.Greet() (embedded)  : Base says: Hello EmbeddedName
```

When `Extended` defines its own `Name` field, that field **shadows** the embedded `Base.Name`:
`g.Name` now goes to the outer type's own field (`"OuterName"`); reaching the embedded field
requires writing `g.Base.Name` explicitly. But the call `g.Greet()` still uses
`"EmbeddedName"` — because `Greet` belongs to `Base`, and `Base`'s own `Name` was never
shadowed at all, only its **visibility** from the outer type changed. This is further proof
that the receiver is the embedded value itself: the outer type reusing the same name changes
nothing inside the embedded type itself.

## Summary

- An unwritten struct field takes its own type's zero value; this is an example that falls
  into the defined class of the Zero Values lesson's binding distribution.
- Positional struct literals depend on field order; if two fields are the same type, the
  compiler catches nothing when the order changes, and values silently swap places. Keyed
  notation does not carry this fragility.
- A struct's comparability with `==` is not a rule of its own; it is the sum of every field's
  comparability — a single slice field makes the struct non-comparable.
- Embedding carries the embedded type's fields and methods to the outer type without being
  written; access happens directly by field selection.
- When an embedded method is called, the receiver is the embedded value itself, not the outer
  struct; embedding is not inheritance, it is a shorthand the compiler expands automatically.

## Next Step

In this lesson, the notation `Base{Name: "Alice"}` was a value, and access to `g.Name`
happened directly through that value. But a method that wants to change a struct, if it
operates on its own copy, never affects the caller — just like an array being copied when
passed to a function. The next lesson looks at this problem: why is it not possible to change
a struct without taking a variable's **address**, and once the address is taken, which calls
affect the caller?
