JavaRush /Courses /Go SELF /Struct embedding: promoted members

Struct embedding: promoted members

Go SELF
Level 23, Lesson 0
Available

1. Why struct embedding is useful

When you start modeling more “alive” data, you quickly want to compose types out of parts. For example, a task has a title and a status, but it also has “metadata” like ID, author, or version. In languages with inheritance, your hand reaches for Task extends Entity, but Go looks at that calmly and says: “Don’t complicate it; just put one inside the other.”

Embedding in Go is a way to embed one type inside another so that you can access its fields (and later methods) more concisely, as if they were “lifted” to the outside. This is still composition, meaning “there is a field inside,” just with convenient syntax. And yes, this is exactly where many people start accidentally designing weird APIs—so we’ll go slowly and use examples.

A quick aside, but useful for the bigger picture: in Go’s standard library, they sometimes add a field to a struct for API compatibility. For example, in net/http they made the context part of http.Request so they wouldn’t have to break client method signatures. This is a good example that “putting something into a struct” is a serious design decision, not just a technique.

Anonymous field: what it looks like and what it means

If a regular struct field is written as Name Type, then an embedded field is written “without a name”: just the type. That is, we are basically saying: “inside Task there is also a Meta.” The field name does not disappear; it is derived from the type name.

Let’s start right away with a micro-example, with as little magic as possible:


package main

import "fmt"

type Meta struct {
	ID int
}

type Task struct {
	Title string
	Meta // embedded field (anonymous field)
}

func main() {
	t := Task{Title: "Read Go spec", Meta: Meta{ID: 42}}
	fmt.Println(t.Meta.ID) // 42
}

It’s important not to mix this up: Meta is actually stored inside Task as a field. It’s just declared without an explicit name on the left. This means Task has a field, and that field’s name is Meta (because the type is called Meta).

2. Promoted fields: why t.ID compiles

The most noticeable “feature” of embedding is promoted members (in this lecture, we care specifically about fields). If Meta is embedded inside Task, and Meta has a field ID, then Go allows you to access it like this:

  • short: t.ID
  • explicit: t.Meta.ID

The short form is promotion: Go looks for a field with that name inside an embedded field and “promotes” it outward as a convenient access path.

Let’s show it directly in code:

package main

import "fmt"

type Meta struct {
	ID int
}

type Task struct {
	Title string
	Meta
}

func main() {
	t := Task{Title: "Write code", Meta: Meta{ID: 7}}
	fmt.Println(t.ID)      // 7
	fmt.Println(t.Meta.ID) // 7
}

It’s important to catch Go’s intonation here: t.ID is not a new field and not a copy. It’s just a shorthand for t.Meta.ID. The field is still stored inside Meta.

A useful analogy: imagine Task is a folder, and Meta is a subfolder. Promotion is like a shortcut on your desktop: the file lives in the subfolder, but you can reach it faster.

4. The embedded field name: where the name for *Meta comes from

Up to now, we embedded a value of type Meta. But in Go you can also embed a pointer, i.e. *Meta. Then a reasonable question comes up: “If the field has no name, what is it called? *Meta isn’t a name.”

The rule is simple: the embedded field name is taken from the type name without the *. That is:

  • if you write Meta, the field is named Meta
  • if you write *Meta, the field is still named Meta

Example:

package main

import "fmt"

type Meta struct {
	ID int
}

type Task struct {
	Title string
	*Meta // embedded pointer
}

func main() {
	t := Task{Title: "Pay bills", Meta: &Meta{ID: 100}}
	fmt.Println(t.Meta.ID) // 100
	fmt.Println(t.ID)      // 100 (promoted field)
}

As long as Meta is not nil, this looks very convenient: t.ID works, and you don’t have to constantly write t.Meta.ID.

But this is where it starts to smell like “be careful”: the zero value for *Meta is nil. That means you can create Task{Title: "X"} and get t.Meta == nil. And if you later access a promoted field, you can hit a runtime panic. In this lecture we only note the fact and learn to recognize such code by sight. We’ll talk more deeply about pointer risks and methods in the next lectures of the day.

5. Explicit qualification: when it’s better to write t.Meta.ID

When you write t.ID, the code gets shorter, but sometimes less obvious. Especially if the struct is large, or if it has many embedded fields. A reader (including you two weeks later) may ask: “Is ID a field of Task itself, or did it come from something embedded?”

So a useful habit is: if the field conceptually belongs to the “embedded part,” then in important places you can write it explicitly as t.Meta.ID. This isn’t “better” or “worse”; it’s about clarity.

Let’s make a small snippet from our training app (roughly, a CLI task tracker). We want to print a task nicely and explicitly show that ID is metadata:

package main

import "fmt"

type Meta struct {
	ID int
}

type Task struct {
	Title string
	Done  bool
	Meta
}

func main() {
	t := Task{Title: "Buy milk", Done: false, Meta: Meta{ID: 1}}
	fmt.Println(t.Meta.ID, t.Title, t.Done) // 1 Buy milk false
}

This style helps especially when you discuss the data model with a team or when you do code review: the origin of fields is visible immediately.

6. Initializing embedded fields: literals and zero value

The most common way to initialize an embedded field is with a keyed literal, where the key is the embedded field’s type name.

That is, for:

type Task struct {
	Title string
	Meta
}

initialization looks like this:

Task{Title: "...", Meta: Meta{...}}

Example:

package main

import "fmt"

type Meta struct {
	ID int
}

type Task struct {
	Title string
	Meta
}

func main() {
	t := Task{
		Title: "Learn embedding",
		Meta:  Meta{ID: 10},
	}
	fmt.Println(t.ID) // 10
}

Why do we insist so much on a keyed literal? Because a positional literal with embedded fields quickly becomes an eye-trap:

// Task{"Learn embedding", Meta{ID: 10}} // compiles, but is hard to read

When a struct has 3–5 fields, you can still guess the order. When it has 10 fields, the order turns into a “guess what’s where” quest. And the order is easy to break during a struct refactor.

Zero value is also important to understand: if Meta is embedded as a value (Meta), then even with var t Task you will have t.Meta as an “empty” struct, and t.ID will simply be 0. If Meta is embedded as a pointer (*Meta), then t.Meta will be nil, and that’s a different story.

7. Examples: embedding parts into models

Meta in a task model

Now we’ll do something very typical in real projects: we’ll extract “common fields” into a separate struct Meta and embed it into Task. This is not about optimization and not about beauty for beauty’s sake—it’s about building a data model out of understandable pieces.

Let’s start with the definition:

package main

type Meta struct {
	ID      int
	Version int
}

type Task struct {
	Title string
	Done  bool
	Meta
}

func main() {}

Now let’s create a task and see how promoted fields work:

package main

import "fmt"

type Meta struct {
	ID      int
	Version int
}

type Task struct {
	Title string
	Done  bool
	Meta
}

func main() {
	t := Task{Title: "Refactor model", Done: false, Meta: Meta{ID: 5, Version: 1}}
	fmt.Println(t.ID, t.Version) // 5 1
}

In practice this gives you two nice things. First, you group fields and give them a “home” (Meta). Second, you can write t.ID while still keeping the option to write t.Meta.ID when you need to emphasize the meaning.

Stats inside Project

So your brain doesn’t think embedding is only for ID, here’s another variant: “statistics” as a separate struct. In our app this could be a task project that has counters.

package main

import "fmt"

type Stats struct {
	Total int
	Done  int
}

type Project struct {
	Name string
	Stats
}

func main() {
	p := Project{Name: "home", Stats: Stats{Total: 12, Done: 3}}
	fmt.Println(p.Done, "/", p.Total) // 3 / 12
}

Notice: Project already has its own field Name, and at the same time the fields Total and Done have been “promoted” from Stats. For small models this is convenient. For large ones it can become dangerous, because suddenly the type has too many members accessible “from the outside.” We’ll go into that in detail later; for now, our goal is to read such code confidently.

8. Useful nuances of embedding

What actually lives in memory

Beginners sometimes get the impression that embedding is some kind of “compiler mode” that magically merges two types. In reality it’s simpler: a struct contains a struct. That’s it.

You can imagine Task like this:

Task
├── Title (string)
├── Done (bool)
└── Meta
    ├── ID (int)
    └── Version (int)

And promotion is just convenient access via a shorter path. That is, t.ID is as if the compiler substituted t.Meta.ID when it sees that ID does not exist directly in Task but does exist in an embedded field.

Embedding also exists in interfaces

So you won’t be surprised later: the term “embedded” in Go also appears in interfaces—when one interface includes another, combining method sets. It’s the same “build from parts” principle, just at the contract level. In docs and articles you’ll see the word embed used in this sense.

Today we keep the focus on structs: anonymous fields and promoted fields. Methods (and the subtleties around them) are the next step.

9. Typical mistakes when embedding structs

Mistake #1: treating embedding as inheritance and expecting “polymorphism like in OOP.”
Embedding is not “is-a” (is-a), it is “has-a” (has-a). A Task contains a Meta inside. Just because you can write t.ID doesn’t mean Task turns into Meta. If you catch yourself thinking “well this is like extends,” stop and say out loud: “it’s a field, just without an explicit name.”

Mistake #2: overusing the short access form and losing the meaning of where a field comes from.
When the code everywhere says t.ID, t.Version, t.Done, the reader may think all of these are fields of the task itself. And then suddenly it turns out that Version is actually metadata. In such places, explicit qualification like t.Meta.Version helps: the code becomes longer by a few characters, but shorter in time-to-understand.

Mistake #3: initializing structs positionally, especially with embedded fields.
Positional literals quickly turn into a “magic sausage of arguments.” Today the order is one way, tomorrow you add a field—and everything shifts. With embedded fields it’s even worse, because visually you don’t see where “task fields” end and the embedded part begins. Keyed literals (Task{Title: ..., Meta: Meta{...}}) look boring, but they won’t betray you on a Friday evening.

Mistake #4: embedding a pointer *T and forgetting that the zero value is nil.
Even if in this lecture we didn’t go deep into method behavior and panics, the basic fact remains: var t TaskWithPtr gives you t.Meta == nil, and that can blow up when you try to access promoted fields. If you see an embedded *T, keep this question in mind: “Where is initialization guaranteed?”, and look for the answer in the code (literal, constructor, check before access).

1
Task
Go SELF, level 23, lesson 0
Locked
Access Pass
Access Pass
1
Task
Go SELF, level 23, lesson 0
Locked
Draft note
Draft note
1
Task
Go SELF, level 23, lesson 0
Locked
Task without a passport
Task without a passport
1
Task
Go SELF, level 23, lesson 0
Locked
Updating document version
Updating document version
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION