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

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

이 글의 핵심

No implements keyword: satisfy interfaces by methods alone. Compare duck typing to C++ virtuals; cover io.Reader, Writer, any, and type assertions.

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

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";
}
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

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

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

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

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

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

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.

// 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

// 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

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

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

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

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.