Go
Structs & Methods
A Go struct starts out looking identical to a
C struct — a bundle of named fields. What's different is
what you can attach to it: methods, functions associated with a specific type, giving
Go something close to C++'s classes — deliberately
without inheritance, Go's most consequential design decision covered in this chapter.
- What it is
- A struct is a bundle of named fields, like C's. A method is a function with a special "receiver" parameter, associating it with a specific type.
- Mental model
- Where C++ says "a class owns its methods," Go says "a method is just a function that happens to take a specific type as its first, special argument" — syntactically attached, conceptually separate.
- Key numbers
- Two receiver kinds: value receivers (get a copy) and pointer receivers (can modify the original) — choosing between them is this chapter's most important practical decision.
- Reach for it when
- Bundling related data (a struct) is needed almost everywhere; methods specifically when behavior naturally belongs to that data.
Structs
type SensorReading struct {
Timestamp int
Value float64
Status string
}
r := SensorReading{Timestamp: 1000, Value: 23.5, Status: "OK"}
fmt.Println(r.Timestamp) // 1000 — dot operator, exactly like C
Field names starting with a capital letter (Timestamp, not timestamp) are
exported — visible outside the package that defines them. This is Go's entire access
control mechanism: capitalization, not a public/private keyword like
C++'s.
Methods and receivers
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 { // (c Circle) is the RECEIVER — associates Area with Circle
return 3.14159 * c.Radius * c.Radius
}
circle := Circle{Radius: 5}
fmt.Println(circle.Area()) // 78.5397 — called just like a C++ method
(c Circle) between func and the method name is the receiver —
it's what makes Area callable as circle.Area() instead of a free-floating
function. Structurally, this is the same "hidden first argument" idea from
C++'s member functions, just written
explicitly instead of implicitly.
Value receivers vs. pointer receivers
func (c Circle) Scale(factor float64) { // VALUE receiver — gets a COPY
c.Radius *= factor // only changes the copy — caller's circle is unaffected
}
func (c *Circle) ScalePtr(factor float64) { // POINTER receiver — gets the REAL thing
c.Radius *= factor // actually changes the caller's circle
}
circle := Circle{Radius: 5}
circle.Scale(2)
fmt.Println(circle.Radius) // still 5 — Scale only touched a copy
circle.ScalePtr(2)
fmt.Println(circle.Radius) // 10 — ScalePtr touched the real thing
This is exactly the pass-by-value vs. pass-by-reference distinction from C, applied to methods specifically. The practical rule of thumb: use a pointer receiver whenever a method needs to modify the receiver, or when the struct is large enough that copying it on every call would be wasteful.
Embedding — composition instead of inheritance
Go has no inheritance at all — deliberately, a core design decision, not a missing feature. Instead, a struct can embed another struct, gaining its fields and methods directly:
type Sensor struct {
Name string
}
func (s Sensor) PrintName() {
fmt.Println(s.Name)
}
type TemperatureSensor struct {
Sensor // embedded — NOT "extends Sensor," just "contains a Sensor, promoted"
Unit string
}
t := TemperatureSensor{Sensor: Sensor{Name: "Temp"}, Unit: "Celsius"}
t.PrintName() // works — Sensor's method is "promoted" up to TemperatureSensor
fmt.Println(t.Name) // works — Sensor's field is promoted too
This looks similar to C++ inheritance at the
call site, but it's genuinely different underneath: TemperatureSensor literally
contains a Sensor value as a field — there's no shared type hierarchy, no
virtual, and no "is-a" relationship the compiler enforces. It's composition, with Go's
syntax making the common case of "just forward these calls" nearly invisible.
Real-world applications
- Request/response types in web services — a struct per API endpoint bundling exactly the fields that request or response needs, extremely common in Go server code.
- Configuration structs — a struct with exported fields matching a YAML/JSON config file's structure, parsed directly into it.
- Embedding for shared behavior — embedding a common
LoggerorBaseHandlerstruct into many other structs, sharing behavior without a class hierarchy.
Gotchas
- Mixing value and pointer receivers on the same type is legal but discouraged — pick one convention (almost always pointer receivers, once a type has any method that needs to mutate it) and use it consistently across every method on that type.
- A large struct with only value receivers copies its entire contents on every single method call — a real, easy-to-miss performance cost that pointer receivers avoid.
- Embedding is not inheritance, and code that assumes it behaves identically will be surprised — there's no polymorphism through an embedded type the way there is through a C++ base class; see the next chapter, Interfaces, for Go's actual answer to that problem.
- Lowercase (unexported) struct fields are invisible outside their own package, including to code that embeds the struct from another package — a real, sometimes-surprising access restriction worth remembering when designing a struct meant to be embedded elsewhere.