-
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
242: Practice Exercise: Building a Plugin System with Interfaces
I've spent a lot of my career maintaining legacy systems where adding a single new feature felt like playing Jenga. You touch one thing, and three unrelated modules collapse. Usually, this happens because the core logic knows way too much about the specific details of its peripherals. Today, I want to show you how to solve that using interfaces to build a plugin system.
The Switch Statement Trap
Let's say we're building a data reporting tool. I start by writing a simple function that exports data based on a format string. It feels intuitive at first.
type Report struct {
Title string
Content string
}
func ExportReport(r Report, format string) {
if format == "csv" {
fmt.Printf("Exporting %s as CSV...\n", r.Title)
// CSV logic here
} else if format == "json" {
fmt.Printf("Exporting %s as JSON...\n", r.Title)
// JSON logic here
} else {
fmt.Println("Unsupported format")
}
}
This works for two formats. But imagine we have twenty. Or worse, imagine these formats are defined by different teams in different packages. I'd have to import every single exporter package into my main logic, and every time someone adds a new format, I have to modify this ExportReport function. That's a violation of the Open/Closed Principle, and it's a recipe for merge conflicts.
Defining the Contract
I need a way to tell the core logic: "I don't care how you export the data, as long as you have a method that takes a Report and returns an error." This is where the interface comes in. I'll define a contract that any "plugin" must satisfy.
type Exporter interface {
Export(r Report) error
}
Now, instead of a switch statement, I can just accept the interface. I'll try implementing a CSV version to see if it feels right.
type CSVExporter struct{}
func (c CSVExporter) Export(r Report) error {
fmt.Printf("Writing %s to a CSV file...\n", r.Title)
return nil
}
type JSONExporter struct{}
func (j JSONExporter) Export(r Report) error {
fmt.Printf("Writing %s to a JSON file...\n", r.Title)
return nil
}
Now my main function looks like this: func ExportReport(r Report, e Exporter). It's much cleaner. But there's still a problem: someone, somewhere, still has to decide which concrete struct (CSVExporter or JSONExporter) to instantiate. I want the core system to be completely oblivious to the concrete types.
Decoupling via a Registry
To make this a true plugin system, I'll create a registry. Think of it as a map where plugins can "sign up" to be used. I'll use a map of strings to interfaces.
var registry = make(map[string]Exporter)
func RegisterExporter(name string, e Exporter) {
registry[name] = e
}
func GetExporter(name string) (Exporter, error) {
e, ok := registry[name]
if !ok {
return nil, fmt.Errorf("exporter %s not found", name)
}
return e, nil
}
Wait, if I put this in my main package, I'm still manually calling RegisterExporter in main(). To make it really feel like a plugin, I want the plugins to register themselves. In Go, the init() function is perfect for this. It runs before main().
If I move CSVExporter into its own package (let's call it csvplugin), I can do this:
package csvplugin
import "myproject/exporter"
type CSVExporter struct{}
func (c CSVExporter) Export(r Report) error { /* ... */ }
func init() {
exporter.RegisterExporter("csv", CSVExporter{})
}
Tying it all together
Now, the magic happens. In my main application, I just need to import the plugin packages. I don't even need to call their functions; the init() functions handle the registration behind the scenes. I use a "blank import" (the underscore) to tell Go, "I need the side effects of this package, but I'm not calling its code directly."
import (
_ "myproject/csvplugin"
_ "myproject/jsonplugin"
)
func main() {
report := Report{Title: "Quarterly Sales", Content: "..."}
// The user tells us what they want
format := "csv"
exp, err := GetExporter(format)
if err != nil {
log.Fatal(err)
}
exp.Export(report)
}
I've just moved the knowledge of "what exists" out of my business logic and into the plugins themselves. If I want to add an XML exporter tomorrow, I just create a new package, implement the Exporter interface, and add one import line. The core logic never changes. That's how you build a system that actually scales.
📋 Practical Task
Exercise: Implementing a Multi-Gateway Payment System
You are building a checkout system that needs to support multiple payment providers (like Stripe, PayPal, and Square). Instead of hardcoding each provider, you need to implement a plugin-based architecture using interfaces.
Your requirements:
- Define a
PaymentProcessorinterface with a methodProcessPayment(amount float64) error. - Create a
PaymentRegistrythat allows processors to be registered by a string name (e.g., "stripe"). - Implement two concrete processors:
StripeProcessorandPayPalProcessor. Each should simply print a message saying "Processing [amount] via [Provider]..." and returnnil. - In your
mainfunction, simulate a user choosing a provider via a variable. Use the registry to fetch the correct processor and execute the payment. - Challenge: Ensure that if a user requests a provider that hasn't been registered, your system returns a clear, descriptive error rather than panicking.
There are no comments for now.