Learning Interfaces in Go: Concepts, Examples, and Best Practices
Interfaces are one of Go’s most important abstraction tools. An interface describes behavior as a set of method signatures. A concrete type satisfies an interface implicitly simply by implementing the required methods—there is no separate implements declaration.
What Is an Interface?
An interface is a set of methods. For example:
type Speaker interface {
Speak() string
}
type Person struct {
Name string
}
func (p Person) Speak() string {
return "Hello, my name is " + p.Name
}Because Person has the required Speak() string method, it satisfies Speaker:
var s Speaker
s = Person{Name: "Alice"}
fmt.Println(s.Speak()) // Hello, my name is AliceThis implicit implementation keeps interfaces decoupled from the concrete types that happen to satisfy them.
Why Use Interfaces?
Interfaces are useful when they express a real behavioral boundary:
- Loose coupling → callers depend on required behavior rather than a specific implementation.
- Replaceable implementations → different types can be used through the same API.
- Testability → small interfaces make it easy to provide controlled test implementations.
- Composition → interfaces can combine behavior without inheritance hierarchies.
Do not create an interface solely because a concrete type exists. In idiomatic Go, interfaces are often defined by the package that consumes the behavior, not by the package that implements it.
Example: Notification Providers
Suppose an application can send notifications by email or SMS:
type Notifier interface {
Notify() string
}
type Email struct{ Address string }
func (e Email) Notify() string {
return "Sending email to " + e.Address
}
type SMS struct{ Number string }
func (s SMS) Notify() string {
return "Sending SMS to " + s.Number
}Both types can be used through Notifier:
var n Notifier
n = Email{Address: "example@example.com"}
fmt.Println(n.Notify())
n = SMS{Number: "123-456-7890"}
fmt.Println(n.Notify())A function can accept the interface directly:
func SendNotification(n Notifier) {
fmt.Println(n.Notify())
}
func main() {
SendNotification(Email{Address: "example@example.com"})
SendNotification(SMS{Number: "123-456-7890"})
}Adding another notifier does not require changing SendNotification; the new type only needs to satisfy the interface.
Without an Interface
Without a shared abstraction, you might end up with a separate function for each concrete type:
func SendEmail(e Email) {
fmt.Println(e.Notify())
}
func SendSMS(s SMS) {
fmt.Println(s.Notify())
}That may be perfectly fine when the operations are genuinely different. The interface becomes valuable when callers should treat the implementations uniformly.
Interface Composition
Small interfaces can be embedded into larger ones:
type Reader interface {
Read() string
}
type Writer interface {
Write() string
}
type ReadWriter interface {
Reader
Writer
}Any type implementing both methods satisfies ReadWriter.
The standard library uses this style extensively, for example with io.Reader, io.Writer, and io.ReadWriter.
The Empty Interface and any
Historically, interface{} was used to represent a value of any type:
func printValue(v interface{}) {
fmt.Println(v)
}Modern Go provides any as an alias for interface{}:
func printValue(v any) {
fmt.Println(v)
}Use any when the value truly can be any type, such as generic serialization boundaries. If the code operates on a specific set of behaviors, a typed interface or a generic type parameter usually communicates more information.
Method Sets Matter
Whether a value or pointer satisfies an interface depends on the method receiver:
type Counter struct{}
func (c *Counter) Reset() {}
type Resetter interface {
Reset()
}Here *Counter satisfies Resetter, but Counter does not, because Reset has a pointer receiver.
This distinction is important when assigning values to interface variables or accepting interfaces as function parameters.
Best Practices
- Keep interfaces small and focused. One-method interfaces are common and useful in Go.
- Define interfaces where behavior is consumed when practical.
- Accept interfaces, but return concrete types unless an abstraction is genuinely required.
- Do not introduce interfaces prematurely just to imitate class-based object-oriented design.
- Prefer
anyonly when no stronger type information is available or useful. - Remember that a nil concrete pointer stored inside an interface can make the interface itself non-nil; handle nil semantics carefully in APIs.
Conclusion
A Go interface is a behavioral contract satisfied implicitly by matching method sets. Small, consumer-oriented interfaces make packages easier to compose and test while keeping concrete implementations independent. Use them where multiple implementations genuinely share behavior, not as a mandatory wrapper around every type.