Problem overview
An app tells users about news by email or by SMS. The code that sends the news shouldn't care which kind each user picked. It only needs to know that each one can send a message. That promise is an interface.
You write the interface Notifier, with one method, send(message string) string, which returns the text that would be sent. Then two structs:
EmailNotifier, made byNewEmailNotifier(address):sendreturns"To ", the address,": "and the whole message.SmsNotifier, made byNewSmsNotifier(phone):sendreturns"SMS ", the phone number,": "and only the first 20 characters of the message. A message of 20 characters or fewer goes whole.
The function notifyAll(notifiers, message) takes a slice of notifiers of any kind and returns the texts they send, in the same order. For the notifiers NewEmailNotifier("ana@mail.com") and NewSmsNotifier("0712345678") and the message "Meeting at 5", the result is To ana@mail.com: Meeting at 5 and SMS 0712345678: Meeting at 5.
Approach
In Go, an interface type is a list of method signatures. A struct satisfies it just by having those methods: there is no implements. So Notifier needs one line, send(message string) string, and each struct gets a method with that exact name and signature.
Then notifyAll is a loop over a []Notifier. For each element it calls send and collects the result. It never asks which struct is inside, and a third kind of notifier would work with it unchanged.
One more point about the design: notifyAll receives its notifiers as a []Notifier, a slice whose elements have the interface type. A slice of EmailNotifier values can't be passed there directly, even though each element would qualify, because the element types differ. You build the []Notifier yourself, listing the values of both kinds in one literal, as in the examples below.
Step by step
- Declare
Notifieras an interface with the methodsend. - Give
EmailNotifiera fieldaddress. Go has no constructors, soNewEmailNotifieris a function that returns anEmailNotifierwith that field set. - Write
EmailNotifier'ssend: join the pieces with+. With this method, the struct is aNotifier. - Do the same for
SmsNotifier, with aphonefield and a constructor. - For the SMS text, slice the message:
message[:n]keeps the firstnbytes. The countnmust be the smaller of 20 andlen(message), because a slice can't end past the end of the string. The built-inmindoes that. - In
notifyAll, create an empty[]stringnamedtexts, loop over the notifiers, appendnotifier.send(message)each time, and returntexts.
The solution in Go
type Notifier interface {
send(message string) string
}
type EmailNotifier struct {
address string
}
func NewEmailNotifier(address string) EmailNotifier {
return EmailNotifier{address: address}
}
func (this EmailNotifier) send(message string) string {
return "To " + this.address + ": " + message
}
type SmsNotifier struct {
phone string
}
func NewSmsNotifier(phone string) SmsNotifier {
return SmsNotifier{phone: phone}
}
func (this SmsNotifier) send(message string) string {
return "SMS " + this.phone + ": " + message[:min(20, len(message))]
}
func notifyAll(notifiers []Notifier, message string) []string {
texts := []string{}
for _, notifier := range notifiers {
texts = append(texts, notifier.send(message))
}
return texts
}
Details worth a look:
- The built-in
minexists since Go 1.21, so it works in Go 1.22. In older code you would see anifinstead. texts := []string{}makes an empty, non-nil slice. With no notifiers the loop doesn't run, and the function returns that empty slice.- Each call
notifier.send(message)runs the method of the struct stored inside the interface value: the email text for anEmailNotifier, the SMS text for anSmsNotifier.
Using the notifiers
notifiers := []Notifier{NewSmsNotifier("5550100"), NewEmailNotifier("bob@work.org")}
for _, text := range notifyAll(notifiers, "Your order has shipped today") {
fmt.Println(text)
}
SMS 5550100: Your order has shipp
To bob@work.org: Your order has shipped today
The message has 28 characters. The SMS keeps the first 20, Your order has shipp, and the email sends everything. The texts come out in the order of the notifiers.
Edge cases and common mistakes
- Exactly 20 characters. With two SMS notifiers and the messages
"Exactly twenty chars"and"Exactly twenty chars!", the first is sent whole and the second loses its!, so both texts readSMS 111: Exactly twenty charsandSMS 222: Exactly twenty chars. - Short messages. Writing
message[:20]without theminpanics at run time for a message shorter than 20 bytes. For"Hi"the error isslice bounds out of range [:20] with length 2, because the slice goes past the end. - No notifiers.
notifyAll([]Notifier{}, "Hi")returns an empty slice, length 0. - Bytes, not characters. Slicing a string counts bytes, not characters. For plain ASCII text they are the same. For a string like
"Ünïcödé", which has 7 characters but 11 bytes,s[:4]cuts the third byte of a two-byte letter and gives broken text. The lesson's messages use letters, digits and punctuation, but for text with accents you would convert with[]rune(message), slice the runes and convert back withstring(...). With that, 20 characters of"Ünïcödé Ünïcödé Ünïcödé"end atÜnïc. - Checking the kind. A type switch inside
notifyAlldefeats the purpose of the interface.
Testing
Run the first example: email and SMS with "Meeting at 5", both whole, since the message has 12 characters. Run the second, where the 28-character message is cut for the SMS only. Then call notifyAll twice with the same two SMS notifiers and two messages, as in the third example, to see that each call is independent and that the notifiers are not changed by sending: neither struct stores the message, so the same notifier can be used again for the next one.
