Skip to content

Interfaces

Interfaces in Go

An interface in Go specifies a method set and allows you to define the behavior that types must implement. Interfaces are a cornerstone of polymorphism in Go.


1. Declaring an Interface

An interface is declared using the type keyword, followed by its name and method set.

Example: Simple Interface

package main

import "fmt"

// Define an interface
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Implement the interface in a type
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)
}

func main() {
    var s Shape = Rectangle{Width: 5, Height: 10}
    fmt.Println("Area:", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}

2. Empty Interface

The empty interface (interface{}) can hold values of any type. It is commonly used when the type is not known at compile time.

Example: Using interface{}

package main

import "fmt"

func describe(i interface{}) {
    fmt.Printf("Type: %T, Value: %v\n", i, i)
}

func main() {
    describe(42)
    describe("hello")
    describe(true)
}

3. Type Assertions

Type assertions are used to extract the underlying value of an interface.

Syntax:

value := i.(T) // Asserts that the interface `i` holds a value of type `T`

Example: Safe Type Assertion

package main

import "fmt"

func main() {
    var i interface{} = "hello"

    // Safe assertion
    if str, ok := i.(string); ok {
        fmt.Println("String value:", str)
    } else {
        fmt.Println("Not a string")
    }
}

4. Type Switch

A type switch allows you to handle multiple types stored in an interface.

Example: Type Switch

package main

import "fmt"

func process(i interface{}) {
    switch v := i.(type) {
    case string:
        fmt.Println("String value:", v)
    case int:
        fmt.Println("Integer value:", v)
    default:
        fmt.Println("Unknown type")
    }
}

func main() {
    process("hello")
    process(42)
    process(3.14)
}

5. Interface Composition

Go supports interface composition, allowing you to combine multiple interfaces into one.

Example: Composed Interface

package main

import "fmt"

// Define two interfaces
type Reader interface {
    Read() string
}

type Writer interface {
    Write(data string)
}

// Compose Reader and Writer
type ReadWriter interface {
    Reader
    Writer
}

type File struct {
    content string
}

func (f *File) Read() string {
    return f.content
}

func (f *File) Write(data string) {
    f.content = data
}

func main() {
    var rw ReadWriter = &File{}

    rw.Write("Hello, Go!")
    fmt.Println("Content:", rw.Read())
}

6. Nil Interfaces

An interface with a nil value behaves differently based on its underlying type.

Example: Nil Interface Behavior

package main

import "fmt"

type Printer interface {
    Print()
}

type ConsolePrinter struct{}

func (c *ConsolePrinter) Print() {
    fmt.Println("Printing...")
}

func main() {
    var p Printer

    if p == nil {
        fmt.Println("Interface is nil")
    }

    p = &ConsolePrinter{}
    p.Print()
}

7. Practical Use Cases

  • Polymorphism: Interfaces allow functions to work with multiple types that implement the same interface.
  • Dependency Injection: Pass interfaces to functions to increase testability and decouple implementations.
  • Mocking: Use interfaces to mock dependencies in tests.

8. Common Interface Examples in Go's Standard Library

Interface Description
fmt.Stringer Defines the String() string method for custom string formatting.
io.Reader Represents types that can read data.
io.Writer Represents types that can write data.
error Represents types that can return error messages (Error() string).

Example: fmt.Stringer

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("Name: %s, Age: %d", p.Name, p.Age)
}

func main() {
    p := Person{Name: "Alice", Age: 25}
    fmt.Println(p)
}

Best Practices with Interfaces

  1. Keep Interfaces Small: Prefer single-method interfaces (e.g., io.Reader) for flexibility.
  2. Name Interfaces after Behavior: Use verbs or roles like Reader, Writer, or Closer.
  3. Design for Contracts: Ensure interfaces represent meaningful contracts for types.
  4. Avoid Empty Interfaces: Use interface{} sparingly; prefer concrete types when possible.