본문으로 건너뛰기 Go Interfaces Complete Guide — Polymorphism Without virtual,

Go Interfaces Complete Guide — Polymorphism Without virtual,

Go Interfaces Complete Guide — Polymorphism Without virtual,

이 글의 핵심

How to implement polymorphism with Go interfaces instead of C++ virtual functions and inheritance: implicit interfaces (no implements), duck typing, and small interfaces like io.Reader and io.Writer.

Series overview

📚 Go in 2 Weeks #04 | Full series index

This article is 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 declaring virtual functions with virtual. Go has no explicit inheritance declaration. If you implement the methods, you satisfy the interface automatically. That is the duck-typing mindset: “If it walks like a duck and quacks like a duck, it is a duck.” You will learn:

  • What interfaces are and how they are satisfied implicitly
  • Core interfaces from the standard library
  • The empty interface and type assertions · type switches
  • Practical patterns combining io.Reader, io.Writer, and error
  • Interface design patterns

From a C++ developer’s perspective: This post emphasizes differences and pitfalls when moving from a C++ background to Go. It compares pointers, concurrency, and memory management where it helps.

How it feels in practice

When teams that mainly used C++ for servers adopt Go, a common first impression is that syntax and the toolchain look simpler. In production, that simplicity often translates into faster builds and deploys and more readable concurrency code. Commonly cited strengths:

  • Development speed: Varies by team and domain, but network and CLI code is often quick to ship.
  • Safety: A GC reduces manual deallocation burden.
  • Deployment: A single binary is easy to move around.

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 implementation, any, assertions, standard I/O, errors
  6. Interface design principles
  7. Hands-on exercises

1. Interface basics: method sets

C++ vs Go: implementing polymorphism

// C++: polymorphism with virtual functions
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;
    }
};
class Rectangle : public Shape {
    double width, height;
    
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double Area() const override {
        return width * height;
    }
    
    double Perimeter() const override {
        return 2 * (width + height);
    }
};
// Using polymorphism
void printShapeInfo(const Shape& s) {
    std::cout << "Area: " << s.Area() << "\n";
    std::cout << "Perimeter: " << s.Perimeter() << "\n";
}
int main() {
    Circle c(5.0);
    Rectangle r(4.0, 6.0);
    
    printShapeInfo(c);
    printShapeInfo(r);
}
// Go: polymorphism with interfaces (no explicit inheritance)
package main
import (
    "fmt"
    "math"
)
// Interface definition
type Shape interface {
    Area() float64
    Perimeter() float64
}
// Circle type
type Circle struct {
    Radius float64
}
// Circle implements Area() and Perimeter(), so it satisfies Shape automatically
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}
// Rectangle type
type Rectangle struct {
    Width, Height float64
}
// Rectangle also implements Area() and Perimeter(), so it satisfies Shape
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}
// Using polymorphism
func printShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
    fmt.Printf("Perimeter: %.2f\n", s.Perimeter())
}
func main() {
    c := Circle{Radius: 5.0}
    r := Rectangle{Width: 4.0, Height: 6.0}
    
    printShapeInfo(c)
    printShapeInfo(r)
}

Key differences:

  • Go has no implements keyword
  • Implement the methods and you automatically satisfy the interface
  • Looser coupling between concrete types and interfaces

2. Implicit interface satisfaction (duck typing)

Why duck typing helps

// Go: you can introduce an interface later
package main
import "fmt"
// Existing types (they do not know about any interface yet)
type Dog struct {
    Name string
}
func (d Dog) Speak() string {
    return "Woof!"
}
type Cat struct {
    Name string
}
func (c Cat) Speak() string {
    return "Meow!"
}
// Define the interface later (no change to existing types required)
type Speaker interface {
    Speak() string
}
func makeSpeak(s Speaker) {
    fmt.Println(s.Speak())
}
func main() {
    dog := Dog{Name: "Buddy"}
    cat := Cat{Name: "Whiskers"}
    
    // Dog and Cat satisfy Speaker (no explicit declaration)
    makeSpeak(dog)
    makeSpeak(cat)
}

Compared to C++:

  • C++: you typically design base classes up front and inherit from them
  • Go: you can add an interface later without editing existing types

3. Standard-library interfaces

The Go standard library is full of small, powerful interfaces.

io.Reader and io.Writer

// Go: io.Reader interface
type Reader interface {
    Read(p []byte) (n int, err error)
}
// io.Writer interface
type Writer interface {
    Write(p []byte) (n int, err error)
}

Example usage:

// Go: using io.Reader
package main
import (
    "bytes"
    "fmt"
    "io"
    "os"
    "strings"
)
func processData(r io.Reader) error {
    data, err := io.ReadAll(r)
    if err != nil {
        return err
    }
    fmt.Println(string(data))
    return nil
}
func main() {
    // 1. Read from a file
    f, _ := os.Open("file.txt")
    defer f.Close()
    processData(f) // *os.File implements io.Reader
    
    // 2. Read from a string
    sr := strings.NewReader("Hello from string")
    processData(sr) // *strings.Reader implements io.Reader
    
    // 3. Read from a byte buffer
    buf := bytes.NewBufferString("Hello from buffer")
    processData(buf) // *bytes.Buffer implements io.Reader
}

fmt.Stringer

// C++: operator<< overloading
class Person {
    std::string name;
    int age;
    
public:
    Person(const std::string& n, int a) : name(n), age(a) {}
    
    friend std::ostream& operator<<(std::ostream& os, const Person& p) {
        os << p.name << " (" << p.age << ")";
        return os;
    }
};
// Usage
Person p("Alice", 30);
std::cout << p << "\n";
// Go: fmt.Stringer interface
package main
import "fmt"
type Person struct {
    Name string
    Age  int
}
// Satisfies fmt.Stringer
func (p Person) String() string {
    return fmt.Sprintf("%s (%d)", p.Name, p.Age)
}
func main() {
    p := Person{Name: "Alice", Age: 30}
    fmt.Println(p) // "Alice (30)" — String() is called automatically
}

error interface

// Go: the built-in error interface is: type error interface { Error() string }
package main

import (
    "fmt"
    "strings"
)

// Custom error type
type ValidationError struct {
    Field string
    Value string
}
func (e ValidationError) Error() string {
    return fmt.Sprintf("validation failed: %s = %s", e.Field, e.Value)
}
// Usage
func validate(email string) error {
    if !strings.Contains(email, "@") {
        return ValidationError{
            Field: "email",
            Value: email,
        }
    }
    return nil
}
func main() {
    if err := validate("invalid"); err != nil {
        fmt.Println(err) // "validation failed: email = invalid"
    }
}

4. Empty interface and type assertions

interface{} (any)

// C++: void* (not type-safe)
void* ptr = new int(42);
int* p = static_cast<int*>(ptr);  // manual cast
// Go: interface{} or any (type-safe when narrowed)
package main
import "fmt"
func printAny(v interface{}) {
    fmt.Printf("Value: %v, Type: %T\n", v, v)
}
func main() {
    printAny(42)
    printAny("hello")
    printAny(3.14)
    printAny([]int{1, 2, 3})
}

Type assertions

// Go: type assertions
package main
import "fmt"
func process(v interface{}) {
    // Safe assertion (ok idiom)
    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")
}
func main() {
    process("hello")  // "String: hello"
    process(42)       // "Int: 42"
    process(3.14)     // "Unknown type"
}

Type switches

// Go: type switch (handle many types)
package main
import "fmt"
func describe(v interface{}) {
    switch t := v.(type) {
    case string:
        fmt.Printf("String: %s (length %d)\n", t, len(t))
    case int:
        fmt.Printf("Int: %d\n", t)
    case bool:
        fmt.Printf("Bool: %t\n", t)
    case []int:
        fmt.Printf("Int slice: %v\n", t)
    default:
        fmt.Printf("Unknown type: %T\n", t)
    }
}
func main() {
    describe("hello")
    describe(42)
    describe(true)
    describe([]int{1, 2, 3})
    describe(3.14)
}

5. Practical patterns: implicit implementation, any, assertions, standard I/O, errors

Using implicit implementation in real code

  • Interfaces are usually defined by the consumer. Say “this function only needs an io.Reader” and only the required methods matter—any type that has them can be passed in.
  • Concrete types do not need to know the interface name. That makes it easy to extend with test doubles or adapters without editing existing types.
  • A common rule of thumb: accept interfaces, return structs—take narrow interfaces at public boundaries; constructors return concrete pointers.
// HTTP handler example: ResponseWriter and Request combine interfaces and structs
func Handler(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "ok") // w satisfies io.Writer
}

interface{} and any

  • Before Go 1.18, the empty interface was written interface{}; since then any is an alias for interface{}. Style guides usually prefer any.
  • Every type is assignable to any, but you lose compile-time checking, so you narrow with assertions or type switches afterward.
  • Typical for data whose shape is only known at runtime, e.g. JSON decoding (see Exercise 4).

Type assertions and type switches — checklist

SituationRecommendation
Only one or two concrete typesv.(ConcreteType) or s, ok := v.(string)
Many types in one functionswitch v := x.(type) { ... }
nil interface valuesAssertions can fail or surprise—see the “nil interface” trap in #05
// Use the ok form to avoid panic on failed assertion
if n, ok := v.(int); ok {
    _ = n
}

Composing io.Reader, io.Writer, and error

The standard library builds large behavior from very small interfaces.

  • io.Copy(dst Writer, src Reader): connects read and write streams—files, buffers, network, bytes.Reader all look the same.
  • io.MultiWriter: one Write can fan out to several Writers (e.g. file + hash).
  • error: a single method Error() string, so any custom type can be an error. Carry domain fields and use errors.As at the boundary (#05).
package main
import (
    "bytes"
    "fmt"
    "io"
    "strings"
)
func main() {
    r := strings.NewReader("hello")
    var buf bytes.Buffer
    if _, err := io.Copy(&buf, r); err != nil {
        fmt.Println(err)
        return
    }
    // buf implements io.Writer — same pattern for files, hashes, etc.
    fmt.Println(buf.String())
}

One-line summary: model I/O with io.Reader/io.Writer, failures with error, and use any plus narrowing only when types truly vary at runtime.


6. Interface design principles

Small interfaces

Go’s philosophy: the smaller the interface, the better

// Good: small interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}
// Compose when needed
type ReadWriter interface {
    Reader
    Writer
}
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}
// Bad: a large “do everything” interface
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
}
// Too many methods — hard to implement and test

Interface segregation

// Good: segregated interfaces
type Querier interface {
    Query(sql string) ([]Row, error)
}
type Inserter interface {
    Insert(table string, data map[string]interface{}) error
}
type Transactional interface {
    BeginTransaction() error
    CommitTransaction() error
    RollbackTransaction() error
}
// Require only what you need
func fetchData(q Querier) ([]Row, error) {
    return q.Query("SELECT * FROM users")
}

7. Hands-on exercises

Exercise 1: Shape interface

// Go: multiple 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 // side lengths
}
func (t Triangle) Area() float64 {
    // Heron's formula
    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: Custom Writer

// Go: custom Writer
package main
import (
    "bytes"
    "fmt"
    "io"
    "os"
)
// Writer that uppercases output
type UpperWriter struct {
    w io.Writer
}
func NewUpperWriter(w io.Writer) *UpperWriter {
    return &UpperWriter{w: w}
}
// Satisfies io.Writer
func (uw *UpperWriter) Write(p []byte) (n int, err error) {
    upper := bytes.ToUpper(p)
    return uw.w.Write(upper)
}
func main() {
    // Uppercase stdout
    uw := NewUpperWriter(os.Stdout)
    
    fmt.Fprintln(uw, "hello world")  // "HELLO WORLD"
    
    // Composable with io.Copy and friends
    io.WriteString(uw, "go is awesome\n")  // "GO IS AWESOME"
}

Exercise 3: Combining interfaces

// Go: combining several interfaces
package main
import (
    "fmt"
    "io"
    "os"
)
// Type that satisfies multiple interfaces via embedding
type File struct {
    *os.File
}
// File satisfies io.Reader, io.Writer, io.Closer
// (os.File already does; embedding forwards the methods)
func processReadWriteCloser(rwc io.ReadWriteCloser) {
    // Read, Write, Close available
    defer rwc.Close()
    
    data := []byte("test data")
    rwc.Write(data)
}
func main() {
    f, err := os.Create("temp.txt")
    if err != nil {
        panic(err)
    }
    
    // *os.File satisfies io.ReadWriteCloser
    processReadWriteCloser(f)
}

Exercise 4: Type assertions with JSON

// Go: JSON parsing with type assertions
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
    }
    
    // Type switch
    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

Completion checklist

  • Interfaces are sets of methods
  • No explicit implements — automatic satisfaction (duck typing)
  • Use standard interfaces such as io.Reader, io.Writer, fmt.Stringer
  • Empty interface (interface{}, any) and type assertions
  • Type switches for multiple types
  • Design small interfaces
  • Finish all four exercises

C++ to Go transition

C++GoNotes
virtual functionsinterface methodsexplicit vs implicit
Explicit inheritanceimplicit satisfactionduck typing
Base-class pointerinterface valuemore flexible
RTTI (dynamic_cast)type assertionmore concise
Multiple inheritanceinterface compositionsafer

End of week one

Congratulations—you have finished week one. So far you have covered:

  • ✅ Go syntax and philosophy
  • ✅ Pointers and data structures (slice, map)
  • ✅ Structs and methods
  • ✅ Interfaces and polymorphism In week two you will learn concurrent programming, one of Go’s main strengths.

📚 Series navigation

PreviousIndexNext
← #03 OOP📑 Full index#05 Errors →

Go in 2 weeks: Curriculum#01 Basics#02 Data structures#03 OOP#04 Interfaces#05 Errors#06 Goroutines & channels#07 Testing#08 REST API#09 Context & graceful shutdown


TL;DR: Go interfaces are satisfied automatically when you implement the methods—no explicit inheritance. Prefer small interfaces for reuse.

These posts connect to this topic.

  • [Go in 2 Weeks #03] Days 5–6: OOP without classes — favor composition over inheritance
  • Two-week Go curriculum for C++ developers
  • C++ virtual functions guide

Go interface, duck typing, io.Reader, type assertion, any, Go polymorphism, C++ virtual functions compared, Golang interfaces, Go in 2 weeks, and similar queries should surface this article.

Practical tips

Tips you can apply immediately.

Debugging

  • Check compiler warnings first
  • Reproduce with a small test case

Performance

  • Do not optimize without profiling
  • Define measurable goals first

Code review

  • Check common review feedback early
  • Follow team conventions

Field checklist

Use this when applying these ideas in production.

Before you code

  • Is this the best fit for the problem?
  • Can teammates understand and maintain it?
  • Does it meet performance requirements?

While coding

  • Are all compiler warnings addressed?
  • Did you consider edge cases?
  • Is error handling appropriate?

At review

  • Is intent clear?
  • Are tests sufficient?
  • Is documentation in place?

Use this checklist to reduce mistakes and improve quality.


FAQ

Q. Where do I use this in production?

A. Anywhere you would use virtual functions and inheritance in C++—pluggable I/O, test doubles, library boundaries—with Go’s implicit interfaces and small interface design. Apply the examples and decision guides from the main text.

Q. What should I read first?

A. Follow Previous / Next links at the bottom of each post, or see the C++ series index for the full picture.

Q. How do I go deeper?

A. Use go.dev and official library docs. cppreference helps when comparing with C++.


  • Understanding Go through a C++ developer’s lens [#47-2]
  • Two-week Go curriculum for C++ developers
  • C++ vs Go — performance, concurrency, and how to choose [#47-1]
  • [Go in 2 Weeks #01] Days 1–2: philosophy and syntax
  • [Go in 2 Weeks #02] Days 3–4: memory and data structures

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • Go in 2 Weeks #04
  • Go in 2 Weeks #05
  • Go in 2 Weeks #01

이 글에서 다루는 키워드 (관련 검색어)

Go, Golang, C++, Interfaces, Polymorphism, Duck Typing, io.Reader, io.Writer 등으로 검색하시면 이 글이 도움이 됩니다.