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, 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
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
implementskeyword - 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.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
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 |
|---|---|
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
- [Go #03] OOP without classes
- Two-week Go curriculum
- C++ virtual functions
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.
Related posts
- C++ dev’s view of Go
- Two-week Go curriculum
- C++ vs Go
- [Go #01] Philosophy & syntax
- [Go #02] Memory & data structures