Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Low-Level Design (Object-Oriented Design)  Module 9 – One design, six languages

LLD in Go: interfaces, embedding, generics

Write a low-level design in Go: small interfaces declared where they are used, embedding without its traps, useful zero values and Go 1.27 generic methods.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and Go 1.27.1
  • By MySmartCoPilot
Free intro, Pro: 8 of 30 minutes free
Free: A parking design in Go's three tools. With a pass: the other 8 sections and 3 interview answers, about 22 minutes.

What you will learn

  • Design with structs, methods and small consumer-side interfaces
  • Use embedding for composition without mistaking it for inheritance
  • Write constructors and zero values that leave no object half ready
  • Use generics, and Go 1.27 generic methods, where they help

Before you start

What you’ll learn · Before you start

What you will learn

  • Design with structs, methods and small consumer-side interfaces
  • Use embedding for composition without mistaking it for inheritance
  • Write constructors and zero values that leave no object half ready
  • Use generics, and Go 1.27 generic methods, where they help

Before you start

On this page

Go has no classes and no inheritance, so a low-level design in Go is built from structs, methods and three tools. Interfaces are sets of methods, and a type satisfies one implicitly, by having the methods, with no implements clause. Because of that, Go code declares small interfaces in the package that uses them rather than beside the types that implement them. Embedding places one struct inside another and promotes the inner struct’s fields and methods, which gives composition with very little code. The outer type does not become the inner one, though, and a promoted method never calls a method that the outer type adds. Generics let a type or a function take type parameters, such as a repository for any key and value, and Go 1.27 added generic methods, methods that declare type parameters of their own. Around these tools sit the habits of Go designs: a NewX constructor that returns a ready value, zero values that work where possible, and errors returned as values.

Every example in this lesson ran on Go 1.27.1, and the comparisons ran on Go 1.26.8, the release before it.

A parking design in Go’s three tools

Class diagram: a Lot uses a FeePolicy interface that two structs satisfy implicitly, a generic Repo, and an EVSpot that embeds Spot.Lot-policyFeePolicy-tickets*Repo[string, Ticket]+Park(number, spot, minute)+Leave(number, minute)(int, error)«interface, declared with Lot»FeePolicy+Fee(minutes int)intstructs that never mention FeePolicy«generic»Repo[K comparable, V any]+Put(key K, value V)+Get(key K)(V, bool)+Delete(key K)+Map[R any](f func(V) R)[]REVSpot+ChargerKWintSpot+IDstring+Levelint+Label()stringHourly+PerHourintFlat+Amountintpolicy1has Fee(int) inttickets1embeds

The parking design in Go: an interface declared by its user, implicit implementations, a generic repository and embedding

Text description of the diagram

A class diagram in one column, from top to bottom.

  • Lot, a struct: unexported fields policy: FeePolicy and tickets: *Repo[string, Ticket]; methods Park(number, spot, minute) and Leave(number, minute), which returns (int, error). A line named policy goes from Lot to FeePolicy, with multiplicity 1.
  • FeePolicy, marked «interface, declared with Lot», with one method, Fee(minutes int) int.
  • A group labelled "structs that never mention FeePolicy" holds Hourly, with PerHour: int, and Flat, with Amount: int. A dashed line labelled "has Fee(int) int" goes from the group to FeePolicy: both structs have that method, so both satisfy the interface without declaring it.
  • Repo, marked «generic», with type parameters K comparable and V any: methods Put(key K, value V), Get(key K) returning (V, bool), Delete(key K) and the generic method Map[R any](f func(V) R) returning []R. A line named tickets goes from Lot to Repo, with multiplicity 1.
  • EVSpot, with ChargerKW: int, and Spot, with ID: string, Level: int and the method Label(): string. A line labelled embeds goes from EVSpot to Spot, with a filled diamond at EVSpot: an EVSpot contains a Spot, and Spot's fields and methods are promoted to it.

The lot needs one thing from a fee policy, a Fee(minutes int) int method, so it declares a one-method interface for exactly that. Hourly and Flat never mention the interface, yet both satisfy it. Tickets live in a generic Repo[K, V], and EVSpot embeds Spot:

Interfaces, embedding and a generic repository in one program

session · parking_session.py

from session import run, version

version()
run("go run parking/main.go")

Output

# go version go1.27.1 darwin/arm64
$ go run parking/main.go
B2 on level 1 | charger: 7 kW
main.Hourly   T1 pays 12000 paise | still parked: [T2@B2]
main.Flat     T1 pays 10000 paise | still parked: [T2@B2]
leave T9: unknown ticket | is ErrUnknownTicket: true

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 parking_session.py

parking/main.go

// A parking design in Go: structs with methods, an interface declared by the code that uses it,
// a spot type that embeds another, and a generic repository with a Go 1.27 generic method.
package main

import (
	"errors"
	"fmt"
	"slices"
)

type Spot struct {
	ID    string
	Level int
}

func (s Spot) Label() string { return fmt.Sprintf("%s on level %d", s.ID, s.Level) }

// EVSpot embeds Spot: it has Spot's fields and methods, and a field of its own.
type EVSpot struct {
	Spot
	ChargerKW int
}

// Repo keeps values by key, in the order they were first added.
type Repo[K comparable, V any] struct {
	items map[K]V
	order []K
}

func NewRepo[K comparable, V any]() *Repo[K, V] {
	return &Repo[K, V]{items: make(map[K]V)}
}

func (r *Repo[K, V]) Put(key K, value V) {
	if _, ok := r.items[key]; !ok {
		r.order = append(r.order, key)
	}
	r.items[key] = value
}

func (r *Repo[K, V]) Get(key K) (V, bool) {
	value, ok := r.items[key]
	return value, ok
}

func (r *Repo[K, V]) Delete(key K) {
	delete(r.items, key)
	r.order = slices.DeleteFunc(r.order, func(k K) bool { return k == key })
}

// Map is a generic method: it declares a type parameter of its own, R (Go 1.27).
func (r *Repo[K, V]) Map[R any](f func(V) R) []R {
	out := make([]R, 0, len(r.order))
	for _, key := range r.order {
		out = append(out, f(r.items[key]))
	}
	return out
}

// FeePolicy is the only thing a lot needs from a policy. Any type with this method satisfies it.
type FeePolicy interface {
	Fee(minutes int) int // in paise
}

type Hourly struct{ PerHour int }

func (h Hourly) Fee(minutes int) int { return h.PerHour * ((minutes + 59) / 60) }

type Flat struct{ Amount int }

func (f Flat) Fee(int) int { return f.Amount }

type Ticket struct {
	Number, Spot string
	Entered      int
}

var ErrUnknownTicket = errors.New("unknown ticket")

type Lot struct {
	policy  FeePolicy
	tickets *Repo[string, Ticket]
}

func NewLot(policy FeePolicy) *Lot {
	return &Lot{policy: policy, tickets: NewRepo[string, Ticket]()}
}

func (l *Lot) Park(number, spot string, minute int) {
	l.tickets.Put(number, Ticket{Number: number, Spot: spot, Entered: minute})
}

func (l *Lot) Leave(number string, minute int) (int, error) {
	t, ok := l.tickets.Get(number)
	if !ok {
		return 0, fmt.Errorf("leave %s: %w", number, ErrUnknownTicket)
	}
	l.tickets.Delete(number)
	return l.policy.Fee(minute - t.Entered), nil
}

func main() {
	ev := EVSpot{ID: "B2", Level: 1, ChargerKW: 7} // Go 1.27: promoted fields as keys
	fmt.Println(ev.Label(), "| charger:", ev.ChargerKW, "kW")

	for _, policy := range []FeePolicy{Hourly{PerHour: 4000}, Flat{Amount: 10000}} {
		lot := NewLot(policy)
		lot.Park("T1", "A1", 0)
		lot.Park("T2", ev.ID, 30)
		fee, _ := lot.Leave("T1", 130)
		open := lot.tickets.Map(func(t Ticket) string { return t.Number + "@" + t.Spot })
		fmt.Printf("%-13T T1 pays %d paise | still parked: %v\n", policy, fee, open)
	}

	_, err := NewLot(Flat{Amount: 10000}).Leave("T9", 10)
	fmt.Println(err, "| is ErrUnknownTicket:", errors.Is(err, ErrUnknownTicket))
}

session.py

"""Prints a terminal session: "$ " and each command, then everything it printed (standard output and standard
error together), and its exit status when that is not 0.

`go` is Go 1.27.1, the version this lesson pins, from the mise installation that also holds the Python running this
file; go="1.26.8" switches to Go 1.26.8 for the comparisons with the previous release. learn:verify's sandbox may not
read that folder, so these sessions are recorded with LEARN_VERIFY_NETWORK=allow. They read only this folder and the
toolchain, write only inside this folder (Go's build cache included), and never use the network: GOTOOLCHAIN=local
stops Go from downloading another version.

A command that fails makes the session exit with status 1, unless the script lists it in may_fail. For a command
that may fail, `keep` (a regular expression) shows only the lines of its output that matter. Test timings, which
change on every run, are left out.
"""

import os
import re
import subprocess
import sys
from pathlib import Path

# <mise>/installs/python/3.14.8/bin/python3 -> <mise>/installs
INSTALLS = Path(sys.executable).resolve().parents[3]
TIMING = re.compile(r"\s*\(\d+\.\d+s\)|\t\d+\.\d+s$")


def _env(go):
    here = Path.cwd()
    for folder in ("home", "tmp", "gocache", "gopath"):
        (here / folder).mkdir(exist_ok=True)
    env = dict(os.environ)
    env.update(
        PATH=os.pathsep.join([str(INSTALLS / "go" / go / "bin"), "/usr/bin", "/bin"]),
        HOME=str(here / "home"),
        TMPDIR=str(here / "tmp"),
        GOCACHE=str(here / "gocache"),
        GOPATH=str(here / "gopath"),
        GOTOOLCHAIN="local",
        GOTELEMETRY="off",
        GOFLAGS="-trimpath",
        GO111MODULE="auto",
    )
    return env


def _shell(command, go):
    done = subprocess.run(command, shell=True, env=_env(go), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    return done.returncode, [TIMING.sub("", line) for line in done.stdout.splitlines()]


def version(go="1.27.1"):
    """Prints `go version` as a comment, so the record says which Go ran."""
    status, lines = _shell("go version", go)
    if status != 0 or not lines:
        sys.exit(f"Go {go} is not installed on this computer")
    print("#", lines[0])


def run(script, go="1.27.1", may_fail=(), keep=None):
    """Runs each line of the script as one shell command, in the examples folder."""
    unexpected = 0
    for command in (line.strip() for line in script.strip().splitlines()):
        print(f"$ {command}")
        status, lines = _shell(command, go)
        if status != 0 and keep:
            lines = [line for line in lines if re.search(keep, line)]
        for line in lines:
            print(line)
        if status != 0:
            print(f"[exit status {status}]")
            if command not in may_fail:
                unexpected += 1
    if unexpected:
        sys.exit(1)

The first line comes from the embedded Spot: ev.Label() and ev.ID work on an EVSpot as if they were its own, and Go 1.27 even accepts the promoted fields ID and Level as keys of the struct literal. The hourly policy charged 3 started hours of 40.00 for T1’s 130 minutes, the flat one 100.00, and the lot’s code did not change between the two. Map is a generic method: called with a function from Ticket to string, it returned the labels of the tickets still parked. The last line is an error value: Leave wrapped ErrUnknownTicket with %w, and errors.Is found it.

Each tool, and each habit, has an edge that a design must respect:

  • Interfaces stay small and live with their users; a one-line check makes the compiler confirm that a type fits.
  • Embedding promotes methods, but a promoted method’s receiver is the inner value, so outer methods are never called from inside.
  • Zero values are the default state of every Go struct, so a design either makes them work or offers a constructor.
  • A nil pointer stored in an error makes a non-nil error, a trap common enough that the Go FAQ answers it.
  • Generic methods arrived in Go 1.27, and interface methods still cannot declare type parameters.

Check yourself

3 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 3 In the parking program, how does Hourly come to satisfy FeePolicy?

    Choose one answer.

    Show the answer to question 1

    Answer: It has a method Fee(minutes int) int, and nothing else is needed

    Go interfaces are satisfied implicitly: a type with all of an interface's methods satisfies it, and never names it. That is why Hourly and Flat can fit an interface declared beside the lot.

  2. Question 2 of 3 Where does a Go design usually declare an interface such as FeePolicy?

    Choose one answer.

    Show the answer to question 2

    Answer: In the package that uses it, with only the methods that package needs

    Because satisfaction is implicit, the user of an interface can declare exactly what it needs, and types written elsewhere fit without depending on it.

  3. Question 3 of 3 Repo's Map method is written Map[R any]. What makes it new in Go 1.27?

    Choose one answer.

    Show the answer to question 3

    Answer: It is a method that declares a type parameter of its own

    Methods of generic types existed before; Go 1.27 lets a method declare type parameters beyond its type's own, so Map can return []R for any R chosen at the call.

Small interfaces, declared where they are used Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • Embedding is not inheritance: the override that never runs
  • Constructors and zero values that work
  • Errors as values: the nil that is not nil
  • Generic repositories, and Go 1.27 generic methods
  • Practise: port a booking service to Go
  • Interview questions
    • How does a Go type satisfy an interface, and where should the interface be declared?
    • A teammate ports a Java class hierarchy to Go: Account has Statement(), which calls Fee(), and PremiumAccount embeds Account and defines a lower Fee(). Premium customers are still charged the standard fee. Explain why, and redesign it.
    • After a refactor, every successful seat reservation in a Go service logs "reservation failed: <nil>" and is rolled back. The function returns error. What is the likely bug, how do you prove it, and how do you fix and prevent it?
    • Your team adopts Go 1.27 and wants a generic Repository[K, V] with Map, Filter and GroupBy methods, plus a Repository interface that services depend on, so they can swap in a database later. How do you design it, and what can and cannot be generic?
  • Key takeaways

7 runnable examples (Python, assert.go, booking.go, booking_test.go, copied.go, fixed.go, generic_impl.go, interface_method.go, nilerr.go, override.go, receipts.go, repo.go, session, zero.go) · 3 interview answers · about 17 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Read a complete sample first: Low-level design: parking lot (the model)

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.