Go in 2 Weeks #03 | Day 5–6: OOP Without Classes — Prefer Composition Over Inheritance

Go in 2 Weeks #03 | Day 5–6: OOP Without Classes — Prefer Composition Over Inheritance

이 글의 핵심

Go has no class keyword. Learn struct, methods, pointer vs value receivers, and embedding for reuse—contrasted with C++ classes and inheritance.

Series overview

📚 Go in 2 Weeks #03 | Full series index

This post covers Days 5–6 of the two-week Go curriculum for C++ developers.

Previous: #02 Memory & data structures ← | → Next: #04 Interfaces


Introduction: OOP without classes

In C++, class bundles data and behavior; inheritance reuse is common. Go has no classes and no inheritance. Instead, structs and composition keep models simple and flexible. “Favor composition over inheritance” is not just a slogan—it is how Go is meant to be written.

You will learn:

  • Defining data with struct
  • Methods and receivers
  • Pointer vs value receivers
  • Struct embedding for reuse

Real-world notes

Moving from C++ to Go

I spent over a decade on C++ servers. Go felt trivially small at first—until production taught me that small is a feature: faster iteration, GC safety, easy deploys.


Table of contents

  1. Structs: classes reimagined
  2. Methods and receivers
  3. Pointer vs value receivers
  4. Struct embedding: reuse without inheritance
  5. Constructor patterns
  6. Exercises

1. Structs: classes reimagined

C++ vs Go

class Person {
private:
    std::string name;
    int age;
public:
    Person(const std::string& n, int a) : name(n), age(a) {}
    void SetName(const std::string& n) { name = n; }
    std::string GetName() const { return name; }
    void SetAge(int a) { age = a; }
    int GetAge() const { return age; }
    void Introduce() const {
        std::cout << "I'm " << name << ", " << age << " years old\n";
    }
};

Person p("Alice", 30);
p.Introduce();
package main

import "fmt"

type Person struct {
    Name string  // exported (capitalized)
    age  int     // package-private
}

func NewPerson(name string, age int) *Person {
    return &Person{Name: name, age: age}
}

func (p *Person) Age() int { return p.age }

func (p *Person) SetAge(age int) { p.age = age }

func (p *Person) Introduce() {
    fmt.Printf("I'm %s, %d years old\n", p.Name, p.age)
}

func main() {
    p := NewPerson("Alice", 30)
    p.Introduce()
}

Differences:

  • Visibility: capitalized = exported; lowercase = package-private
  • Constructors: NewXxx is a convention, not syntax
  • Methods: defined outside the type with a receiver

Struct initialization

package main

type Point struct {
    X, Y int
}

func main() {
    p1 := Point{10, 20}
    p2 := Point{X: 10, Y: 20}
    p3 := Point{X: 10}
    p4 := Point{}
    p5 := &Point{X: 10, Y: 20}
    p6 := new(Point)
    _ = p1; _ = p2; _ = p3; _ = p4; _ = p5; _ = p6
}

2. Methods and receivers

Go methods are functions with a receiver—explicit, unlike hidden this.

C++ vs Go

class Counter {
    int count = 0;
public:
    void Increment() { count++; }
    int GetCount() const { return count; }
};
package main

type Counter struct{ count int }

func NewCounter() *Counter { return &Counter{count: 0} }

func (c *Counter) Increment() { c.count++ }

func (c *Counter) GetCount() int { return c.count }

Syntax: func (receiver Type) MethodName() { }


3. Pointer vs value receivers

Value receiver

package main

import (
    "fmt"
    "math"
)

type Point struct{ X, Y int }

func (p Point) Distance() float64 {
    return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}

func (p Point) Move(dx, dy int) {
    p.X += dx  // does NOT mutate original
    p.Y += dy
}

func main() {
    p := Point{3, 4}
    fmt.Println(p.Distance())
    p.Move(10, 10)
    fmt.Println(p)  // {3 4}
}

Pointer receiver

package main

import "fmt"

type Point struct{ X, Y int }

func (p *Point) Move(dx, dy int) {
    p.X += dx
    p.Y += dy
}

func (p *Point) Reset() {
    p.X, p.Y = 0, 0
}

func main() {
    p := Point{3, 4}
    p.Move(10, 10)  // Go passes &p automatically
    fmt.Println(p)

    p2 := &Point{1, 2}
    p2.Move(5, 5)
    fmt.Println(*p2)
}

Choosing receivers

flowchart TD
    A[Define method] --> B{Mutate fields?}
    B -->|Yes| C["Pointer receiver *T"]
    B -->|No| D{Large struct?}
    D -->|Yes ~64B+| C
    D -->|Small| E{Consistency}
    E -->|Other methods use *T| C
    E -->|All value receivers| F["Value receiver T"]

Rules of thumb:

  • Pointer: mutating methods, large structs, or consistency with other methods on the type
  • Value: small immutable reads, or when copying is cheap and clarity benefits

4. Struct embedding: reuse without inheritance

C++ inheritance vs Go embedding

class Animal {
protected:
    std::string name;
public:
    Animal(const std::string& n) : name(n) {}
    virtual void Speak() { std::cout << "Some sound\n"; }
    void Eat() { std::cout << name << " is eating\n"; }
};

class Dog : public Animal {
public:
    Dog(const std::string& n) : Animal(n) {}
    void Speak() override { std::cout << "Woof!\n"; }
    void Fetch() { std::cout << name << " is fetching\n"; }
};

Dog d("Buddy");
d.Speak();
d.Eat();
d.Fetch();
package main

import "fmt"

type Animal struct{ Name string }

func (a Animal) Speak() { fmt.Println("Some sound") }
func (a Animal) Eat()  { fmt.Printf("%s is eating\n", a.Name) }

type Dog struct {
    Animal
    Breed string
}

func (d Dog) Fetch() {
    fmt.Printf("%s is fetching\n", d.Name)
}

func (d Dog) Speak() {
    fmt.Println("Woof!")
}

func main() {
    d := Dog{
        Animal: Animal{Name: "Buddy"},
        Breed:  "Golden Retriever",
    }
    d.Speak()
    d.Eat()
    d.Fetch()
}

Differences:

  • No inheritance in Go
  • Embedding promotes fields and methods of the inner type
  • Defining Speak on Dog shadows Animal.Speak for Dog values

How embedding works

package main

import "fmt"

type Base struct{ Value int }

func (b Base) Print() { fmt.Println("Base:", b.Value) }

type Derived struct {
    Base
    Extra string
}

func main() {
    d := Derived{Base: Base{Value: 42}, Extra: "extra"}
    d.Print()
    fmt.Println(d.Value)
    d.Base.Print()
}

Multiple embeds

package main

import "fmt"

type Logger struct{}

func (Logger) Log(msg string) { fmt.Println("[LOG]", msg) }

type Validator struct{}

func (Validator) Validate(data string) bool { return data != "" }

type Service struct {
    Logger
    Validator
    name string
}

func (s *Service) Process(data string) {
    s.Log("Processing started")
    if !s.Validate(data) {
        s.Log("Invalid data")
        return
    }
    s.Log("Processing completed")
}

5. Constructor patterns

There is no constructor keyword—NewXxx returning *T is idiomatic.

Functional options (advanced)

package main

import "time"

type Server struct {
    host    string
    port    int
    timeout time.Duration
    maxConn int
}

type Option func(*Server)

func WithHost(host string) Option {
    return func(s *Server) { s.host = host }
}

func WithPort(port int) Option {
    return func(s *Server) { s.port = port }
}

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) { s.timeout = timeout }
}

func NewServer(opts ...Option) *Server {
    s := &Server{
        host:    "localhost",
        port:    8080,
        timeout: 30 * time.Second,
        maxConn: 100,
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

6. Exercises

Exercise 1: Rectangle

package main

import "fmt"

type Rectangle struct{ Width, Height float64 }

func NewRectangle(w, h float64) *Rectangle {
    return &Rectangle{Width: w, Height: h}
}

func (r *Rectangle) Area() float64      { return r.Width * r.Height }
func (r *Rectangle) Perimeter() float64  { return 2 * (r.Width + r.Height) }
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func main() {
    rect := NewRectangle(10, 5)
    fmt.Printf("Area: %.2f\n", rect.Area())
    rect.Scale(2)
    fmt.Printf("Area: %.2f\n", rect.Area())
}

Exercise 2: Stack

package main

import (
    "errors"
    "fmt"
)

type Stack struct{ items []int }

func NewStack() *Stack {
    return &Stack{items: make([]int, 0)}
}

func (s *Stack) Push(item int) { s.items = append(s.items, item) }

func (s *Stack) Pop() (int, error) {
    if len(s.items) == 0 {
        return 0, errors.New("stack is empty")
    }
    i := len(s.items) - 1
    v := s.items[i]
    s.items = s.items[:i]
    return v, nil
}

func (s *Stack) Peek() (int, error) {
    if len(s.items) == 0 {
        return 0, errors.New("stack is empty")
    }
    return s.items[len(s.items)-1], nil
}

Exercise 3: Embedding

package main

import (
    "fmt"
    "time"
)

type Timer struct{ start time.Time }

func (t *Timer) Start()                { t.start = time.Now() }
func (t *Timer) Elapsed() time.Duration { return time.Since(t.start) }

type Logger struct{ prefix string }

func (l Logger) Log(msg string) {
    fmt.Printf("[%s] %s\n", l.prefix, msg)
}

type LoggedTimer struct {
    Timer
    Logger
}

func NewLoggedTimer(prefix string) *LoggedTimer {
    return &LoggedTimer{Logger: Logger{prefix: prefix}}
}

func (lt *LoggedTimer) Start() {
    lt.Timer.Start()
    lt.Log("Timer started")
}

func (lt *LoggedTimer) Stop() time.Duration {
    e := lt.Elapsed()
    lt.Log(fmt.Sprintf("Timer stopped: %v", e))
    return e
}

Wrap-up: Days 5–6 checklist

  • Define types with struct
  • Package-level visibility via naming
  • Methods and receiver syntax
  • Pointer vs value receivers
  • NewXxx constructors
  • Embedding for reuse
  • Three exercises done

C++ → Go

C++Go
classstruct + methods
ctor/dtorNewXxx / defer
thisexplicit receiver
inheritanceembedding + interfaces

Next

Interfaces: polymorphism without virtual.


📚 Series navigation

PreviousIndexNext
← #02 Data structures📑 Index#04 Interfaces →

Go in 2 weeks: Curriculum#01#02#03#04#05#06 • …


TL;DR: No classes—use structs, methods, and composition; embedding gives reuse without inheritance.


Keywords

Go struct, pointer receiver, embedding, Go OOP, Golang composition, Go tutorial.

Practical tips

Debugging

  • Fix compiler warnings first; reproduce minimally.

Performance

  • Profile before micro-optimizing.

Code review

  • Match team style; check edge cases.

Field checklist

Before coding

  • Right abstraction?
  • Maintainable by the team?
  • Performance budget OK?

While coding

  • Warnings cleared?
  • Edge cases?
  • Errors handled?

At review

  • Clear intent?
  • Tests?
  • Docs?

FAQ

Q. Practical use?

A. Modeling services and domain types without C++-style class hierarchies—receivers and embedding cover most real patterns.

Q. Prerequisites?

A. Follow series order via nav links or C++ series index.

Q. Deeper study?

A. Official Go docs and cppreference for C++ parallels.