What It Is
The Adapter pattern makes a type with the wrong interface fit the interface your code expects — like a travel plug between a foreign socket and your charger.
You cannot change the old type (it comes from a library, or other code depends on it). So you wrap it in a small type that implements the interface you need and translates the calls.
Example: An Old Printer as a Logger
Our code wants a Logger with Log(message). The legacy type only offers WriteLine(prefix, text). The adapter holds the old type and translates one call into the other.
The adapter translates the call
run() works with any Logger; the adapter makes the legacy printer one of them without touching its code.
Output: Go program and captured output
package main
import "fmt"
// The interface our code expects.
type Logger interface {
Log(message string)
}
// An old library type with a different method — we cannot change it.
type LegacyPrinter struct{}
func (LegacyPrinter) WriteLine(prefix, text string) {
fmt.Println(prefix + " | " + text)
}
// The adapter makes the old type fit the new interface.
type PrinterAdapter struct {
legacy LegacyPrinter
}
func (a PrinterAdapter) Log(message string) {
a.legacy.WriteLine("LOG", message)
}
func run(l Logger) {
l.Log("service started")
l.Log("request handled")
}
func main() {
run(PrinterAdapter{legacy: LegacyPrinter{}})
}
When to Use It
Use it when working code and a needed interface do not match, and you cannot (or should not) change either side — third-party libraries, legacy code, generated code.
Skip it when you own both sides — then just change one of them to match. An adapter between two types you control is a workaround for a decision you could simply make.
Pros and Cons
Pros: old and new code cooperate without changes to either; the translation lives in one small, testable type.
Cons: one more hop to read through; many adapters stacked on each other are a sign the interfaces themselves need rework.