A Beginner’s Guide to Interfaces in Go

BhuwanBhuwan
3 min read

In Go, interfaces are the foundation of polymorphism, abstraction, and flexible design but they work very differently compared to traditional object-oriented languages.

What are interfaces in Go?

In Go, an interface is a type that defines a set of method signatures (i.e., function names, parameters, and return types), but does not define an implementation.

Any type that implements these methods (with matching signatures) automatically satisfies the interface; it doesn't need to be explicitly stated.

Go’s interfaces are:

  • Lightweight and implicit

  • Ideal for decoupling code

  • A powerful tool for testing, mocking, and plugging in behavior dynamically

Understand interfaces in Go with an example

type Speaker interface {
    Speak() string
}

This defines a Speaker interface with one method:

  • Speak() that returns a string

Any type that has a Speak() method becomes a Speaker.

type Dog struct{}

func (d Dog) Speak() string {
    return "Woof!"
}

No need to declare anything like implements Speaker — it's automatic!

func makeItSpeak(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    var d Dog
    makeItSpeak(d) // Output: Woof!
}

Dog has a Speak() method → it satisfies the Speaker interface.

You don't have to explicitly declare that a type satisfies an interface. You can write functions that accept any type that implements the behavior you care about. Different types can satisfy the same interface in their own way.

Think of Go interfaces as an contract. It allows you to swap in any type that fulfills the contract making your code more flexible and interchangeable.

Real Usage of Go Interfaces

Each example focuses on a different use case to show the versatility of interfaces in practice.

Custom Logger Interface

Using interfaces, you can replace logging backends (file, console, external service) without changing app logic. Useful for mocking and test scenario.

package main

import (
  "fmt"
  "os"
)

type Logger interface {
    Log(message string)
}

type ConsoleLogger struct{}

func (c ConsoleLogger) Log(message string) {
    fmt.Println("[Console]", message)
}

type FileLogger struct {
    File *os.File
}

func (f FileLogger) Log(message string) {
    fmt.Fprintln(f.File, "[File]", message)
}


func doSomething(logger Logger) {
    logger.Log("Action completed")
}

func main() {
    cl := ConsoleLogger{}
    doSomething(cl)

    f, _ := os.Create("log.txt")
    fl := FileLogger{File: f}
    doSomething(fl)
}

You can swap between different logging mechanisms without modifying doSomething().

Strategy Pattern via Interfaces

Think of scenario like you need to implement different payment methods with a unified interface. You can add new payment methods without changing processPayment().

package main

import (
  "fmt"
)

type PaymentMethod interface {
    Pay(amount float64) string
}

type CreditCard struct{}
type PayPal struct{}

func (CreditCard) Pay(amount float64) string {
    return fmt.Sprintf("Paid %.2f using Credit Card", amount)
}

func (PayPal) Pay(amount float64) string {
    return fmt.Sprintf("Paid %.2f using PayPal", amount)
}

func processPayment(p PaymentMethod, amount float64) {
    fmt.Println(p.Pay(amount))
}

func main() {
    processPayment(CreditCard{}, 100.0)
    processPayment(PayPal{}, 50.0)
}

Dependency Injection via Interface

Think of a practical scenario like, inject different mailers (mock, SMTP, API-based) into the app without rewriting logic.

package main

import "fmt"


type EmailSender interface {
    Send(to string, body string) error
}

type SMTPSender struct{}

func (s SMTPSender) Send(to string, body string) error {
    fmt.Printf("Sent email to %s via SMTP\n", to)
    return nil
}

type App struct {
    Mailer EmailSender
}

func (a App) NotifyUser(email string) {
    _ = a.Mailer.Send(email, "Welcome!")
}

func main() {
    app := App{Mailer: SMTPSender{}}
    app.NotifyUser("user@example.com")
}

Why Use Interfaces in Go?

The Go interface allows you to design flexible, testable, and loosely coupled systems. Instead of hardware binding specific behaviors, such as sending email over SMTP, you can define an EmailSender and inject the implementation - whether it's SMTP, an API, or a test mock.

This approach allows you to:

  • Exchange implementations without touching business logic

  • Write cleaner tests without external dependencies

  • Follow SOLID principles (especially dependency inversion)

  • Avoid long inheritance chains

0
Subscribe to my newsletter

Read articles from Bhuwan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Bhuwan
Bhuwan