Module 1 ended with a score report — parse → accumulate → sort → format, using maps and slices. The hint at the end was: sorting by value to get "top N" requires bundling each key-value pair into a sortable unit, and that's exactly what structs unlock. Time to cash that in.
Same shape, upgraded. Given a heterogeneous list of infra resources (some Pods, some Services), produce a sorted health report grouped by type.
Step 1: Define the types
Pod and Service share a name and namespace, so embed Metadata. Each one has its own resource-specific fields and its own IsHealthy rule. Both satisfy a small Resource interface so the report code doesn't care which is which.
type Metadata struct {
Name string
Namespace string
}
func (m Metadata) FullName() string {
return m.Namespace + "/" + m.Name
}
type Pod struct {
Metadata
Status string
MemoryMB int
}
func (p Pod) IsHealthy() bool {
return p.Status == "Running"
}
type Service struct {
Metadata
Port int
Healthy bool
}
func (s Service) IsHealthy() bool {
return s.Healthy
}
// The interface every reportable type satisfies.
type Resource interface {
FullName() string
IsHealthy() bool
}
FullName is promoted from Metadata — neither Pod nor Service defines it. Both types satisfy Resource implicitly: no implements keyword needed.
Step 2: Group with a map
Same pattern Module 1 used for status counts. The key is the resource kind; the value is a slice of Resource. Grouping a heterogeneous slice into typed buckets:
func groupByKind(rs []Resource) map[string][]Resource {
grouped := make(map[string][]Resource)
for _, r := range rs {
kind := "Unknown"
switch r.(type) {
case Pod:
kind = "Pod"
case Service:
kind = "Service"
}
grouped[kind] = append(grouped[kind], r)
}
return grouped
}
The type switch is the bridge — it pulls the concrete type out of the interface so the report can label things correctly. This is the textbook "type switch at a boundary" use: serialization / display / categorization, not core logic.
Step 3: Sorted, formatted output
Inside each bucket, sort unhealthy first (so the report leads with what's broken), then by name. Across buckets, sort by kind so output is deterministic.
func report(rs []Resource) []string {
grouped := groupByKind(rs)
kinds := make([]string, 0, len(grouped))
for k := range grouped {
kinds = append(kinds, k)
}
sort.Strings(kinds)
var out []string
for _, kind := range kinds {
bucket := grouped[kind]
sort.Slice(bucket, func(i, j int) bool {
// Unhealthy first
if bucket[i].IsHealthy() != bucket[j].IsHealthy() {
return !bucket[i].IsHealthy()
}
return bucket[i].FullName() < bucket[j].FullName()
})
out = append(out, fmt.Sprintf("== %s (%d) ==", kind, len(bucket)))
for _, r := range bucket {
mark := "OK"
if !r.IsHealthy() {
mark = "FAIL"
}
out = append(out, fmt.Sprintf(" [%s] %s", mark, r.FullName()))
}
}
return out
}
Three patterns from Module 1 carried straight through: sorted-keys for deterministic output, sort.Slice with a comparator, formatted lines with fmt.Sprintf. The new pieces — Resource interface, embedded Metadata, type switch — let the same code work over any future resource type. Add ConfigMap, give it IsHealthy and a Metadata, and report doesn't change.
What just happened
Take stock of every Module 2 idea you used here:
- Structs with embedded
Metadatato shareName/Namespace(§01, §06) - Promoted methods —
FullNamecame along for free (§06) - Pointers vs values — value receivers throughout because we only read (§03, §05)
- Implicit interface satisfaction —
PodandServicesatisfyResourcewithout declaring it (§07) - Type switch at a boundary — categorize for display, not for logic (§08)
If anything here felt fuzzy, the cross-references are the place to go back. If it all clicked, you're ready for Module 3, where every one of these types gets a test.