Type Switches & Common Interfaces
Type switches and assertions let you work with interface values dynamically. Go's standard library also provides essential interfaces that you'll use constantly. In this lesson, you'll master both concepts.
Type Assertions
Type assertions extract the concrete value from an interface:
Click Run to execute your code
// Single return (panics if wrong type)
value := interfaceValue.(ConcreteType)
// Two returns (safe, doesn't panic)
value, ok := interfaceValue.(ConcreteType)
if ok {
// Type assertion succeeded
}
value, ok := i.(Type)
Figure: Type Assertion and Type Switch - Safe vs unsafe type assertions and handling multiple types with type switch
Type Switches
Type switches let you handle different types in a clean way:
Click Run to execute your code
switch v := interfaceValue.(type) {
case Type1:
// v is Type1
case Type2:
// v is Type2
default:
// v is the interface type
}
Type Switch Patterns
// Multiple types in one case
switch v := i.(type) {
case int, int64:
fmt.Println("Integer:", v)
case string:
fmt.Println("String:", v)
case nil:
fmt.Println("Nil value")
default:
fmt.Printf("Unknown type: %T\n", v)
}
Common Standard Library Interfaces
1. fmt.Stringer
The Stringer interface customizes how types are printed:
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
func main() {
p := Person{Name: "Alice", Age: 25}
fmt.Println(p) // Alice (25 years old)
}
2. io.Reader
Reader reads data into a byte slice:
type Reader interface {
Read(p []byte) (n int, err error)
}
// Common implementations:
// - os.File
// - strings.Reader
// - bytes.Buffer
// - net.Conn
func readData(r io.Reader) {
buf := make([]byte, 1024)
n, err := r.Read(buf)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Read %d bytes: %s\n", n, buf[:n])
}
3. io.Writer
Writer writes data from a byte slice:
type Writer interface {
Write(p []byte) (n int, err error)
}
// Common implementations:
// - os.File
// - bytes.Buffer
// - net.Conn
// - http.ResponseWriter
func writeData(w io.Writer, data string) {
n, err := w.Write([]byte(data))
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Wrote %d bytes\n", n)
}
4. error Interface
The error interface is used throughout Go:
type error interface {
Error() string
}
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func validate(age int) error {
if age < 0 {
return ValidationError{
Field: "age",
Message: "must be non-negative",
}
}
return nil
}
Common Standard Library Interfaces
| Interface | Package | Purpose |
|---|---|---|
Stringer |
fmt | Custom string representation |
Reader |
io | Read data |
Writer |
io | Write data |
Closer |
io | Close resources |
error |
builtin | Error handling |
Handler |
http | HTTP request handling |
Practical Examples
JSON Marshaling with Interfaces
type Animal 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 main() {
animals := []Animal{
Dog{Name: "Buddy"},
Cat{Name: "Whiskers"},
}
for _, animal := range animals {
// Type switch to get specific behavior
switch a := animal.(type) {
case Dog:
fmt.Printf("%s says %s\n", a.Name, a.Speak())
case Cat:
fmt.Printf("%s says %s\n", a.Name, a.Speak())
}
}
}
Common Mistakes
1. Type assertion without checking
// โ Wrong - panics if wrong type
var i interface{} = "hello"
num := i.(int) // panic!
// โ
Correct - check before using
if num, ok := i.(int); ok {
fmt.Println(num)
} else {
fmt.Println("Not an int")
}
2. Forgetting nil case in type switch
// โ Incomplete - doesn't handle nil
func process(i interface{}) {
switch v := i.(type) {
case string:
fmt.Println("String:", v)
case int:
fmt.Println("Int:", v)
}
}
// โ
Complete - handles nil
func process(i interface{}) {
switch v := i.(type) {
case nil:
fmt.Println("Nil value")
case string:
fmt.Println("String:", v)
case int:
fmt.Println("Int:", v)
default:
fmt.Printf("Unknown: %T\n", v)
}
}
3. Not implementing error interface correctly
// โ Wrong - pointer receiver
type MyError struct {
msg string
}
func (e *MyError) Error() string {
return e.msg
}
// This doesn't work as expected
err := MyError{msg: "oops"}
var e error = err // Error: MyError doesn't implement error
// โ
Correct - value receiver for error
func (e MyError) Error() string {
return e.msg
}
err := MyError{msg: "oops"}
var e error = err // OK
Exercise: Shape Calculator
Task: Create a shape calculator using interfaces and type switches.
Requirements:
- Define a
Shapeinterface withArea()method - Implement
CircleandRectangle - Add
String()method to both (fmt.Stringer) - Use type switch to print specific details
Show Solution
package main
import (
"fmt"
"math"
)
// Shape interface
type Shape interface {
Area() float64
}
// Circle implementation
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) String() string {
return fmt.Sprintf("Circle(radius=%.2f)", c.Radius)
}
// Rectangle implementation
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle(%.2f x %.2f)", r.Width, r.Height)
}
// Triangle implementation (bonus)
type Triangle struct {
Base float64
Height float64
}
func (t Triangle) Area() float64 {
return 0.5 * t.Base * t.Height
}
func (t Triangle) String() string {
return fmt.Sprintf("Triangle(base=%.2f, height=%.2f)", t.Base, t.Height)
}
// Describe shape using type switch
func describe(s Shape) {
fmt.Printf("%s has area %.2f\n", s, s.Area())
// Type switch for specific details
switch shape := s.(type) {
case Circle:
fmt.Printf(" Circumference: %.2f\n", 2*math.Pi*shape.Radius)
case Rectangle:
fmt.Printf(" Perimeter: %.2f\n", 2*(shape.Width+shape.Height))
case Triangle:
fmt.Printf(" Base: %.2f, Height: %.2f\n", shape.Base, shape.Height)
default:
fmt.Printf(" Unknown shape type: %T\n", shape)
}
}
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 6},
Triangle{Base: 3, Height: 4},
}
fmt.Println("Shape Calculator")
fmt.Println("================")
for _, shape := range shapes {
describe(shape)
fmt.Println()
}
// Calculate total area
totalArea := 0.0
for _, shape := range shapes {
totalArea += shape.Area()
}
fmt.Printf("Total area of all shapes: %.2f\n", totalArea)
}
Summary
- Type assertions extract concrete values from interfaces
- Use two-value form to avoid panics:
v, ok := i.(Type) - Type switches handle multiple types cleanly
- fmt.Stringer customizes string representation
- io.Reader/Writer are fundamental for I/O operations
- error interface is used for error handling
- Small interfaces are more flexible and composable
- Standard library interfaces enable powerful abstractions
What's Next?
Congratulations on completing the Pointers & Interfaces module! You now understand Go's memory model and polymorphism. Next, you'll learn about Error HandlingโGo's philosophy on errors, custom errors, and best practices.
Enjoying these tutorials?