[2026] Go in 2 Weeks #04 | Day 7: Polymorphism Reimagined — Interfaces Without virtual
이 글의 핵심
Go interfaces vs C++ virtual functions: implicit satisfaction, duck typing, io.Reader, io.Writer, small interfaces, any, type assertions, and type switches. SEO: Go interface tutorial, golang polymorphism.
Series overview
📚 Go in 2 Weeks #04 | Full series index
This post covers Day 7 of the two-week Go curriculum for C++ developers.
Previous: #03 OOP & composition ← | → Next: #05 Error handling
Introduction: from explicit inheritance to implicit satisfaction
In C++, polymorphism usually means inheriting a base class and marking overrides virtual. Go has no inheritance declaration: implement the methods and you satisfy the interface—duck typing (“if it walks and quacks like a duck…”).
You will learn:
- Interface definition and implicit satisfaction
- Key standard-library interfaces
- Empty interface, type assertions, type switches
- Practical patterns with
io.Reader,io.Writer, anderror - Interface design guidelines
Real-world notes
Moving from C++ to Go
Ten-plus years of C++ servers taught me that Go’s simplicity pays off in delivery speed, GC safety, and single-binary deploys.
Table of contents
- Interface basics: method sets
- Implicit interface satisfaction (duck typing)
- Standard-library interfaces
- Empty interface and type assertions
- Practical patterns: implicit impl, any, I/O, errors
- Interface design principles
- Exercises
1. Interface basics: method sets
C++ vs Go: polymorphism
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Shape {
public:
virtual double Area() const = 0;
virtual double Perimeter() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double Area() const override {
return 3.14159 * radius * radius;
}
double Perimeter() const override {
return 2 * 3.14159 * radius;
}
};
void printShapeInfo(const Shape& s) {
std::cout << "Area: " << s.Area() << "\n";
}
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 패키지 선언
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
type Rectangle struct{ Width, Height float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func printShapeInfo(s Shape) {
fmt.Printf("Area: %.2f\n", s.Area())
fmt.Printf("Perimeter: %.2f\n", s.Perimeter())
}
Key points:
- No
implementskeyword - Implement the methods → automatically satisfy
Shape - Lower coupling between concrete types and interfaces
2. Implicit interface satisfaction
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import "fmt"
type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof!" }
type Cat struct{ Name string }
func (c Cat) Speak() string { return "Meow!" }
type Speaker interface{ Speak() string }
func makeSpeak(s Speaker) { fmt.Println(s.Speak()) }
func main() {
makeSpeak(Dog{Name: "Buddy"})
makeSpeak(Cat{Name: "Whiskers"})
}
Compared to C++: base classes must be designed up front; in Go you can introduce Speaker later without editing Dog/Cat.
3. Standard-library interfaces
io.Reader and io.Writer
아래 코드는 go를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Example: one processData accepts any io.Reader.
fmt.Stringer
아래 코드는 go를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
type Person struct{ Name string; Age int }
func (p Person) String() string {
return fmt.Sprintf("%s (%d)", p.Name, p.Age)
}
error
type error interface {
Error() string
}
4. Empty interface and type assertions
interface{} and any
func printAny(v interface{}) {
fmt.Printf("Value: %v, Type: %T\n", v, v)
}
Type assertion
아래 코드는 go를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
func process(v interface{}) {
if s, ok := v.(string); ok {
fmt.Println("String:", s)
return
}
if i, ok := v.(int); ok {
fmt.Println("Int:", i)
return
}
fmt.Println("Unknown type")
}
Type switch
아래 코드는 go를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
func describe(v interface{}) {
switch t := v.(type) {
case string:
fmt.Printf("String: %s (len %d)\n", t, len(t))
case int:
fmt.Printf("Int: %d\n", t)
default:
fmt.Printf("Unknown: %T\n", t)
}
}
5. Practical patterns
- Define interfaces at the call site (“this function only needs
io.Reader”). - Implementations need not import the interface—great for tests and adapters.
- “Accept interfaces, return structs”: public APIs take narrow interfaces; constructors return concrete
*T.
any vs assertions
Prefer any (Go 1.18+) over interface{}. After storing any, narrow with assertions or type switches—JSON decoding is a classic use case.
Nil interface gotchas
See #05 for the “typed nil in interface” trap.
Wiring Reader, Writer, error
io.Copy(dst, src)connects streams.io.MultiWriterfans out oneWriteto many writers.- Custom errors implement
Error() string; useerrors.Asat the boundary (#05).
r := strings.NewReader("hello")
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
TL;DR: model I/O with io.Reader/Writer, failures with error, and use any only when runtime types vary.
6. Interface design principles
Small interfaces
아래 코드는 go를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
type Reader interface {
Read(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
Avoid “god” interfaces with dozens of methods—hard to implement and test.
Interface segregation
Split large surface areas into Querier, Inserter, Transactional, etc., and depend only on what you need.
아래 코드는 go를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// Prefer small interfaces
type Querier interface {
Query(sql string) ([]Row, error)
}
func fetchData(q Querier) ([]Row, error) {
return q.Query("SELECT * FROM users")
}
Anti-pattern: oversized interface
아래 코드는 go를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// Hard to implement and mock — split by role instead
type Database interface {
Connect() error
Disconnect() error
Query(sql string) ([]Row, error)
Insert(table string, data map[string]interface{}) error
Update(table string, id int, data map[string]interface{}) error
Delete(table string, id int) error
BeginTransaction() error
CommitTransaction() error
RollbackTransaction() error
}
7. Exercises
Exercise 1: shapes
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
type Rectangle struct{ Width, Height float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
type Triangle struct{ A, B, C float64 }
func (t Triangle) Area() float64 {
s := (t.A + t.B + t.C) / 2
return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}
func (t Triangle) Perimeter() float64 { return t.A + t.B + t.C }
func printShapeInfo(s Shape) {
fmt.Printf("Type: %T\n", s)
fmt.Printf("Area: %.2f\n", s.Area())
fmt.Printf("Perimeter: %.2f\n", s.Perimeter())
fmt.Println()
}
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 6},
Triangle{A: 3, B: 4, C: 5},
}
for _, shape := range shapes {
printShapeInfo(shape)
}
}
Exercise 2: UpperWriter
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import (
"bytes"
"fmt"
"io"
"os"
)
type UpperWriter struct{ w io.Writer }
func NewUpperWriter(w io.Writer) *UpperWriter { return &UpperWriter{w: w} }
func (uw *UpperWriter) Write(p []byte) (n int, err error) {
return uw.w.Write(bytes.ToUpper(p))
}
func main() {
uw := NewUpperWriter(os.Stdout)
fmt.Fprintln(uw, "hello world")
io.WriteString(uw, "go is awesome\n")
}
Exercise 3: combining interfaces
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import (
"fmt"
"io"
"os"
)
func processReadWriteCloser(rwc io.ReadWriteCloser) {
defer rwc.Close()
_, _ = rwc.Write([]byte("test data"))
}
func main() {
f, err := os.Create("temp.txt")
if err != nil {
panic(err)
}
processReadWriteCloser(f)
}
Exercise 4: JSON and type switch
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import (
"encoding/json"
"fmt"
)
func parseJSON(jsonStr string) {
var data interface{}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
fmt.Println("Parse error:", err)
return
}
switch v := data.(type) {
case map[string]interface{}:
fmt.Println("Object:")
for key, value := range v {
fmt.Printf(" %s: %v\n", key, value)
}
case []interface{}:
fmt.Println("Array:")
for i, item := range v {
fmt.Printf(" [%d]: %v\n", i, item)
}
default:
fmt.Printf("Other type: %T\n", v)
}
}
func main() {
parseJSON(`{"name":"Alice","age":30}`)
parseJSON(`[1, 2, 3, 4, 5]`)
}
Wrap-up: Day 7 checklist
- Interfaces are method sets
- Implicit satisfaction—no
implements - Know
io.Reader,io.Writer,fmt.Stringer -
any, assertions, type switches - Prefer small interfaces
- Four exercises
C++ → Go
| C++ | Go |
|---|---|
virtual | interface methods |
| explicit inheritance | implicit satisfaction |
| base pointer | interface value |
dynamic_cast | type assertion |
End of week one
You now have syntax, data structures, methods, and interfaces. Week two focuses on concurrency.
📚 Series navigation
| Previous | Index | Next |
|---|---|---|
| ← #03 OOP | 📑 Index | #05 Errors → |
| Go in 2 weeks: | ||
| Curriculum • #01 • … • #06 • … |
TL;DR: Implement methods; satisfy interfaces automatically. Keep interfaces small for maximum reuse.
Related reading
Keywords
Go interface, duck typing, io.Reader, type assertion, any, Golang polymorphism, Go tutorial.
Practical tips
Debugging
- Compiler warnings first; minimal repro.
Performance
- Profile before tuning.
Code review
- Team conventions; clarity over cleverness.
Field checklist
Before coding
- Right abstraction?
- Team can maintain?
- Meets perf goals?
While coding
- Warnings fixed?
- Edge cases?
- Errors handled?
At review
- Intent obvious?
- Tests?
- Docs?
FAQ
Q. Where is this used?
A. Anywhere you’d use virtuals in C++—pluggable I/O, test doubles, and library boundaries—without inheritance trees.
Q. Reading order?
A. Use Previous/Next links or C++ series index.