Practical examples of creating and using Go interfaces
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
Shapeinterface describes behavior, not data. CircleandRectangleimplicitly implement the interface by having anArea() float64method.PrintAreadepends 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.
Modern 2024–2025 trends: small, focused interfaces
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.
Related Topics
Practical examples of Go command-line application examples for 2025
Practical examples of 3 examples of working with Go maps for real projects
Practical examples of simple HTTP server examples in Go
Practical examples of defining and using structs in Go
Practical examples of creating and using Go interfaces
So You Think You Know HTTP in Go? Think Again
Explore More Go Code Snippets
Discover more examples and insights in this category.
View All Go Code Snippets