Stephen Gilmore

Go interfaces make development easier

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.

#Go