Interface as Contract
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 27 of 107.
Thinking of interfaces as contracts helps you design better Go programs. When a function accepts an interface type, it's saying: "I don't care what concrete type you give me, as long as it fulfills this contract."
This contract-based thinking lets you write functions that focus on behavior rather than specific types. Consider a function that needs to process data from various sources:
type DataSource interface {
FetchData() string
}
func ProcessData(src DataSource) {
data := src.FetchData()
fmt.Println("Processing:", data)
}The ProcessData function doesn't know or care whether it receives a database connection, an API client, or a file reader. It only requires that the input can FetchData(). Any type honoring this contract works.
type Database struct{ ConnectionString string }
func (d Database) FetchData() string { return "data from DB" }
type APIClient struct{ Endpoint string }
func (a APIClient) FetchData() string { return "data from API" }
func main() {
db := Database{ConnectionString: "localhost"}
api := APIClient{Endpoint: "https://api.example.com"}
ProcessData(db) // Works!
ProcessData(api) // Also works!
}This contract approach makes your code more testable too. During testing, you can create a mock type that satisfies the interface without needing real databases or network connections. The function under test doesn't know the difference because the contract is fulfilled.
Challenge
EasyLet's build a notification system that demonstrates how interfaces act as contracts. You'll create a Notifier interface that defines what it means to send a notification, then implement multiple notification channels that all honor this contract.
You'll organize your code across two files:
notifiers.go: Define aNotifierinterface with a single methodNotify(message string) string. Then create three different structs that satisfy this contract:ConsoleNotifierwith aPrefixfieldFileNotifierwith aFilenamefieldWebhookNotifierwith aURLfield
Notifymethod to return a string describing how the notification would be sent.main.go: Create a function calledBroadcastAlertthat accepts a slice ofNotifiervalues and a message string. This function should callNotifyon each notifier and print the results. Read configuration from input, create all three notifier types, and broadcast an alert through all of them.
The following inputs will be provided:
- Line 1: Console prefix
- Line 2: Filename
- Line 3: Webhook URL
- Line 4: Alert message
Your Notify methods should return strings in these formats:
- ConsoleNotifier:
[Prefix] [message] - FileNotifier:
Writing to [Filename]: [message] - WebhookNotifier:
POST [URL]: [message]
For example, given ALERT:, log.txt, https://hooks.example.com, and Server down!, your output should be:
ALERT: Server down!
Writing to log.txt: Server down!
POST https://hooks.example.com: Server down!The power of this design is that your BroadcastAlert function doesn't know or care about the specific notifier types—it only requires that each one fulfills the Notifier contract. You could add a SlackNotifier or EmailNotifier later without changing the broadcast function at all.
Cheat sheet
Interfaces in Go act as contracts that define required behavior without specifying concrete types. When a function accepts an interface type, any type implementing the interface's methods can be used.
Define an interface by specifying method signatures:
type DataSource interface {
FetchData() string
}Functions accepting interfaces work with any type that satisfies the contract:
func ProcessData(src DataSource) {
data := src.FetchData()
fmt.Println("Processing:", data)
}Multiple types can implement the same interface:
type Database struct{ ConnectionString string }
func (d Database) FetchData() string { return "data from DB" }
type APIClient struct{ Endpoint string }
func (a APIClient) FetchData() string { return "data from API" }
func main() {
db := Database{ConnectionString: "localhost"}
api := APIClient{Endpoint: "https://api.example.com"}
ProcessData(db) // Works!
ProcessData(api) // Also works!
}This contract-based approach enables:
- Functions that focus on behavior rather than specific types
- Better testability through mock implementations
- Extensibility without modifying existing code
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
)
// BroadcastAlert sends a message through all notifiers
// TODO: Implement this function to:
// - Accept a slice of Notifier values and a message string
// - Call Notify on each notifier and print the results
func BroadcastAlert(notifiers []Notifier, message string) {
// TODO: Loop through notifiers and print each notification result
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Read console prefix
scanner.Scan()
prefix := scanner.Text()
// Read filename
scanner.Scan()
filename := scanner.Text()
// Read webhook URL
scanner.Scan()
url := scanner.Text()
// Read alert message
scanner.Scan()
message := scanner.Text()
// TODO: Create instances of all three notifier types
// - ConsoleNotifier with the prefix
// - FileNotifier with the filename
// - WebhookNotifier with the URL
// TODO: Create a slice of Notifier containing all three notifiers
// TODO: Call BroadcastAlert with the notifiers and message
_ = prefix
_ = filename
_ = url
_ = message
fmt.Println("TODO: Implement the solution")
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool