Start learning free

LESSON 566 OF 573 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Notifiers in Go: One Interface, Two Senders

Practice O(n) Read the full explanation

An app tells its users about news in different ways: some get an email, some get a text message, an 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.

Write:

  • the interface Notifier, with one method, send(message), which returns the text that would be sent;
  • EmailNotifier, created by NewEmailNotifier(address), a notifier whose send returns "To ", the address, ": " and the whole message;
  • SmsNotifier, created by NewSmsNotifier(phone), a notifier whose send returns "SMS ", the phone number, ": " and only the first 20 characters of the message (a message of 20 characters or fewer is sent whole);
  • a function notifyAll(notifiers, message), which gets a list of notifiers of any kind and returns the list of the texts they send, in the same order.

notifyAll must work through the interface: it calls send on each notifier and never checks which kind of notifier it is.

Go has no classes and no implements: Notifier is an interface type, EmailNotifier and SmsNotifier are structs, and a struct that has the method send(message string) string is a Notifier without saying so. NewEmailNotifier(address) and NewSmsNotifier(phone) create them, and notifyAll is a plain function that gets a []Notifier.

The program creates the notifiers listed in the input, calls notifyAll once for each message, and prints each text it returns on its own line.

 

Example 1:

Input:
    notifiers: NewEmailNotifier("ana@mail.com"), NewSmsNotifier("0712345678")
    message: "Meeting at 5"
Output:
To ana@mail.com: Meeting at 5
SMS 0712345678: Meeting at 5
Explanation: The message has 12 characters, so the SMS sends all of it too.

Example 2:

Input:
    notifiers: NewSmsNotifier("5550100"), NewEmailNotifier("bob@work.org")
    message: "Your order has shipped today"
Output:
SMS 5550100: Your order has shipp
To bob@work.org: Your order has shipped today
Explanation: The message has 28 characters. The SMS keeps the first 20, "Your order has shipp";
the email sends all of it. The texts come in the order of the notifiers.

Example 3:

Input:
    notifiers: NewSmsNotifier("111"), NewSmsNotifier("222")
    messages: "Exactly twenty chars", "Exactly twenty chars!"
Output:
SMS 111: Exactly twenty chars
SMS 222: Exactly twenty chars
SMS 111: Exactly twenty chars
SMS 222: Exactly twenty chars
Explanation: notifyAll is called once for each message, with the same two notifiers.
The first message has exactly 20 characters and is sent whole. The second has 21, so its "!" is cut.

 

Constraints:

  • There are at most 50 notifiers. There can be none: then notifyAll returns an empty list.
  • A message has 1 to 100 characters: letters, digits, spaces and punctuation, with no space at its start or end.
  • Addresses and phone numbers have no spaces.
STEP 1 OF 11
    Ctrl + Enter to check

    GO · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Notifiers in Go, explained

    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 by NewEmailNotifier(address): send returns "To ", the address, ": " and the whole message.
    • SmsNotifier, made by NewSmsNotifier(phone): send returns "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

    1. Declare Notifier as an interface with the method send.
    2. Give EmailNotifier a field address. Go has no constructors, so NewEmailNotifier is a function that returns an EmailNotifier with that field set.
    3. Write EmailNotifier's send: join the pieces with +. With this method, the struct is a Notifier.
    4. Do the same for SmsNotifier, with a phone field and a constructor.
    5. For the SMS text, slice the message: message[:n] keeps the first n bytes. The count n must be the smaller of 20 and len(message), because a slice can't end past the end of the string. The built-in min does that.
    6. In notifyAll, create an empty []string named texts, loop over the notifiers, append notifier.send(message) each time, and return texts.

    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 min exists since Go 1.21, so it works in Go 1.22. In older code you would see an if instead.
    • 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 an EmailNotifier, the SMS text for an SmsNotifier.

    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 read SMS 111: Exactly twenty chars and SMS 222: Exactly twenty chars.
    • Short messages. Writing message[:20] without the min panics at run time for a message shorter than 20 bytes. For "Hi" the error is slice 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 with string(...). 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 notifyAll defeats 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.

    Keep going with Go500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free