Methods vs Functions — Tasks¶
Exercise structure¶
- 🟢 Easy — for beginners
- 🟡 Medium — middle level
- 🔴 Hard — senior level
- 🟣 Expert — professional level
A solution for each exercise is provided at the end.
Easy 🟢¶
Task 1 — Your first method¶
Create a Rectangle struct and add an Area() method.
Task 2 — Constructor function¶
Write NewRectangle(w, h float64) Rectangle.
Task 3 — Method on a nilable receiver¶
Create a Logger type whose Log(msg string) method does nothing when the receiver is nil.
Task 4 — Value vs pointer receiver¶
For Counter, write Inc() (pointer receiver) and Value() int (value receiver).
Task 5 — String() method¶
Add String() string to the Color type:
Medium 🟡¶
Task 6 — Method on a slice type¶
Define type Words []string and add a Join(sep string) string method.
Task 7 — Method on a function type¶
Define type Handler func(int) int and add Pipe(other Handler) Handler — composition.
Task 8 — Map of methods¶
Create a Calculator with Add, Sub, Mul, Div methods. Dispatch the appropriate method based on an op string argument provided by the user.
Task 9 — Embedding and promotion¶
Animal (Name string, Speak() string) and Dog (embeds Animal). For Dog, Speak() should return "Woof!" (override).
Task 10 — Method chain¶
Build a Pipeline:
Task 11 — Callback via method value¶
A Worker struct with a Run(callback func()) method. Pass another Worker's OnDone() method as the callback.
Task 12 — Choosing a receiver¶
Add Append(data []byte) to Buffer (a 1MB byte slice). Pick a pointer receiver and explain why in a comment.
Hard 🔴¶
Task 13 — Builder with validation¶
Create a UserBuilder: - Setter methods Name(s string), Age(n int), Email(e string) - Build() (User, error) — return an error when any required field is missing
Task 14 — Decorator pattern¶
A Repo interface (Find(id string)). Write a LoggingRepo decorator that logs every Find call.
Task 15 — Functional options¶
Configure a Server with the functional options pattern:
Task 16 — Method expression dispatch table¶
Implement the following task using method expressions:
type Shape struct{}
func (Shape) Area(...) float64
func (Shape) Perimeter(...) float64
// Dispatch based on op string — without if statements
Task 17 — Concurrent counter¶
Write AtomicCounter — using atomic.Int64 for lock-free inc/value.
Task 18 — Repository + Decorator¶
A UserRepo interface, pgUserRepo (mock SQL), and cachingUserRepo (in-memory cache wrapper). On a cache miss the user request reaches pg.
Expert 🟣¶
Task 19 — Generic List with methods¶
type List[T any] struct{ items []T } — Add, Get, Len, and Map (also explain why Map must live at the package level).
Task 20 — DDD aggregate¶
Order aggregate root: - AddItem(p Product, qty int) error - Submit() error - MarkPaid() error - State machine — Draft → Submitted → Paid - PullEvents() []Event
Task 21 — Method dispatch via a plugin system¶
Build a small plugin system that dispatches via method expressions, without runtime plugin loading.
Task 22 — Context-aware methods¶
Write a Service struct where every method accepts a context.Context and respects cancellation.
Solutions¶
Solution 1¶
type Rectangle struct{ Width, Height float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
Solution 2¶
Solution 3¶
type Logger struct{ prefix string }
func (l *Logger) Log(msg string) {
if l == nil { return }
fmt.Println(l.prefix, msg)
}
Solution 4¶
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
func (c Counter) Value() int { return c.n }
Solution 5¶
type Color int
const ( Red Color = iota; Green; Blue )
func (c Color) String() string {
switch c {
case Red: return "red"
case Green: return "green"
case Blue: return "blue"
}
return "unknown"
}
Solution 6¶
Solution 7¶
type Handler func(int) int
func (h Handler) Pipe(other Handler) Handler {
return func(x int) int { return other(h(x)) }
}
Solution 8¶
type Calculator struct{}
func (Calculator) Add(a, b int) int { return a+b }
func (Calculator) Sub(a, b int) int { return a-b }
func (Calculator) Mul(a, b int) int { return a*b }
func (Calculator) Div(a, b int) int { return a/b }
var ops = map[string]func(Calculator, int, int) int{
"+": Calculator.Add, "-": Calculator.Sub,
"*": Calculator.Mul, "/": Calculator.Div,
}
func Apply(op string, a, b int) int {
return ops[op](Calculator{}, a, b)
}
Solution 9¶
type Animal struct{ Name string }
func (a Animal) Speak() string { return "..." }
type Dog struct{ Animal }
func (d Dog) Speak() string { return "Woof!" }
Solution 10¶
type Pipeline struct{ ops []func(int) int }
func NewPipeline() *Pipeline { return &Pipeline{} }
func (p *Pipeline) Add(op func(int) int) *Pipeline {
p.ops = append(p.ops, op)
return p
}
func (p *Pipeline) Run(x int) int {
for _, op := range p.ops { x = op(x) }
return x
}
Solution 11¶
type Worker struct{ name string }
func (w Worker) OnDone() { fmt.Println(w.name, "done") }
func (w Worker) Run(cb func()) { cb() }
a, b := Worker{"a"}, Worker{"b"}
a.Run(b.OnDone) // callback via method value
Solution 12¶
type Buffer struct{ data [1 << 20]byte; n int }
// Pointer receiver — Buffer is 1MB; copying it on every call is expensive
// Mutation: the n field changes
func (b *Buffer) Append(data []byte) {
copy(b.data[b.n:], data)
b.n += len(data)
}
Solution 13¶
type UserBuilder struct{ user User }
func (b *UserBuilder) Name(s string) *UserBuilder { b.user.Name = s; return b }
func (b *UserBuilder) Age(n int) *UserBuilder { b.user.Age = n; return b }
func (b *UserBuilder) Email(e string) *UserBuilder { b.user.Email = e; return b }
func (b *UserBuilder) Build() (User, error) {
if b.user.Name == "" { return User{}, errors.New("name required") }
if b.user.Email == "" { return User{}, errors.New("email required") }
return b.user, nil
}
Solution 14¶
type Repo interface { Find(id string) (string, error) }
type pgRepo struct{}
func (p *pgRepo) Find(id string) (string, error) { return "user_" + id, nil }
type LoggingRepo struct{ inner Repo }
func (l *LoggingRepo) Find(id string) (string, error) {
log.Println("find:", id)
return l.inner.Find(id)
}
Solution 15¶
type Server struct {
port int
timeout time.Duration
cert string
key string
}
type Option func(*Server)
func WithPort(p int) Option { return func(s *Server) { s.port = p } }
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func WithTLS(cert, key string) Option {
return func(s *Server) { s.cert = cert; s.key = key }
}
func NewServer(opts ...Option) *Server {
s := &Server{port: 80, timeout: 30*time.Second}
for _, opt := range opts { opt(s) }
return s
}
Solution 16¶
type Shape struct{ Width, Height float64 }
func (s Shape) Area() float64 { return s.Width * s.Height }
func (s Shape) Perimeter() float64 { return 2*(s.Width+s.Height) }
var dispatch = map[string]func(Shape) float64{
"area": Shape.Area,
"perimeter": Shape.Perimeter,
}
func Compute(s Shape, op string) float64 { return dispatch[op](s) }
Solution 17¶
import "sync/atomic"
type AtomicCounter struct{ n atomic.Int64 }
func (c *AtomicCounter) Inc() { c.n.Add(1) }
func (c *AtomicCounter) Value() int64 { return c.n.Load() }
Solution 18¶
type User struct{ ID, Name string }
type UserRepo interface { Find(id string) (*User, error) }
type pgUserRepo struct{}
func (p *pgUserRepo) Find(id string) (*User, error) {
return &User{ID: id, Name: "User_" + id}, nil
}
type cachingUserRepo struct {
inner UserRepo
cache map[string]*User
mu sync.Mutex
}
func (c *cachingUserRepo) Find(id string) (*User, error) {
c.mu.Lock()
if u, ok := c.cache[id]; ok { c.mu.Unlock(); return u, nil }
c.mu.Unlock()
u, err := c.inner.Find(id)
if err != nil { return nil, err }
c.mu.Lock()
c.cache[id] = u
c.mu.Unlock()
return u, nil
}
Solution 19¶
type List[T any] struct{ items []T }
func (l *List[T]) Add(x T) { l.items = append(l.items, x) }
func (l *List[T]) Get(i int) T { return l.items[i] }
func (l *List[T]) Len() int { return len(l.items) }
// Map at the package level — because methods cannot introduce their own type parameters
func Map[T, U any](l *List[T], f func(T) U) *List[U] {
r := &List[U]{}
for _, x := range l.items { r.Add(f(x)) }
return r
}
Solution 20¶
type OrderState int
const ( Draft OrderState = iota; Submitted; Paid )
type OrderItem struct{ ProductID string; Qty int; Price Money }
type Order struct {
ID string
items []OrderItem
state OrderState
events []Event
}
func (o *Order) AddItem(p Product, qty int) error {
if o.state != Draft { return ErrFrozen }
if qty <= 0 { return ErrInvalidQty }
o.items = append(o.items, OrderItem{ProductID: p.ID, Price: p.Price, Qty: qty})
o.events = append(o.events, ItemAdded{OrderID: o.ID, ProductID: p.ID})
return nil
}
func (o *Order) Submit() error {
if o.state != Draft { return ErrInvalidState }
if len(o.items) == 0 { return ErrEmptyOrder }
o.state = Submitted
o.events = append(o.events, OrderSubmitted{OrderID: o.ID})
return nil
}
func (o *Order) MarkPaid() error {
if o.state != Submitted { return ErrInvalidState }
o.state = Paid
o.events = append(o.events, OrderPaid{OrderID: o.ID})
return nil
}
func (o *Order) PullEvents() []Event {
events := o.events
o.events = nil
return events
}
Solution 21 (abridged)¶
type Plugin struct{ name string }
func (p Plugin) Hello() string { return "hi from " + p.name }
func (p Plugin) Status() string { return "OK" }
type PluginMethod = func(Plugin) string
var registry = map[string]PluginMethod{
"hello": Plugin.Hello,
"status": Plugin.Status,
}
func Invoke(p Plugin, method string) string {
if fn, ok := registry[method]; ok { return fn(p) }
return "unknown method"
}
Solution 22¶
type DataService struct{ db *sql.DB }
func (s *DataService) Fetch(ctx context.Context, id string) (*Data, error) {
select {
case <-ctx.Done(): return nil, ctx.Err()
default:
}
return s.db.QueryContext(ctx, "SELECT...", id) // ...
}
func (s *DataService) Save(ctx context.Context, d *Data) error {
_, err := s.db.ExecContext(ctx, "INSERT...", d.Field)
return err
}