Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
237: Interface Satisfaction Implicitly
I remember when I first switched to Go from Java. I spent a good twenty minutes staring at a piece of code, wondering why the developer had forgotten to declare that a class implemented an interface. I kept looking for the implements keyword, but it just wasn't there. That's when I realized Go does things differently.
Setting up a simple Sender
Let's play this out. I want to build a small notification system. I've got different ways to send messages—email, SMS, maybe Slack—but the rest of my app shouldn't care how the message is sent, just that it can be sent. So, I'll start by defining an interface.
type Sender interface {
Send(message string) error
}
Now, I'll create a concrete type for sending emails. I'll give it a from address so it's a bit more realistic.
type EmailSender struct {
FromAddress string
}
func (e EmailSender) Send(msg string) error {
fmt.Printf("Sending email from %s: %s\n", e.FromAddress, msg)
return nil
}
At this point, if you're coming from an OOP background, you're probably waiting for me to write type EmailSender implements Sender. But I'm not going to. I'm just going to try and use it in a function that requires a Sender and see if the compiler yells at me.
Wait, where is the 'implements' keyword?
Let's write a function that takes the interface and call it with our EmailSender.
func NotifyUser(s Sender, msg string) {
err := s.Send(msg)
if err != nil {
fmt.Println("Error sending notification:", err)
}
}
func main() {
email := EmailSender{FromAddress: "bot@company.com"}
NotifyUser(email, "Your order has shipped!")
}
I'll run this. And... it works. No compiler errors, no warnings. The EmailSender was accepted as a Sender simply because it possessed the Send method with the correct signature.
This is what we call implicit satisfaction. In Go, you don't explicitly announce that a type satisfies an interface. If the type has the required methods, it satisfies the interface. Period. I actually love this because it decouples the definition of the interface from the implementation. I could define an interface in my own package to describe behavior from a third-party library that I don't even have the source code for.
Testing the boundaries
But what happens if I mess up? Let's see if the compiler is actually checking this or if I've stumbled into some strange Go magic. I'll create another sender, but I'll give the method a slightly different name.
type SMSSender struct {
PhoneNumber string
}
func (s SMSSender) Dispatch(msg string) error {
fmt.Printf("Sending SMS to %s: %s\n", s.PhoneNumber, msg)
return nil
}
I called it Dispatch instead of Send. Now, let's try to pass an SMSSender into NotifyUser:
sms := SMSSender{PhoneNumber: "555-0199"}
NotifyUser(sms, "Your code is 1234")
The compiler catches it immediately. It tells me that SMSSender does not implement Sender because the Send method is missing. This proves that the check is rigorous; it's just invisible. You don't have to maintain a giant list of implements clauses at the top of your structs. You just write the methods, and the types "fit" where they are needed.
📋 Practical Task
Building a Pluggable Data Archiver
You are building a system that needs to archive different types of data to different destinations (e.g., a Local Disk, a Cloud Bucket). You need to ensure that any archiver used by the system satisfies a specific interface without explicitly declaring it.
Your Task:
- Define an interface named
Archiverthat requires a methodArchive(data []byte) error. - Create a struct named
DiskArchiverwith a fieldFilePath string. - Implement the
Archivemethod forDiskArchiver(it can just print "Saving to [FilePath]"). - Create a struct named
CloudArchiverwith a fieldBucketName string. - Implement the
Archivemethod forCloudArchiver(it can just print "Uploading to [BucketName]"). - Write a function called
PerformBackupthat takes anArchiverand a slice of bytes, then calls theArchivemethod. - In your
mainfunction, instantiate both archivers and pass them intoPerformBackupto demonstrate implicit satisfaction.
There are no comments for now.