Notessh2a

Interfaces

Overview

In Go, an interface is a type that defines a set of method signatures. A type satisfies an interface when it implements all of the methods declared by that interface.

Interfaces help you write flexible code by allowing functions to work with any type that provides the required methods. A type can satisfy multiple interfaces, and a single interface can be satisfied by many different types.

  • Define:

    type shape interface {
        area() float64
        perimeter() float64
    }

    Here, the shape interface defines two method signatures, area() and perimeter(). Any type that implements both methods satisfies shape.

  • Implement:

    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
    }
    type rect struct {
        width  float64
        height float64
    }
    
    func (r rect) area() float64 {
        return r.width * r.height
    }
    
    func (r rect) perimeter() float64 {
        return 2*r.width + 2*r.height
    }

    Both circle and rect satisfy the shape interface because they implement all required methods. This happens automatically, with no explicit declaration.

    Interface satisfaction also depends on method receivers. Methods defined on a value receiver are part of both T and *T, while methods defined on a pointer receiver are part of only *T.

    type speaker interface {
        sayHello()
    }
    
    type person struct {
        name string
    }
    
    func (d *person) sayHello() {
        fmt.Println("Hello, my name is " + d.name)
    }
    
    func main() {
        var s speaker
    
        p := person{name: "John"}
    
        // s = p   // <-- compiler: cannot use p (variable of struct type person) as speaker value in assignment: person does not implement speaker (method sayHello has pointer receiver)
        s = &p
        s.sayHello() // Hello, my name is John
    }
  • Use:

    func printArea(s shape) {
        fmt.Println("Area:", s.area())
        fmt.Println("Perimeter:", s.perimeter())
    }
    
    func main() {
        myCircle := circle{radius: 3}
        printArea(myCircle)
        // Area: 28.274333882308138
        // Perimeter: 18.84955592153876
    
        myRect := rect{width: 3, height: 4}
        printArea(myRect)
        // Area: 12
        // Perimeter: 14
    }

    Functions can accept interface types as parameters, which lets them work with any type that satisfies the interface.

Examples:

  1. Let's say you have a slice and want to sort it using the Sort function ↗ from the sort package.

    The Sort function accepts data that implements an interface. To pass the slice to it, we need to satisfy that interface ↗. It requires us to have Len() int, Less(i, j int) bool, and Swap(i, j int) methods.

    import (
    	"fmt"
    	"sort"
    )
    
    type ages []uint8
    
    func (a ages) Len() int {
        return len(a)
    }
    
    func (a ages) Swap(i, j int) {
        a[i], a[j] = a[j], a[i]
    }
    
    func (a ages) Less(i, j int) bool {
        return a[i] < a[j]
    }
    
    func main() {
        studentAges := ages{19, 18, 22, 20, 20, 18}
        sort.Sort(studentAges)
    
        fmt.Println(studentAges) // [18 18 19 20 20 22]
    }
  2. Change how fmt behaves for your own type.

    The fmt package checks whether a value satisfies the Stringer interface ↗. This interface requires a single method, String() string. If a type implements that method, fmt uses it automatically when printing. Otherwise, it falls back to the default representation.

    import (
    	"fmt"
    )
    
    type ipAddr [4]byte
    
    func (i ipAddr) String() string {
    	return fmt.Sprintf("is: %d.%d.%d.%d", i[0], i[1], i[2], i[3]) // prints the address as a dotted quad
    }
    
    func main() {
    	hosts := map[string]ipAddr{
    		"loopback":  {127, 0, 0, 1},
    		"googleDNS": {8, 8, 8, 8},
    	}
    
    	for name, ip := range hosts {
    		fmt.Println(name, ip)
    	}
    }
    
    // loopback is: 127.0.0.1
    // googleDNS is: 8.8.8.8

    As you can see from the output, fmt prints the map keys using the default behavior because they are plain string values. For the map values, it detects that they are of type IPAddr and automatically uses the custom String() method.

  3. Customize printing errors.

    Go programs express error state with error values. The built-in error type is an interface and defined like:

    type error interface {
    	Error() string
    }

    If a type implements the Error() string method, it satisfies the error interface. The fmt package also looks for the error interface when printing values, so if a value is an error, fmt uses its Error() method.

    import (
    	"fmt"
    )
    
    type errNegativeVal int
    
    func (e errNegativeVal) Error() string {
    	return fmt.Sprintf("Number can't be negative: %v", int(e))
    }
    
    func twoTimes(x int) (int, error) {
    	if x < 0 {
    		return 0, errNegativeVal(x)
    	}
    
    	return x * 2, nil
    }
    
    func main() {
    	fmt.Println(twoTimes(4)) // 8 <nil>
    	fmt.Println(twoTimes(2)) // 4 <nil>
    	fmt.Println(twoTimes(-2)) // 0 Number can't be negative: -2
    }

Empty Interface

An empty interface (interface{}) requires zero methods, so every type satisfies it. You can also use the alias any instead of interface{}.

func main() {
    var x interface{}

	x = 5
	x = "hello"
	x = true

	fmt.Println(x) // true
}

Type Assertion

A type assertion is an operation that compares an interface value against a specific type and returns the underlying value if it matches.

Syntax:

val, ok := x.(assertedType)
  • assertedType: Can be any type, including named types and pointer types.
  • val: The extracted value. If the assertion fails, it is the zero value of the asserted type.
  • ok: A boolean indicating whether the assertion succeeded.
func main() {
	var x interface{} = "Hello"

	val, ok := x.(string)

	if ok {
		fmt.Println("It's a string:", val)
	} else {
		fmt.Println("Not a string")
	}
}

// It's a string: Hello

You can also omit ok, but if the assertion fails, it causes a panic:

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

	s1, ok1 := i.(string)
	fmt.Println(s1, ok1) // hello true

	s2 := i.(string)
	fmt.Println(s2) // hello

	f1, ok2 := i.(float64)
	fmt.Println(f1, ok2) // 0 false

	f2 := i.(float64) // <-- panic: interface conversion: interface {} is string, not float64
	fmt.Println(f2)
}

Here is an example showing usage with the shape interface defined above:

// ...

func identifyShape(s shape) {
	val, ok := s.(circle)
	if ok {
		fmt.Println("Shape is a circle, val:", val)
	} else {
		fmt.Println("Shape is not a circle")
		// val is zero value here
	}
}

func main() {
	// ...

	identifyShape(myCircle) // Shape is a circle, val: {3}
}

Type Switches

A type switch is a switch statement that compares an interface value against multiple possible types.

func describe(i interface{}) {
	switch v := i.(type) {
	case string:
		fmt.Println("A string", v)
	case int:
		fmt.Println("An int", v)
	default:
		fmt.Println("Unknown type", v)
	}
}

func main() {
	x := 5
	describe(x) // An int 5
}

Interface Composition

Interface composition allows you to create new interfaces by combining existing ones.

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

type ReadWriter interface {
	Reader
	Writer
}

The ReadWriter interface requires both Read and Write methods. Any type that implements both methods automatically satisfies the ReadWriter interface.

If multiple interfaces are composed and they contain methods with the same name, those methods must have identical signatures in all interfaces.

On this page