In the previous tutorial, you learned about structs, methods, and composition. Now it is time to learn about interfaces — one of Go’s most powerful features.
Interfaces in Go are different from most languages. There is no implements keyword. If a type has the right methods, it automatically satisfies the interface. This is called implicit implementation.
What is an Interface?
An interface defines a set of method signatures. Any type that implements all those methods satisfies the interface:
package main
import (
"fmt"
"math"
)
// Interface — defines behavior
type Shape interface {
Area() float64
Perimeter() float64
}
// Circle implements Shape (implicitly)
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// Rectangle implements Shape (implicitly)
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// This function accepts any Shape
func printShape(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
func main() {
circle := Circle{Radius: 5}
rect := Rectangle{Width: 10, Height: 3}
fmt.Print("Circle — ")
printShape(circle)
fmt.Print("Rectangle — ")
printShape(rect)
}
Output:
Circle — Area: 78.54, Perimeter: 31.42
Rectangle — Area: 30.00, Perimeter: 26.00
Notice: Circle and Rectangle never say they implement Shape. They just have the Area() and Perimeter() methods. Go checks this at compile time.
Why Implicit Implementation Matters
In Java or C#, you must write implements Shape. In Go, you don’t. This means:
- You can define interfaces after the types exist. You can create an interface for types from other packages, even standard library types.
- Small interfaces are easy to create. You don’t need to go back and add
implementsto existing code. - Less coupling. The type doesn’t need to know about the interface.
This design encourages small, focused interfaces.
The Empty Interface and any
The empty interface has no methods. Every type satisfies it:
package main
import "fmt"
func printAnything(value interface{}) {
fmt.Printf("Type: %T, Value: %v\n", value, value)
}
func main() {
printAnything(42)
printAnything("hello")
printAnything(3.14)
printAnything(true)
printAnything([]int{1, 2, 3})
}
Output:
Type: int, Value: 42
Type: string, Value: hello
Type: float64, Value: 3.14
Type: bool, Value: true
Type: []int, Value: [1 2 3]
Since Go 1.18, you can use any instead of interface{}. They are the same:
func printAnything(value any) {
fmt.Printf("Type: %T, Value: %v\n", value, value)
}
Use any sparingly. It turns off type checking. Prefer specific interfaces when possible.
Type Assertions
A type assertion extracts the concrete type from an interface:
package main
import "fmt"
func main() {
var value interface{} = "hello"
// Type assertion — extract the string
str := value.(string)
fmt.Println("String:", str)
// This would panic if the type is wrong:
// num := value.(int) // panic: interface conversion
// Safe type assertion with ok check
num, ok := value.(int)
if ok {
fmt.Println("Integer:", num)
} else {
fmt.Println("Not an integer")
}
// Common pattern
if str, ok := value.(string); ok {
fmt.Println("It is a string:", str)
}
}
Output:
String: hello
Not an integer
It is a string: hello
Always use the two-value form (value, ok) to avoid panics. The single-value form panics if the type does not match.
Type Switches
A type switch checks the type of an interface value. It is cleaner than multiple type assertions:
package main
import "fmt"
func describe(value interface{}) string {
switch v := value.(type) {
case int:
return fmt.Sprintf("Integer %d (doubled: %d)", v, v*2)
case float64:
return fmt.Sprintf("Float %.2f", v)
case string:
return fmt.Sprintf("String %q (length: %d)", v, len(v))
case bool:
if v {
return "Boolean: true"
}
return "Boolean: false"
case nil:
return "nil"
default:
return fmt.Sprintf("Unknown type: %T", v)
}
}
func main() {
values := []interface{}{42, 3.14, "hello", true, nil, []int{1, 2}}
for _, v := range values {
fmt.Println(describe(v))
}
}
Output:
Integer 42 (doubled: 84)
Float 3.14
String "hello" (length: 5)
Boolean: true
nil
Unknown type: []int
The variable v gets the correct type inside each case branch. This makes it safe to use type-specific operations.
Interface Composition
You can combine small interfaces into larger ones:
package main
import "fmt"
type Reader interface {
Read(data []byte) (int, error)
}
type Writer interface {
Write(data []byte) (int, error)
}
// ReadWriter combines Reader and Writer
type ReadWriter interface {
Reader
Writer
}
// Closer is another small interface
type Closer interface {
Close() error
}
// ReadWriteCloser combines all three
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// A simple implementation
type Buffer struct {
data []byte
}
func (b *Buffer) Read(p []byte) (int, error) {
n := copy(p, b.data)
return n, nil
}
func (b *Buffer) Write(p []byte) (int, error) {
b.data = append(b.data, p...)
return len(p), nil
}
func (b *Buffer) Close() error {
b.data = nil
return nil
}
func main() {
var rw ReadWriter = &Buffer{}
rw.Write([]byte("hello"))
buf := make([]byte, 10)
n, _ := rw.Read(buf)
fmt.Println("Read:", string(buf[:n]))
}
Output:
Read: hello
This is how the Go standard library works. The io package defines small interfaces (Reader, Writer, Closer) and combines them into larger ones (ReadWriter, ReadWriteCloser).
Common Standard Library Interfaces
Go’s standard library has several important interfaces you should know:
fmt.Stringer
Implement String() to control how your type is printed:
package main
import "fmt"
type Color struct {
R, G, B uint8
}
func (c Color) String() string {
return fmt.Sprintf("rgb(%d, %d, %d)", c.R, c.G, c.B)
}
func main() {
red := Color{R: 255, G: 0, B: 0}
blue := Color{R: 0, G: 0, B: 255}
fmt.Println(red) // rgb(255, 0, 0)
fmt.Println(blue) // rgb(0, 0, 255)
}
error Interface
The error interface is one of Go’s most used interfaces:
package main
import "fmt"
// The error interface is: type error interface { Error() string }
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s — %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{Field: "age", Message: "must be positive"}
}
if age > 150 {
return &ValidationError{Field: "age", Message: "must be under 150"}
}
return nil
}
func main() {
if err := validateAge(-5); err != nil {
fmt.Println(err)
}
if err := validateAge(25); err != nil {
fmt.Println(err)
} else {
fmt.Println("Age 25 is valid")
}
}
Output:
validation error: age — must be positive
Age 25 is valid
sort.Interface
Implement three methods to make any collection sortable:
package main
import (
"fmt"
"sort"
)
type Employee struct {
Name string
Salary float64
}
// ByName implements sort.Interface
type ByName []Employee
func (b ByName) Len() int { return len(b) }
func (b ByName) Less(i, j int) bool { return b[i].Name < b[j].Name }
func (b ByName) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
func main() {
employees := []Employee{
{Name: "Sam", Salary: 60000},
{Name: "Alex", Salary: 75000},
{Name: "Jordan", Salary: 55000},
}
sort.Sort(ByName(employees))
for _, e := range employees {
fmt.Printf("%-8s $%.0f\n", e.Name, e.Salary)
}
}
Output:
Alex $75000
Jordan $55000
Sam $60000
In modern Go, you can also use sort.Slice which is simpler for one-off sorting:
sort.Slice(employees, func(i, j int) bool {
return employees[i].Salary > employees[j].Salary
})
Accept Interfaces, Return Structs
This is an important Go design principle. Functions should accept interfaces as parameters and return concrete types:
package main
import (
"fmt"
"io"
"strings"
)
// Accept an interface — any io.Reader works
func countBytes(r io.Reader) (int, error) {
buf := make([]byte, 1024)
total := 0
for {
n, err := r.Read(buf)
total += n
if err == io.EOF {
break
}
if err != nil {
return total, err
}
}
return total, nil
}
func main() {
// strings.NewReader returns a concrete type, but we pass it as io.Reader
reader := strings.NewReader("Hello, Go interfaces!")
count, err := countBytes(reader)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Counted %d bytes\n", count)
}
Output:
Counted 21 bytes
Why this pattern?
- Accept interfaces: Your function works with any type that satisfies the interface. More flexible, easier to test.
- Return structs: The caller gets the full concrete type with all its methods. No information is lost.
A Complete Example
Here is a program that shows interfaces in action with a simple notification system:
package main
import "fmt"
// Small, focused interface
type Notifier interface {
Notify(message string) error
}
// Email notification
type EmailNotifier struct {
Address string
}
func (e *EmailNotifier) Notify(message string) error {
fmt.Printf("[Email to %s]: %s\n", e.Address, message)
return nil
}
// SMS notification
type SMSNotifier struct {
Phone string
}
func (s *SMSNotifier) Notify(message string) error {
fmt.Printf("[SMS to %s]: %s\n", s.Phone, message)
return nil
}
// Slack notification
type SlackNotifier struct {
Channel string
}
func (sl *SlackNotifier) Notify(message string) error {
fmt.Printf("[Slack #%s]: %s\n", sl.Channel, message)
return nil
}
// Send to multiple notifiers — accepts the interface
func broadcast(notifiers []Notifier, message string) {
for _, n := range notifiers {
if err := n.Notify(message); err != nil {
fmt.Printf("Error: %v\n", err)
}
}
}
func main() {
fmt.Println("=== GO-8: Interfaces and Polymorphism ===")
fmt.Println()
notifiers := []Notifier{
&EmailNotifier{Address: "alex@example.com"},
&SMSNotifier{Phone: "+49-123-456"},
&SlackNotifier{Channel: "alerts"},
}
broadcast(notifiers, "Server is back online")
fmt.Println()
broadcast(notifiers, "Deploy completed successfully")
}
Output:
=== GO-8: Interfaces and Polymorphism ===
[Email to alex@example.com]: Server is back online
[SMS to +49-123-456]: Server is back online
[Slack #alerts]: Server is back online
[Email to alex@example.com]: Deploy completed successfully
[SMS to +49-123-456]: Deploy completed successfully
[Slack #alerts]: Deploy completed successfully
Adding a new notification type (like a webhook) is easy. Just create a struct with a Notify method. No existing code needs to change.
Common Mistakes
1. Using too large interfaces.
// Too big — hard to implement and test
type Service interface {
GetUser(id int) (*User, error)
CreateUser(u *User) error
DeleteUser(id int) error
ListUsers() ([]*User, error)
GetOrder(id int) (*Order, error)
// ... 20 more methods
}
// Better — small, focused interfaces
type UserGetter interface {
GetUser(id int) (*User, error)
}
type UserCreator interface {
CreateUser(u *User) error
}
Go proverb: “The bigger the interface, the weaker the abstraction.” Keep interfaces small (1-3 methods).
2. Returning an interface instead of a concrete type.
// Avoid this — returns interface
func NewLogger() Logger {
return &FileLogger{...}
}
// Better — returns concrete type
func NewFileLogger() *FileLogger {
return &FileLogger{...}
}
Return concrete types so callers get full access to the type. Accept interfaces in function parameters for flexibility.
3. Forgetting the ok check in type assertions.
var v interface{} = "hello"
// Dangerous — panics if v is not an int
num := v.(int) // panic!
// Safe — check with ok
num, ok := v.(int)
if !ok {
fmt.Println("Not an integer")
}
Always use the two-value form unless you are certain about the type.
Source Code
You can find the complete source code for this tutorial on GitHub:
Related Articles
- Go Tutorial #7: Structs, Methods, and Composition — Structs and methods
- Go Tutorial #5: Control Flow — if, switch, type switches
What’s Next?
In the next tutorial, Go Tutorial #9: Pointers, you will learn:
- What pointers are and how they work in Go
- The
&and*operators - When to use pointers vs values
- Nil pointers and how to handle them
- How Go pointers compare to Rust’s borrowing system
This is part 8 of the Go Tutorial series. Follow along to learn Go from scratch.