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
- Structs: classes reimagined
- Methods and receivers
- Pointer vs value receivers
- Struct embedding: reuse without inheritance
- Constructor patterns
- 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:
NewXxxis 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
SpeakonDogshadowsAnimal.SpeakforDogvalues
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
-
NewXxxconstructors - Embedding for reuse
- Three exercises done
C++ → Go
| C++ | Go |
|---|---|
class | struct + methods |
| ctor/dtor | NewXxx / defer |
this | explicit receiver |
| inheritance | embedding + interfaces |
Next
Interfaces: polymorphism without virtual.
📚 Series navigation
| Previous | Index | Next |
|---|---|---|
| ← #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.
Related reading
- [Go #02] Memory & data structures
- Two-week Go curriculum
- C++ inheritance & polymorphism
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.
Related posts
- Two-week Go curriculum
- C++ vs Go
- C++ dev’s view of Go
- [Go #01] Philosophy & syntax
- [Go #02] Memory & data structures