What Comes After Custom Types?
In the last few parts, we've learned how to create our own data types with structs
and attach behavior to them with methods
. This is great for creating organized, self-contained components.
But what if we want to write a function that can work with different types, as long as they share a common behavior? For example, a function that can process anything that can be "saved" to a file, whether it's a User
struct, a Product
struct, or an Invoice
struct.
This is where Interfaces come in. An interface is one of Go's most powerful features for writing flexible and abstract code.
1. What is an Interface? (The Contract)
An interface in Go is a type that defines a set of method signatures. It's not a piece of data; it's a contract. It says, "Any type that wants to be considered a member of my group must have these specific methods."
Analogy: An electrical wall socket (stopkontak). The socket is the interface. It has a contract: "Anything that wants to get power from me must have a two-pronged plug that fits my shape." The socket doesn't care if you plug in a phone charger, a fan, or a TV. As long as the device's plug matches the contract, it works.
Defining an Interface
Let's define a simple interface for anything that can make a sound.
package main
import "fmt"
// This is our contract.
// Any type that wants to be a "SoundMaker" MUST have a method
// called MakeSound() that returns a string.
type SoundMaker interface {
MakeSound() string
}
2. Implementing an Interface (Implicitly)
Here's a key difference between Go and other languages like Java or C#: you don't explicitly say that your type implements an interface.
A type in Go implicitly satisfies an interface if it defines all the methods specified in that interface. No implements
keyword needed.
Let's create two different types that will satisfy our SoundMaker
contract.
// (Continuing from the code above)
// --- First Type: Dog ---
type Dog struct {
Name string
}
// Dog now satisfies the SoundMaker interface because it has the MakeSound() method.
func (d Dog) MakeSound() string {
return "Woof!"
}
// --- Second Type: Cat ---
type Cat struct {
Name string
}
// Cat also satisfies the SoundMaker interface.
func (c Cat) MakeSound() string {
return "Meow!"
}
func main() {
// We'll see how to use these in the next step.
}
Even though Dog
and Cat
are completely different structs, Go considers both of them to be SoundMaker
s because they both fulfill the contract.
3. Using the Interface (The Payoff)
Now for the magic. We can create a function that accepts our SoundMaker
interface as a parameter. This function doesn't know or care whether it's receiving a Dog
or a Cat
. It only knows that whatever it receives, it's guaranteed to have a MakeSound()
method.
The Flexible Function
Let's create a function that makes any SoundMaker
speak.
// (Continuing from the code above)
// This function can accept ANY type that satisfies the SoundMaker interface.
func letThemSpeak(s SoundMaker) {
fmt.Println("The sound is:", s.MakeSound())
}
func main() {
// Create an instance of Dog and Cat
myDog := Dog{Name: "Buddy"}
myCat := Cat{Name: "Whiskers"}
// We can pass both myDog and myCat to the same function!
letThemSpeak(myDog) // Prints: The sound is: Woof!
letThemSpeak(myCat) // Prints: The sound is: Meow!
}
This is incredibly powerful. It allows us to write generic functions that are decoupled from specific data types, making our code much more reusable and easier to maintain.
Putting It All Together: A Full Example
Here is the complete, runnable program that demonstrates everything we've discussed.
package main
import "fmt"
// 1. The contract (the interface)
type SoundMaker interface {
MakeSound() string
}
// 2. The first type that satisfies the contract
type Dog struct {
Name string
}
func (d Dog) MakeSound() string {
return "Woof!"
}
// 3. The second type that also satisfies the contract
type Cat struct {
Name string
}
func (c Cat) MakeSound() string {
return "Meow!"
}
// 4. The flexible function that works with any SoundMaker
func letThemSpeak(s SoundMaker) {
fmt.Println("The sound is:", s.MakeSound())
}
func main() {
// Create instances of our concrete types
myDog := Dog{Name: "Buddy"}
myCat := Cat{Name: "Whiskers"}
// Call the same function with different types
letThemSpeak(myDog)
letThemSpeak(myCat)
}
Conclusion
Interfaces are a core concept in Go for writing clean, abstract, and testable code. By defining contracts (interfaces) instead of depending on concrete types (structs), you can build systems that are much more flexible and scalable.
You've now learned how to:
- Define an interface as a set of method signatures.
- Implicitly satisfy an interface by implementing its methods on a struct.
- Write generic functions that operate on interfaces, not specific types.
In the next part, we'll cover another of Go's most defining features: its elegant approach to Error Handling. See you there!
Top comments (0)