Practical examples of creating and using Go interfaces

If you’re learning Go in 2024 and you still feel a bit fuzzy about interfaces, you’re not alone. Many developers only “get it” when they see real code. That’s why this guide focuses on practical, working examples of creating and using Go interfaces that you can drop into your own projects. We’ll walk through an example of simple behavior abstraction, then move into real examples drawn from logging, HTTP handlers, testing, and concurrency. Instead of just repeating the language spec, we’ll look at how interfaces show up in everyday Go code, why they’re so powerful for testing, and how to avoid common design mistakes. By the end, you’ll have several concrete examples of creating and using Go interfaces that demonstrate not just the syntax, but the tradeoffs and patterns professional Go developers rely on today.
Written by
Jamie
Published

Simple example of creating and using Go interfaces

Let’s start with one of the smallest possible examples of creating and using Go interfaces: modeling shapes that can compute their area.

package main

import (
    "fmt"
    "math"
)

type Shape interface {
    Area() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func PrintArea(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
}

func main() {
    c := Circle{Radius: 2.5}
    r := Rectangle{Width: 3, Height: 4}

    PrintArea(c)
    PrintArea(r)
}

This tiny snippet already gives you one of the best examples of how Go interfaces work:

  • The Shape interface describes behavior, not data.
  • Circle and Rectangle implicitly implement the interface by having an Area() float64 method.
  • PrintArea depends only on the interface, not on concrete types.

This is the core pattern you’ll see repeated in many real examples of creating and using Go interfaces in production code.


Real examples of creating and using Go interfaces in standard library code

If you want real examples that mirror how Go is used in serious systems, the standard library is your best teacher.

Example of I/O abstraction with io.Reader and io.Writer

io.Reader and io.Writer are arguably the best examples of creating and using Go interfaces:

type Reader interface {
    Read(p []byte) (n int, err error)
}

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

Anything that implements these methods is a reader or writer: files, network connections, in-memory buffers, compressed streams, and so on.

Here’s a simple example of using them:

package main

import (
    "io"
    "log"
    "os"
)

func CopyFile(dst, src string) error {
    in, err := os.Open(src)
    if err != nil {
        return err
    }
    defer in.Close()

    out, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer out.Close()

    if _, err := io.Copy(out, in); err != nil {
        return err
    }

    return out.Close()
}

func main() {
    if err := CopyFile("out.txt", "in.txt"); err != nil {
        log.Fatal(err)
    }
}

io.Copy doesn’t care if the source is a file, an HTTP response, or a gzip reader. This is a textbook example of creating and using Go interfaces to make code reusable and testable.

You can browse more interface-heavy code in the official Go documentation:

  • Go standard library docs: https://pkg.go.dev/std

Logging: real examples of creating and using Go interfaces

Logging is where interfaces start paying rent in real-world services.

Imagine you want your app to log to stdout in development and to a structured logging system in production. You can define a small logging interface and plug in different implementations.

package logging

type Logger interface {
    Info(msg string, fields map[string]any)
    Error(msg string, err error, fields map[string]any)
}
``

A simple standard-output logger:

```go
package logging

import (
    "fmt"
    "log"
)

type StdLogger struct{}

func (StdLogger) Info(msg string, fields map[string]any) {
    log.Printf("INFO: %s %v", msg, fields)
}

func (StdLogger) Error(msg string, err error, fields map[string]any) {
    log.Printf("ERROR: %s err=%v %v", msg, err, fields)
}

Now your application code only depends on the interface:

package service

import "myapp/logging"

type UserService struct {
    log logging.Logger
}

func NewUserService(log logging.Logger) *UserService {
    return &UserService{log: log}
}

func (s *UserService) CreateUser(email string) {
    s.log.Info("creating user", map[string]any{"email": email})
    // create user...
}

Later, you can add a JSON logger or an adapter to another logging library without touching UserService. This is one of the most common real examples of creating and using Go interfaces in backend services.


HTTP handlers: examples include interfaces you already use

If you’ve written any web code in Go, you’ve already used an interface without thinking about it: http.Handler.

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

A basic router function gives a nice example of creating and using Go interfaces for HTTP:

package main

import (
    "log"
    "net/http"
)

type LoggingMiddleware struct {
    Next http.Handler
}

func (m LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    log.Printf("%s %s", r.Method, r.URL.Path)
    m.Next.ServeHTTP(w, r)
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello"))
}

func main() {
    base := http.HandlerFunc(helloHandler)
    handler := LoggingMiddleware{Next: base}

    http.Handle("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

This pattern—wrapping one http.Handler with another—is a live example of creating and using Go interfaces for cross-cutting concerns like logging, metrics, and authentication.

You can explore more handler patterns in the Go net/http docs:

  • https://pkg.go.dev/net/http

Interfaces for testing: examples of mocking in Go

Testing is where interfaces really shine. Instead of pulling in a heavy mocking framework, Go encourages you to define small interfaces and pass in fakes during tests.

Here’s an example of using an interface to test code that talks to an external API.

package weather

import "time"

type Client interface {
    CurrentTemp(city string) (float64, error)
}

type Service struct {
    client Client
}

func NewService(c Client) *Service {
    return &Service{client: c}
}

func (s *Service) IsHeatWarning(city string) (bool, error) {
    temp, err := s.client.CurrentTemp(city)
    if err != nil {
        return false, err
    }
    return temp >= 95.0, nil
}

A fake client for tests:

package weather_test

import (
    "testing"
    "myapp/weather"
)

type fakeClient struct {
    temp float64
    err  error
}

func (f fakeClient) CurrentTemp(city string) (float64, error) {
    return f.temp, f.err
}

func TestIsHeatWarning(t *testing.T) {
    svc := weather.NewService(fakeClient{temp: 100})

    warn, err := svc.IsHeatWarning("Phoenix")
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if !warn {
        t.Fatalf("expected heat warning")
    }
}

This is a very typical example of creating and using Go interfaces to isolate your code from external dependencies. It lines up nicely with modern testing guidance you’ll see in many engineering blogs and conference talks.

For broader testing principles (even though they’re language-agnostic), you can look at:

  • NIST software testing resources: https://csrc.nist.gov

Concurrency: examples of using Go interfaces with workers

Go’s concurrency story pairs nicely with interfaces. A worker pool is a good example of creating and using Go interfaces to coordinate different kinds of tasks.

package worker

type Job interface {
    Do() error
}

type WorkerPool struct {
    jobs    chan Job
    results chan error
}

func NewWorkerPool(size int) *WorkerPool {
    p := &WorkerPool{
        jobs:    make(chan Job),
        results: make(chan error),
    }

    for i := 0; i < size; i++ {
        go func() {
            for job := range p.jobs {
                p.results <- job.Do()
            }
        }()
    }

    return p
}

func (p *WorkerPool) Submit(j Job) {
    p.jobs <- j
}

func (p *WorkerPool) Results() <-chan error {
    return p.results
}

Concrete jobs:

package worker

import "time"

type EmailJob struct {
    To, Subject, Body string
}

func (e EmailJob) Do() error {
    // pretend to send email
    time.Sleep(10 * time.Millisecond)
    return nil
}

type ReportJob struct {
    ID int
}

func (r ReportJob) Do() error {
    // generate report
    time.Sleep(20 * time.Millisecond)
    return nil
}

Now you can submit different job types to the same pool. This is one of the more scalable real examples of creating and using Go interfaces in background processing systems.


Design tips: when examples of Go interfaces go wrong

Not every interface is a good idea. Some common anti-patterns show up repeatedly in code reviews.

Interface too big

A massive interface like this is a red flag:

type UserStore interface {
    CreateUser(...)
    UpdateUser(...)
    DeleteUser(...)
    ListUsers(...)
    FindByEmail(...)
    // and 10 more methods
}

It’s hard to implement, hard to mock, and often reflects leaky design. Many of the best examples of creating and using Go interfaces in the standard library use small interfaces: io.Closer, fmt.Stringer, context.Context.

Interface where a concrete type is fine

If your function only ever needs *sql.DB, you don’t need to wrap it in type DB interface { ... } just for the sake of abstraction. Use an interface when you:

  • Have multiple implementations today, or
  • Expect to need them soon, or
  • Want to make testing easier.

This mindset aligns with Go’s philosophy documented by the Go team and communities around software engineering best practices.


Looking at recent open-source Go projects and talks from GopherCon and similar conferences, a few patterns keep coming up in modern examples of creating and using Go interfaces:

  • Interfaces are defined close to where they’re used, often in the consumer package rather than the provider package.
  • Interfaces are usually tiny—often one or two methods.
  • Teams rely heavily on the standard library interfaces (io.Reader, context.Context, error) instead of inventing new ones.
  • Testing strategies lean on hand-written fakes, similar to the weather client example earlier, instead of heavy mocking frameworks.

If you browse popular Go projects on GitHub or read engineering blogs from major companies, you’ll see these patterns repeated in the best examples of Go interface design.

For general software engineering education and patterns (even if they’re not Go-specific), universities publish a lot of open material, for example:

  • MIT OpenCourseWare on software engineering: https://ocw.mit.edu

FAQ: examples of creating and using Go interfaces

Q: Can you give another small example of using Go interfaces for configuration?

Yes. Imagine you want to read configuration from different sources (file, environment variables, remote service). You can define:

type ConfigSource interface {
    Get(key string) (string, bool)
}

Then implement EnvConfig, FileConfig, and RemoteConfig. Your app code depends only on ConfigSource, not on how config is loaded. This is a small, focused example of creating and using Go interfaces to keep configuration flexible.

Q: Do I need interfaces for every service or repository?

No. Many teams overdo it early on. If you only have one implementation and no clear need for testing via fakes, a concrete type is fine. Introduce an interface when you see a real need: multiple implementations, complex external dependencies, or heavy integration with external APIs.

Q: How do I decide method receivers when implementing interfaces?

Use value receivers when the method doesn’t need to mutate the receiver and the type is small. Use pointer receivers when you need to modify state or avoid copying large structs. Remember that if your interface method is defined on a pointer receiver, only *T implements it, not T.

Q: Are there performance costs in using interfaces?

There is a small indirection cost, but in most web and service code it’s not the bottleneck. If you’re writing performance-sensitive code (for example, tight loops in data processing), you can benchmark both interface-based and concrete versions and choose based on actual numbers.

Q: Where can I find more real examples of Go interfaces?

The standard library is still the best source. Browse io, net/http, database/sql, and context in the official docs. You can also study popular open-source Go projects on GitHub to see how experienced teams apply these patterns in real systems.

Explore More Go Code Snippets

Discover more examples and insights in this category.

View All Go Code Snippets