Structs and Interfaces
Ask a programmer coming from Java, C++, or Python what they miss most
when they first pick up Go, and the honest ones will usually say the same
thing: classes. No class keyword, no extends, no inheritance chain to
climb when you're trying to figure out where a method actually lives.
It feels like a gap at first. It isn't one — it's a deliberate bet that
two smaller, independent tools, used together, beat one big fused one.
This chapter is about those two tools.
A struct is a labeled drawer set — each drawer holds one named piece of data. An interface, by contrast, describes no drawers at all: it is a contract that says "anything handed to me must know how to do these specific things," with no opinion on what it's made of inside.
Go has no classes and no inheritance keyword. Instead, it builds object-like behavior from two independent, composable pieces: structs, which group related data, and interfaces, which describe behavior without naming a concrete type. This chapter covers both, plus a brief, practical look at generics — a newer tool for the cases where an interface isn't quite the right fit.
Defining and Using Structs
A struct groups named fields under one type:
type Point struct {
X, Y int
}
Values are created with a struct literal, ideally using field names for clarity:
p := Point{X: 3, Y: 4}
fmt.Println(p.X, p.Y) // 3 4
Fields can also be set positionally (Point{3, 4}), but that form breaks
silently if the struct's field order ever changes — naming the fields is
the safer default, especially for structs with more than two fields.
Struct values are compared with == when every field is itself
comparable, and are copied by value on assignment or when passed to a
function — exactly like the basic types from the previous chapter:
a := Point{1, 2}
b := Point{1, 2}
fmt.Println(a == b) // true
c := a
c.X = 99
fmt.Println(a.X, c.X) // 1 99 — c is an independent copy
== at all — the compiler rejects it at compile time with "invalid operation: struct containing []int cannot be compared." Reach for reflect.DeepEqual or a custom Equal method when a struct has such a field and you still need to compare instances.Struct Embedding
Go achieves code reuse between structs through embedding — including one struct (or interface) inside another with no field name, which promotes its fields and methods to the outer type:
type Animal struct {
Name string
}
func (a Animal) Describe() string {
return "an animal named " + a.Name
}
type Dog struct {
Animal
Breed string
}
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
fmt.Println(d.Name) // Rex — promoted field
fmt.Println(d.Describe()) // an animal named Rex — promoted method
Dog did not inherit from Animal in the class-based sense — it simply
holds an Animal value as an anonymous field, and Go's field/method
lookup rules automatically forward d.Name and d.Describe() to the
embedded value when Dog itself has no field or method of that name.
Dog can define its own Describe() to override the promoted one, and
can always reach the original explicitly via d.Animal.Describe().
Interfaces: Implicit Satisfaction
Here's the part of Go's design that tends to genuinely surprise people
the first time they understand it, and it's arguably the single most
consequential idea in this chapter. An interface lists a set of method
signatures. Any type that has methods matching that set automatically
satisfies the interface — there is no implements keyword, and no
explicit declaration of intent anywhere near the type's definition:
type Shaper interface {
Area() float64
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
Rectangle satisfies Shaper the moment it has an Area() float64
method — nothing else needs to change, and Rectangle never mentions
Shaper by name:
func printArea(s Shaper) {
fmt.Println("area:", s.Area())
}
printArea(Rectangle{Width: 3, Height: 4}) // area: 12
Implicit satisfaction means an interface can be defined after the types that will implement it, even in a package that has never heard of those types. This is what lets Go code depend on behavior instead of concrete implementations, without any coordination between the package defining the interface and the package defining the type.
This is exactly the pattern the standard library's io.Reader and
io.Writer use throughout the networking code later in this book: a
net.Conn, an os.File, and a bytes.Buffer all satisfy io.Writer
simply by having a matching Write method, with no shared ancestry
between them at all. Once this idea clicks, a lot of Go's standard
library stops looking like a pile of unrelated packages and starts
looking like the same handful of tiny contracts, reused everywhere.
== compiles fine and then panics at runtime: "comparing uncomparable type." This is one of the few places Go's static type checker cannot save you, precisely because the concrete type is only known at runtime.Composing Interfaces by Embedding
Interfaces can embed other interfaces, building a larger contract out of smaller ones — the same "compose, don't inherit" philosophy from struct embedding, applied to behavior instead of data:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
Any type with both a matching Read and a matching Write method
automatically satisfies ReadWriter — nothing needs to reference
ReadWriter by name. This is precisely how the standard library's io
package is built: io.Reader and io.Writer are two of the smallest,
most reused interfaces in all of Go, and io.ReadWriter simply composes
them, exactly as shown above.
The Empty Interface and any
interface{} — aliased since Go 1.18 to the more readable any — is the
interface with zero required methods, so every type satisfies it. It is
Go's escape hatch for "accept literally anything":
func describe(v any) {
fmt.Printf("value: %v, type: %T\n", v, v)
}
describe(42) // value: 42, type: int
describe("hello") // value: hello, type: string
describe(Rectangle{3, 4}) // value: {3 4}, type: main.Rectangle
Getting a concrete value back out of an any requires a type
assertion, which should almost always use the two-value form so a
mismatched type produces ok = false instead of a panic:
var v any = "hello"
s, ok := v.(string)
if !ok {
fmt.Println("not a string")
return
}
fmt.Println(s)
A type switch handles several possible concrete types cleanly:
func describeType(v any) string {
switch x := v.(type) {
case int:
return fmt.Sprintf("int: %d", x)
case string:
return fmt.Sprintf("string: %q", x)
default:
return fmt.Sprintf("other: %v", x)
}
}
any gives up all compile-time checking on that value — a typo that passes the wrong type compiles fine and fails only at runtime, at the type assertion. Use any at genuine boundaries (decoding JSON of unknown shape, a generic container), not as a shortcut to avoid writing a proper type or interface.A Brief Introduction to Generics
Go 1.18 added generics: functions and types parameterized by a type,
checked at compile time — a middle ground between a concrete type
(too rigid) and any (too permissive, no compile-time safety). A generic
function declares type parameters in square brackets before its regular
parameters:
func Sum[T int | float64](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.5, 2.5})) // 4
[T int | float64] declares a type parameter T constrained to either
int or float64 — the compiler checks every call at compile time and
rejects Sum([]string{...}) outright, something a version of Sum
written to accept []any could never do. The standard library's slices
and maps packages (Go 1.21+) are built entirely on generics, and are
usually the first place to reach for generic behavior rather than writing
it yourself.
| Approach | Compile-time safety | Use when |
|---|---|---|
| Concrete type | Full | The function only ever needs to work with one type |
any |
None — checked at runtime via assertions | Genuinely arbitrary or unknown-shape data |
Generics ([T ...]) |
Full, across a constrained set of types | The same logic applies uniformly to several known types |
Try It Yourself
Build a tiny shape hierarchy using embedding, interfaces, and one generic helper:
type Shaper interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func TotalArea[T Shaper](shapes []T) float64 {
var total float64
for _, s := range shapes {
total += s.Area()
}
return total
}
- Add
Circleand itsAreamethod (import"math"), confirming it satisfiesShaperalongsideRectanglefrom earlier in this chapter. - Write
TotalAreaas shown, and call it once with[]Rectangleand once with[]Circle— notice the same generic function works for both without any type assertions. - Add a
Namedstruct embeddingCircleand adding aLabel stringfield; confirmnamed.Area()still works via promotion. - Bonus: try calling
TotalAreawith a mixed[]Shaperslice containing both aRectangleand aCirclevalue, and explain why this works even thoughTotalArea[T Shaper]and a plainfunc TotalArea(shapes []Shaper)behave almost identically here — then consider what would break ifShaperrequired a pointer receiver.
Interfaces answer "what can this value do?" Generics answer "which types is this code allowed to work with?" Confusing the two questions is the fastest way to reach for the wrong tool.
Frequently Asked Questions
Do I need to write something like class Dog implements Animal anywhere for Dog to embed Animal properly?
No, and that's the point — embedding is just Animal sitting inside Dog as an anonymous field, and Go's lookup rules automatically promote Animal's fields and methods up to Dog. There's no inheritance keyword and no declaration of intent anywhere near the type definition; d.Name and d.Describe() simply work because Go looks inside the embedded value when Dog itself has no field or method of that name.
My type has an Area() float64 method but the compiler says it doesn't satisfy my Shaper interface — what am I missing?
The most common culprit is the method-set distinction this chapter flags: a pointer type's method set includes both its value-receiver and pointer-receiver methods, but a plain value type only includes the value-receiver ones. If Area is defined with a pointer receiver, only *Rectangle satisfies Shaper, not bare Rectangle — double check which receiver kind you used and which one you're passing to the function expecting the interface.
Why does Go let an interface be defined in a completely different package from the types that satisfy it?
Because satisfaction is implicit and structural, not declared. A type never mentions the interface by name, so the interface can be written after the type exists, in a package that has never heard of it. This is exactly why io.Reader and io.Writer work across net.Conn, os.File, and bytes.Buffer with zero shared ancestry between them — each one just happens to have a matching method.
Should I reach for generics or a plain interface when writing something like TotalArea?
Ask which question you're actually answering. An interface answers "what can this value do?" — use it when you need a function to accept anything with a certain behavior, like Shaper. Generics answer "which types is this code allowed to work with?" and are the right tool when you want the compiler to specialize the same logic across a constrained set of concrete types, the way Sum[T int | float64] does. Most of this book's networking code sticks to plain interfaces; generics show up far less often.
Is any just a shortcut for "I don't want to write a proper type"?
Treat it as an escape hatch, not a shortcut. any erases all compile-time checking on that value until you assert it back to something concrete, so a typo that passes the wrong type will compile fine and only fail at runtime, at the type assertion. It earns its keep at genuine boundaries — decoding JSON of unknown shape, a truly generic container — not as a way to dodge writing an interface or a type parameter.
Where This Goes From Here
Take a moment to notice what you actually just learned to do without a
class keyword anywhere in sight: group data (structs), reuse it without
a rigid hierarchy (embedding), describe behavior without naming a
concrete type (interfaces), and write logic that works uniformly across a
family of types (generics). That's the whole toolkit Go offers instead of
object orientation, and by this point you have all four pieces.
There's one more piece of everyday Go you've been quietly relying on
throughout every code example in this chapter, without it ever being
named directly: slices like []Rectangle and []T, and the maps you'll
lean on constantly from here forward, both deserve a much closer look
than "looks like an array" gives them credit for. Chapter 2.6 takes
them apart properly, right after the next chapter's look at io.Reader
and io.Writer, because a surprising number of famous Go bugs — and
famous Go interview questions — live entirely inside those two types.