net/go.book
All Parts Marketing

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

Struct equality only works when every field is comparable
A struct containing a slice, map, or function field cannot be compared with == 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.

Comparing interface values can panic at runtime
Two interface values are equal only if their dynamic types are equal and their dynamic values are equal — but if the dynamic type itself is not comparable (a slice or map, say), comparing two such interface values with == 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 erases type safety until you assert it back
Accepting 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
}
  1. Add Circle and its Area method (import "math"), confirming it satisfies Shaper alongside Rectangle from earlier in this chapter.
  2. Write TotalArea as shown, and call it once with []Rectangle and once with []Circle — notice the same generic function works for both without any type assertions.
  3. Add a Named struct embedding Circle and adding a Label string field; confirm named.Area() still works via promotion.
  4. Bonus: try calling TotalArea with a mixed []Shaper slice containing both a Rectangle and a Circle value, and explain why this works even though TotalArea[T Shaper] and a plain func TotalArea(shapes []Shaper) behave almost identically here — then consider what would break if Shaper required 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.