Notessh2a

Interfaces

Overview

In Go, an interface is a type that defines a set of method signatures. A type satisfies an interface by implementing all of its methods.

Interfaces enable flexible code. Functions can operate on any type that satisfies the required methods. A type may satisfy multiple interfaces, and an interface may be satisfied by many 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 shape by implementing all required methods. This happens automatically, no explicit declaration is needed.

    Interface satisfaction also depends on method receivers. Methods with value receivers belong to both T and *T. Methods with pointer receivers belong only to *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. Sorting a slice using 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{}) defines zero methods, so all types satisfy it. The alias any can also be used instead of interface{}.

func main() {
    var x interface{}

	x = 5
	x = "hello"
	x = true

	fmt.Println(x) // true
}

Type Assertion

A type assertion compares an interface value with 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

Omitting ok causes a panic on failure:

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 compares an interface value against multiple 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
}

Type switches also work with named types and pointer types, not just basic types.

type user struct {
	name string
}

func describe(i interface{}) {
	switch v := i.(type) {
	case *user:
		fmt.Println("Pointer to User", v.name)
	case user:
		fmt.Println("User value", v.name)
	default:
		fmt.Println("Unknown type", v)
	}
}

func main() {
	u := user{name: "John"}
	describe(u)  // User value John
	describe(&u) // Pointer to user John
}

Examples:

  1. var m = map[string]interface{}{
    	"Name": "Wednesday",
    	"Age":  6,
    	"Parents": []interface{}{
    		"Gomez",
    		"Morticia",
    	},
    }
    
    func main() {
    	for k, v := range m {
    		switch vv := v.(type) {
    		case string:
    			fmt.Println(k, "is string", vv)
    		case float64:
    			fmt.Println(k, "is float64", vv)
    		case []interface{}:
    			fmt.Println(k, "is an array:")
    			for i, u := range vv {
    				fmt.Println(i, u)
    			}
    		default:
    			fmt.Println(k, "is of a type I don't know how to handle")
    		}
    	}
    }
    
    // Name is string Wednesday
    // Age is of a type I don't know how to handle
    // Parents is an array:
    // 0 Gomez
    // 1 Morticia

Interface Composition

Interface composition combines existing interfaces into a new one.

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