Interface #

Interfaces in Go are the feature that most confuses developers coming from Java or C# — not because they’re hard, but because they’re far simpler than expected. In Java, you must explicitly declare implements Runnable. In Go, there’s no such declaration at all. A type automatically satisfies an interface if it has all the methods the interface requires. This isn’t a design flaw — it’s the greatest strength of Go interfaces, making decoupling between components feel completely natural and making testing much easier.

Interfaces Define Behavioral Contracts #

Interfaces in Go only define what can be done — not what is held. No fields, no implementation, just a list of method signatures:

// The interface defines a contract: "anything that can be written to"
type Writer interface {
    Write(p []byte) (n int, err error)
}

// The interface defines a contract: "anything that can be closed"
type Closer interface {
    Close() error
}

// The interface defines a contract: "anything that has a string representation"
type Stringer interface {
    String() string
}

// A richer interface
type Shape interface {
    Area() float64
    Perimeter() float64
    String() string
}

Notice there’s no public, abstract, or virtual keyword. Just a list of methods and their types.


Implicit Implementation — Go’s Greatest Strength #

In Java: class MyWriter implements Writer { ... } — you must declare that you implement an interface.

In Go: there’s no declaration at all. The compiler checks by itself whether a type satisfies all the methods the interface requires:

type Writer interface {
    Write(p []byte) (n int, err error)
}

// FileWriter implements Writer — without "implements Writer"!
type FileWriter struct {
    path string
    f    *os.File
}

func (fw *FileWriter) Write(p []byte) (int, error) {
    return fw.f.Write(p)
}

// NetworkWriter also implements Writer — same, no declaration
type NetworkWriter struct {
    conn net.Conn
}

func (nw *NetworkWriter) Write(p []byte) (int, error) {
    return nw.conn.Write(p)
}

// This function accepts ANYTHING that can be written to
func saveData(w Writer, data []byte) error {
    _, err := w.Write(data)
    return err
}

func main() {
    fw := &FileWriter{path: "output.txt"}
    nw := &NetworkWriter{}

    saveData(fw, []byte("data to file"))     // ✓
    saveData(nw, []byte("data to network"))  // ✓ — polymorphism!
}

Why Implicit Implementation Is So Powerful #

Imagine you’re using a third-party library that defines a struct ExternalDB. That library knows nothing about the UserRepository interface you created. But as long as ExternalDB has the methods your interface needs, it automatically satisfies it — without you having to modify the library, without needing a wrapper class.

// Your interface in the application package
type UserRepository interface {
    FindByID(id int) (*User, error)
    Save(user *User) error
}

// A third-party library — you can't change this code,
// but it has FindByID and Save methods
type ThirdPartyDB struct { ... }
func (db *ThirdPartyDB) FindByID(id int) (*User, error) { ... }
func (db *ThirdPartyDB) Save(user *User) error { ... }

// ThirdPartyDB automatically satisfies UserRepository
// without modifying ThirdPartyDB at all!
var repo UserRepository = &ThirdPartyDB{}

Interface Values — Two Internal Components #

It’s important to understand: an interface variable stores two things at once — the concrete type and its concrete value. The internal structure of this interface value can be visualized in the following diagram:

flowchart TD
    subgraph InterfaceValue["Interface Value (Internal Structure)"]
        Type["Dynamic Type (T)<br>Stores the concrete type (e.g. *FileWriter)"]
        Value["Concrete Value (V)<br>Points to the concrete value/data"]
    end

This affects how nil interfaces work:

var w Writer         // type=nil, value=nil → nil interface

var fw *FileWriter = nil
w = fw               // type=*FileWriter, value=nil → interface is NOT nil!

fmt.Println(w == nil)  // false! — even though fw is a nil pointer

This is a very famous gotcha in Go — a nil interface is different from an interface holding a nil pointer:

// ANTI-PATTERN: this function doesn't behave as expected
func getWriter(useFile bool) Writer {
    var fw *FileWriter  // nil pointer
    if useFile {
        fw = openFile()
    }
    return fw  // returns a Writer holding (*FileWriter, nil)
               // not a nil Writer!
}

func main() {
    w := getWriter(false)
    if w == nil {
        fmt.Println("no writer")  // NEVER printed!
    }
    // w != nil even though fw is a nil pointer
    // Calling w.Write() will panic because fw is nil
}

// CORRECT: return a nil interface explicitly
func getWriter(useFile bool) Writer {
    if useFile {
        return openFile()  // a valid *FileWriter
    }
    return nil  // the real nil interface
}
Never return a possibly-nil interface variable from a function. Always return nil explicitly when there’s no implementation. Returning (*ConcreteType)(nil) wrapped in an interface produces a non-nil interface, making if result == nil checks always false.

Small Interfaces Are More Powerful #

One of the most important principles in the Go community: a small interface is more useful than a large one. io.Reader has only one method — yet it’s used in thousands of places across the Go ecosystem:

// io.Reader — one method, thousands of implementations
type Reader interface {
    Read(p []byte) (n int, err error)
}

// All of these implement io.Reader:
// - *os.File
// - *bytes.Buffer
// - *strings.Reader
// - net.Conn
// - *http.Request.Body
// - *gzip.Reader
// - *zip.Reader
// ...and hundreds more

// A function accepting io.Reader works with ALL implementations above
func countLines(r io.Reader) (int, error) {
    scanner := bufio.NewScanner(r)
    count := 0
    for scanner.Scan() {
        count++
    }
    return count, scanner.Err()
}

// Can be used with files, strings, network connections, etc.
lines, _ := countLines(os.Stdin)
lines, _ = countLines(strings.NewReader("line 1\nline 2\n"))
lines, _ = countLines(httpResp.Body)
Interface size guide:

  1-2 methods  ✓ Excellent — implementable by many types
  3-5 methods  ✓ Still OK — a clear, bounded contract
  6-10 methods ⚠ Getting heavy — consider splitting into small interfaces
  10+ methods  ✗ Almost certainly too large — hard to mock, hard to use

Interface Composition #

Interfaces can be embedded into other interfaces to form larger contracts, exactly like struct embedding:

// Atomic interfaces — one capability each
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
}

type Seeker interface {
    Seek(offset int64, whence int) (int64, error)
}

// Composition — built from smaller interfaces
type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

type ReadWriteSeeker interface {
    Reader
    Writer
    Seeker
}

// A type implementing ReadWriteCloser automatically
// also implements Reader, Writer, and Closer separately

This composition lets you choose the right contract for each function — give only what’s needed, nothing more:

func processInput(r Reader) { ... }      // only needs Read
func writeOutput(w Writer) { ... }       // only needs Write
func handleConn(rwc ReadWriteCloser) { ... }  // needs all three

any and interface{} #

interface{} (or its alias any since Go 1.18) is the empty interface — it satisfies every type because it requires no methods at all:

// any and interface{} are identical — any is just an alias
var v any = 42
v = "hello"
v = []int{1, 2, 3}
v = struct{ X int }{X: 10}

// Useful for generic containers before generics (Go 1.18)
type Stack struct {
    items []any
}

func (s *Stack) Push(item any) {
    s.items = append(s.items, item)
}

func (s *Stack) Pop() (any, bool) {
    if len(s.items) == 0 {
        return nil, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

Use any very sparingly. Every time you use any, you lose the type safety Go provides — the compiler can’t help detect type errors at compile time. Since Go 1.18, use generics as the type-safe alternative for generic containers:

// ANTI-PATTERN: loses type safety
func contains(slice []any, item any) bool { ... }
items := []any{1, 2, 3}

// CORRECT since Go 1.18: type-safe with generics
func contains[T comparable](slice []T, item T) bool {
    for _, v := range slice {
        if v == item { return true }
    }
    return false
}
fmt.Println(contains([]int{1, 2, 3}, 2))     // true, type-safe
fmt.Println(contains([]string{"a", "b"}, "c")) // false, type-safe

Type Assertions #

A type assertion extracts the concrete value from an interface variable:

var i interface{} = "Hello, Go!"

// Safe form — always use this
str, ok := i.(string)
if ok {
    fmt.Println(str)        // Hello, Go!
    fmt.Println(len(str))   // 10
} else {
    fmt.Println("not a string")
}

// Unsafe form — PANICS if the type doesn't match
str2 := i.(string)   // OK because i is indeed a string
num  := i.(int)      // PANIC: interface conversion: interface {} is string, not int

Type assertions are very useful for checking extra capabilities of a value received as an interface:

type Writer interface {
    Write([]byte) (int, error)
}

// Check whether a writer can also be closed
func writeAndClose(w Writer, data []byte) error {
    if _, err := w.Write(data); err != nil {
        return err
    }

    // Type assertion to check for a Close capability
    if closer, ok := w.(io.Closer); ok {
        return closer.Close()  // call Close if available
    }
    return nil  // fine if there's no Close
}

// Check whether an error carries extra information
func handleError(err error) {
    // Check whether the error is a specific type
    var netErr *net.OpError
    if errors.As(err, &netErr) {
        fmt.Println("network error on operation:", netErr.Op)
        return
    }

    var pathErr *os.PathError
    if errors.As(err, &pathErr) {
        fmt.Println("path error on:", pathErr.Path)
        return
    }

    fmt.Println("generic error:", err)
}

Type Switches #

A type switch is an elegant way to handle various possible types in one block — much cleaner than a series of type assertions:

func formatValue(v any) string {
    switch val := v.(type) {
    case nil:
        return "<nil>"
    case bool:
        if val {
            return "true"
        }
        return "false"
    case int:
        return strconv.Itoa(val)
    case int64:
        return strconv.FormatInt(val, 10)
    case float64:
        return strconv.FormatFloat(val, 'f', -1, 64)
    case string:
        return fmt.Sprintf("%q", val)
    case []byte:
        return fmt.Sprintf("bytes(%d)", len(val))
    case error:
        return "error: " + val.Error()
    case fmt.Stringer:
        // A type implementing Stringer
        return val.String()
    default:
        return fmt.Sprintf("%T(%v)", val, val)
    }
}

func main() {
    fmt.Println(formatValue(nil))          // <nil>
    fmt.Println(formatValue(true))         // true
    fmt.Println(formatValue(42))           // 42
    fmt.Println(formatValue(3.14))         // 3.14
    fmt.Println(formatValue("hello"))      // "hello"
    fmt.Println(formatValue([]byte{1,2}))  // bytes(2)
}

Interfaces for Dependency Injection and Testing #

This is the most important use of interfaces in production code. By defining dependencies as interfaces, you can:

  1. Swap implementations without changing the code that uses them
  2. Inject mocks during testing without any external library
// Define dependencies as interfaces on the consumer side
type EmailSender interface {
    Send(to, subject, body string) error
}

type SMSSender interface {
    Send(to, message string) error
}

type UserRepository interface {
    FindByID(id int) (*User, error)
    Save(user *User) error
}

// The service depends on interfaces, not concrete implementations
type UserService struct {
    repo  UserRepository
    email EmailSender
    sms   SMSSender
}

func NewUserService(repo UserRepository, email EmailSender, sms SMSSender) *UserService {
    return &UserService{repo: repo, email: email, sms: sms}
}

func (s *UserService) Register(name, emailAddr, phone string) error {
    user := &User{Name: name, Email: emailAddr, Phone: phone}

    if err := s.repo.Save(user); err != nil {
        return fmt.Errorf("failed to save user: %w", err)
    }

    // Send notifications — doesn't care about the implementation
    if err := s.email.Send(emailAddr, "Welcome!", "Your account was created successfully."); err != nil {
        return fmt.Errorf("failed to send email: %w", err)
    }

    return nil
}

// ── Real implementations for production ────────────────────────
type SMTPEmailSender struct {
    host string
    port int
}

func (s *SMTPEmailSender) Send(to, subject, body string) error {
    // send email via SMTP
    fmt.Printf("[SMTP] Sending to %s: %s\n", to, subject)
    return nil
}

type TwilioSMSSender struct {
    apiKey string
}

func (t *TwilioSMSSender) Send(to, message string) error {
    // send SMS via the Twilio API
    return nil
}

// ── Mocks for testing — without any framework! ─────────────
type MockEmailSender struct {
    SentEmails []struct{ To, Subject, Body string }
    ShouldFail bool
}

func (m *MockEmailSender) Send(to, subject, body string) error {
    if m.ShouldFail {
        return errors.New("mock: email failed to send")
    }
    m.SentEmails = append(m.SentEmails, struct{ To, Subject, Body string }{to, subject, body})
    return nil
}

type MockUserRepo struct {
    Users   map[int]*User
    SaveErr error
}

func (r *MockUserRepo) FindByID(id int) (*User, error) {
    user, ok := r.Users[id]
    if !ok {
        return nil, fmt.Errorf("user %d not found", id)
    }
    return user, nil
}

func (r *MockUserRepo) Save(user *User) error {
    if r.SaveErr != nil {
        return r.SaveErr
    }
    if r.Users == nil {
        r.Users = make(map[int]*User)
    }
    r.Users[len(r.Users)+1] = user
    return nil
}

Interfaces on the Consumer Side, Not the Producer Side #

This is the most important design principle that’s often violated:

// ANTI-PATTERN: interface defined on the PRODUCER side
// package userservice
type UserServiceInterface interface {
    CreateUser(name, email string) (*User, error)
    GetUser(id int) (*User, error)
    UpdateUser(id int, data UpdateData) error
    DeleteUser(id int) error
    ListUsers(filter Filter) ([]*User, error)
    // ... 10 more methods
}
type UserService struct { ... }
// UserService implements UserServiceInterface

// Problem: a consumer that only needs GetUser is forced to depend
// on this giant interface, and its mock must implement all
// 15 methods even though only 1 is used

// ─────────────────────────────────────────────────────────────

// CORRECT: interfaces defined on the CONSUMER side
// package handler — only define what's needed
type UserGetter interface {
    GetUser(id int) (*User, error)
}

type ProfileHandler struct {
    users UserGetter  // small interface, easy to mock
}

// package ordersvc — different needs, different interface
type UserValidator interface {
    GetUser(id int) (*User, error)
}

type OrderService struct {
    users UserValidator
}

The result: UserService (the real struct) automatically satisfies both UserGetter and UserValidator because both only need the same single method. Each consumer gets the smallest interface they need.


Method Sets — Value vs Pointer #

There’s an important rule about method sets that determines when value and pointer types can satisfy an interface:

type Animal interface {
    Sound() string
    Move()
}

type Dog struct{ Name string }

func (d Dog)  Sound() string { return "Woof" }  // value receiver
func (d *Dog) Move()         { fmt.Println(d.Name, "is running") }  // pointer receiver

func main() {
    // *Dog implements Animal — the pointer has ALL methods
    var a Animal = &Dog{Name: "Buddy"}  // ✓
    a.Sound()
    a.Move()

    // Dog does NOT implement Animal — a value doesn't have pointer methods
    // var b Animal = Dog{Name: "Buddy"}  // ✗ compile error:
    // Dog does not implement Animal (Move method has pointer receiver)
}
Method Set Rules:

  Type T has methods:
    → All methods with VALUE receivers (T)

  Type *T has methods:
    → All methods with VALUE receivers (T)
    → All methods with POINTER receivers (*T)

  Implications for interfaces:
    → If the interface has a method with a pointer receiver,
      only *T can satisfy the interface, not T
    → Use a pointer (&value) when assigning to an interface if
      there are methods with pointer receivers

Complete Example Program #

The following program builds a multi-channel notification system demonstrating dependency injection via interfaces:

package main

import (
    "fmt"
    "strings"
    "time"
)

// ── Interface Definitions ─────────────────────────────────────

type Notifier interface {
    Send(recipient, message string) error
    Name() string
}

type NotificationStore interface {
    Save(n Notification) error
    FindByRecipient(recipient string) []Notification
}

// ── Domain Types ──────────────────────────────────────────────

type Priority int

const (
    PriorityLow Priority = iota
    PriorityNormal
    PriorityHigh
    PriorityCritical
)

func (p Priority) String() string {
    switch p {
    case PriorityLow:      return "Low"
    case PriorityNormal:   return "Normal"
    case PriorityHigh:     return "High"
    case PriorityCritical: return "Critical"
    default:               return "Unknown"
    }
}

type Notification struct {
    ID        int
    Recipient string
    Message   string
    Channel   string
    Priority  Priority
    SentAt    time.Time
    Success   bool
    Error     string
}

// ── Concrete Notifier Implementations ────────────────────────

type EmailNotifier struct {
    SMTPHost  string
    From      string
    sentCount int
}

func (e *EmailNotifier) Send(recipient, message string) error {
    // Simulate sending an email
    e.sentCount++
    fmt.Printf("  📧 [EMAIL] To: %s\n     %s\n", recipient, message)
    return nil
}

func (e *EmailNotifier) Name() string { return "Email" }

type SlackNotifier struct {
    WebhookURL string
    Channel    string
}

func (s *SlackNotifier) Send(recipient, message string) error {
    fmt.Printf("  💬 [SLACK] #%s @%s: %s\n", s.Channel, recipient, message)
    return nil
}

func (s *SlackNotifier) Name() string { return "Slack" }

type SMSNotifier struct {
    APIKey  string
    FromNum string
}

func (s *SMSNotifier) Send(recipient, message string) error {
    // SMS messages are usually length-limited
    if len(message) > 160 {
        message = message[:157] + "..."
    }
    fmt.Printf("  📱 [SMS] To: %s | %s\n", recipient, message)
    return nil
}

func (s *SMSNotifier) Name() string { return "SMS" }

// ── In-Memory Store ───────────────────────────────────────────

type InMemoryStore struct {
    notifications []Notification
    nextID        int
}

func (s *InMemoryStore) Save(n Notification) error {
    s.nextID++
    n.ID = s.nextID
    s.notifications = append(s.notifications, n)
    return nil
}

func (s *InMemoryStore) FindByRecipient(recipient string) []Notification {
    var result []Notification
    for _, n := range s.notifications {
        if n.Recipient == recipient {
            result = append(result, n)
        }
    }
    return result
}

// ── Notification Service ──────────────────────────────────────

type NotificationService struct {
    notifiers map[string]Notifier
    store     NotificationStore
}

func NewNotificationService(store NotificationStore) *NotificationService {
    return &NotificationService{
        notifiers: make(map[string]Notifier),
        store:     store,
    }
}

func (ns *NotificationService) Register(notifier Notifier) {
    ns.notifiers[notifier.Name()] = notifier
}

func (ns *NotificationService) Notify(
    recipient, message string,
    priority Priority,
    channels ...string,
) {
    // Determine channels based on priority if not specified
    if len(channels) == 0 {
        switch priority {
        case PriorityCritical:
            channels = []string{"Email", "SMS", "Slack"}
        case PriorityHigh:
            channels = []string{"Email", "Slack"}
        default:
            channels = []string{"Email"}
        }
    }

    fmt.Printf("\n[%s] Sending to %s (Priority: %s):\n",
        time.Now().Format("15:04:05"), recipient, priority)

    for _, ch := range channels {
        notifier, ok := ns.notifiers[ch]
        if !ok {
            fmt.Printf("  ⚠ Channel %q is not registered\n", ch)
            continue
        }

        n := Notification{
            Recipient: recipient,
            Message:   message,
            Channel:   ch,
            Priority:  priority,
            SentAt:    time.Now(),
        }

        err := notifier.Send(recipient, message)
        if err != nil {
            n.Error = err.Error()
            fmt.Printf("  ✗ Failed to send via %s: %v\n", ch, err)
        } else {
            n.Success = true
        }

        _ = ns.store.Save(n)
    }
}

func (ns *NotificationService) History(recipient string) {
    notifications := ns.store.FindByRecipient(recipient)
    if len(notifications) == 0 {
        fmt.Printf("\nNo notification history for %s\n", recipient)
        return
    }

    fmt.Printf("\n=== Notification History: %s ===\n", recipient)
    for _, n := range notifications {
        status := "✓"
        if !n.Success {
            status = "✗"
        }
        fmt.Printf("  [%s] %s via %-6s | %s\n",
            status,
            n.SentAt.Format("15:04:05"),
            n.Channel,
            truncate(n.Message, 50),
        )
    }
}

func truncate(s string, n int) string {
    if len(s) <= n {
        return s
    }
    return s[:n-3] + "..."
}

// ── Main ──────────────────────────────────────────────────────

func main() {
    // Setup — dependency injection via interfaces
    store := &InMemoryStore{}
    svc := NewNotificationService(store)

    // Register notifiers — all implement Notifier
    svc.Register(&EmailNotifier{SMTPHost: "smtp.example.com", From: "[email protected]"})
    svc.Register(&SlackNotifier{WebhookURL: "https://hooks.slack.com/...", Channel: "alerts"})
    svc.Register(&SMSNotifier{APIKey: "sk_sms_xxx", FromNum: "+6281234567890"})

    // Send various notifications
    svc.Notify("[email protected]", "Welcome to our platform!", PriorityNormal)

    svc.Notify("[email protected]",
        "Your Rp 5,000,000 transaction was processed successfully",
        PriorityHigh)

    svc.Notify("[email protected]",
        "CRITICAL: Server CPU usage reached 98%! Check immediately!",
        PriorityCritical)

    svc.Notify("[email protected]",
        "Your monthly report is ready to download",
        PriorityLow,
        "Email")  // override the channel

    // View history
    svc.History("[email protected]")
    svc.History("[email protected]")

    // Demonstrate type assertions — check for extra capabilities
    fmt.Println("\n=== Notifier Info ===")
    for name, notifier := range svc.notifiers {
        info := fmt.Sprintf("%-10s", name)

        // Type assertion to check whether it's an EmailNotifier
        if emailNotifier, ok := notifier.(*EmailNotifier); ok {
            info += fmt.Sprintf(" | SMTP: %s | Sent: %d",
                emailNotifier.SMTPHost,
                emailNotifier.sentCount)
        }

        // Type switch for per-type specific info
        switch n := notifier.(type) {
        case *SlackNotifier:
            info += fmt.Sprintf(" | Channel: #%s", n.Channel)
        case *SMSNotifier:
            info += fmt.Sprintf(" | From: %s", n.FromNum)
        }

        fmt.Printf("  %s\n", info)
    }

    // Demonstrate: small interfaces on the consumer side
    var channels []string
    for name := range svc.notifiers {
        channels = append(channels, name)
    }
    fmt.Printf("\nAvailable channels: %s\n", strings.Join(channels, ", "))
}

Summary #

  • Implicit implementation — there’s no implements; a type automatically satisfies an interface if it has all the required methods.
  • Interfaces = behavioral contracts — only method signatures, no fields or implementations.
  • Interface values store (type, value) — an interface variable holding a nil pointer isn’t the same as a nil interface; always return nil explicitly.
  • Small interfaces are more powerfulio.Reader with one method is used thousands of times; avoid interfaces with 10+ methods.
  • Interface composition — embed interfaces into other interfaces for richer contracts.
  • any / interface{} loses type safety — use generics since Go 1.18 for type-safe generic containers.
  • Safe type assertions (val, ok := i.(Type)) — always use the two-value form to avoid panics.
  • Type switches handle many possible types elegantly.
  • Define interfaces on the consumer side — each consumer defines the smallest interface it needs, not one big interface on the producer side.
  • Method sets: *T has both value and pointer receiver methods; T only has value receiver methods — use a pointer when assigning to an interface if pointer receivers exist.

← Previous: Struct   Next: Exceptions →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact