The Secret Life of Go: Interfaces
Source: Dev.to
The Power of Implicit Contracts
Tuesday morning brought fog. Ethan descended to the archive carrying coffee and a small box of biscotti.
Eleanor looked up. “Italian today?”
“The baker said biscotti are designed to fit perfectly into coffee cups—form following function.”
She smiled. “Perfect. Today we’re talking about interfaces—Go’s way of defining what something can do, regardless of what it is.”
A Simple Example
package main
import "fmt"
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return "Meow!"
}
func main() {
dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}
fmt.Println(dog.Name, "says:", dog.Speak())
fmt.Println(cat.Name, "says:", cat.Speak())
}
Output
Buddy says: Woof!
Whiskers says: Meow!
Two different types—Dog and Cat. Both have a Speak() method that returns a string, yet they are unrelated types.
Defining an Interface
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return "Meow!"
}
func MakeItSpeak(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}
MakeItSpeak(dog)
MakeItSpeak(cat)
}
Output
Woof!
Meow!
Speaker defines a contract: any type that has a Speak() string method satisfies the interface. The function MakeItSpeak accepts any Speaker, and both Dog and Cat qualify.
Note: In Go, you never explicitly declare that a type implements an interface. If the methods match, the implementation is implicit.
Why Implicit?
- Flexibility: You can define an interface that existing types already satisfy, even if those types were written before your interface existed.
- No upfront coupling: Types aren’t forced to declare relationships, keeping code loosely coupled.
Real‑World Example: Writing to Different Destinations
package main
import (
"fmt"
"os"
)
type Writer interface {
Write([]byte) (int, error)
}
type ConsoleWriter struct{}
func (cw ConsoleWriter) Write(data []byte) (int, error) {
n, err := fmt.Println(string(data))
return n, err
}
func WriteMessage(w Writer, message string) {
w.Write([]byte(message))
}
func main() {
console := ConsoleWriter{}
WriteMessage(console, "Hello, Console!")
file, _ := os.Create("output.txt")
defer file.Close()
WriteMessage(file, "Hello, File!")
}
The Writer interface has a single method Write([]byte) (int, error).
ConsoleWriterimplements it.os.Filefrom the standard library also implements it, even though it was written long before our interface existed.
Thus, WriteMessage works with any type that satisfies Writer—console, file, network connection, in‑memory buffer, etc.
Polymorphism with Interfaces
package main
import "fmt"
type Shape interface {
Area() float64
Perimeter() float64
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * 3.14159 * c.Radius
}
func PrintShapeInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
func main() {
rect := Rectangle{Width: 5, Height: 3}
circle := Circle{Radius: 4}
PrintShapeInfo(rect)
PrintShapeInfo(circle)
}
Output
Area: 15.00, Perimeter: 16.00
Area: 50.27, Perimeter: 25.13
Shape requires Area() and Perimeter(). Both Rectangle and Circle satisfy it, so PrintShapeInfo can operate on any Shape without caring about the concrete type.
This is similar to duck typing, but Go checks the implementation at compile time, providing type safety.
The Empty Interface (any)
package main
import "fmt"
func PrintAnything(v any) {
fmt.Println(v)
}
func main() {
PrintAnything(42)
PrintAnything("hello")
PrintAnything(true)
PrintAnything([]int{1, 2, 3})
}
Output
42
hello
true
[1 2 3]
any is an alias for the empty interface interface{}. Since every type has at least zero methods, every type satisfies it. Use it sparingly, as it removes compile‑time type safety.
Type Assertions
package main
import "fmt"
func Describe(v any) {
// Type assertion with comma‑ok
if str, ok := v.(string); ok {
fmt.Printf("String: %s (length %d)\n", str, len(str))
return
}
if num, ok := v.(int); ok {
fmt.Printf("Integer: %d (doubled: %d)\n", num, num*2)
return
}
fmt.Printf("Unknown type: %T\n", v)
}
func main() {
Describe("hello")
Describe(42)
Describe(true)
}
Output
String: hello (length 5)
Integer: 42 (doubled: 84)
Unknown type: bool
If you omit the ok check (str := v.(string)), the program panics when the assertion fails.
Type Switches
package main
import "fmt"
func Describe(v any) {
switch val := v.(type) {
case string:
fmt.Printf("String: %s (length %d)\n", val, len(val))
case int:
fmt.Printf("Integer: %d (doubled: %d)\n", val, val*2)
case bool:
fmt.Printf("Boolean: %t\n", val)
default:
fmt.Printf("Unknown type: %T\n", val)
}
}
func main() {
Describe("hello")
Describe(42)
Describe(true)
Describe(3.14)
}
Output
String: hello (length 5)
Integer: 42 (doubled: 84)
Boolean: true
Unknown type: float64
The switch val := v.(type) construct cleanly handles multiple possible concrete types.
Interface Hierarchy (From Specific to General)
-
Specific Interface
type Reader interface { Read([]byte) (int, error) }Only types that implement
Read([]byte) (int, error)satisfyReader. -
Empty Interface (
any)type any interface{}Every type satisfies this, making it the most general interface. Use it only when you truly need to accept values of any type.