[2026] Go in 2 Weeks #04 | Day 7: Polymorphism Reimagined — Interfaces Without virtual

[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, and error
  • 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

  1. Interface basics: method sets
  2. Implicit interface satisfaction (duck typing)
  3. Standard-library interfaces
  4. Empty interface and type assertions
  5. Practical patterns: implicit impl, any, I/O, errors
  6. Interface design principles
  7. 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 implements keyword
  • 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.MultiWriter fans out one Write to many writers.
  • Custom errors implement Error() string; use errors.As at 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
virtualinterface methods
explicit inheritanceimplicit satisfaction
base pointerinterface value
dynamic_casttype assertion

End of week one

You now have syntax, data structures, methods, and interfaces. Week two focuses on concurrency.

📚 Series navigation

PreviousIndexNext
← #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.


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.

Q. Go deeper?

A. go.dev and cppreference.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3