Stephen Gilmore

Go interfaces make development easier

Django August 6th, 2024 3 minute read.

I've read about and seen the power of Go interfaces in other people's code, but haven't really come across a good opportunity to use one until I wanted to stop wasting SMTP emails during development.

The app that runs this website has some function to send out emails with a Send(...) method on a Mailer struct. I used a single method interface sot hat I could choose to send out SMTP emails or log one to the console instead. This is similar to selecting smtp.EmailBackend or console.EmailBackend in Django.

Before implementing an interface

I have a mailer struct that holds the configuration in my application to send emails. The relevant bits look something like:

package mailer

import "github.com/go-mail/mail/v2"

// Mailer struct holds configuration to send emails
type Mailer struct {
    dialer *mail.Dialer
    sender string
}

And that mailer struct gets embedded into an application (server) struct so my web application can send emails.

package main

type application struct {
    mailer         mailer.Mailer
}

func main() {
    app := &application{
        mailer: mailer.New(...),
    }
    app.serve()
}

This setup is rather inflexible. The only option I have is to use and send SMTP emails. Next I'll walk through the steps to implement an interface that can send SMTP emails or log them to the console.

Step 1: Create an interface

In package mailer, create a new MailerInterface.

package mailer

type MailerInterface interface {
    Send(recipient, templateFile string, data any) error
}

And then the application struct definition is updated to reference the interface.

package main

type application struct {
    mailer         mailer.MailerInterface
}

Step 2: Create a LogMailer struct

The LogMailer will have a Send(...) method so that it fulfills the definition of a MailerInterface

package mailer

// LogMailer object for logging emails instead of sending them
type LogMailer struct {
    log *slog.Logger
}

// NewLogMailer creates a new logMailer object for logging emails instead of sending them
func NewLogMailer(l *slog.Logger) LogMailer {
    return LogMailer{
        log: l,
    }
}

// Send method logs the recipient email, template file name, and any dynamic data
// as an any parameter.
func (m LogMailer) Send(recipient, templateFile string, data any) error {
    m.log.Info("send email", "to", recipient, "template", templateFile, "data", data)
    return nil
}

Step 3: Choose when to use the Mailer or LogMailer

I added a flag to choose when I want my application to use the Mailer or LogMailer.

package main

func main() {
    var logMail bool
    flag.BoolVar(&logMail, "logMail", false, "Log mail instead of sending a real email")
    flag.Parse()

    // Choose a mailer
    var m mailer.MailerInterface
    switch {
    case logMail:
        m = mailer.NewLogMailer(logger)
    default:
        m = mailer.New(...)
    }

Tadaa! Now I can include or omit the flag -logMail when launching my web app to choose whether emails should sent or logged.