An alternative to the class-based inheritance models seen in languages such as Java or C++, Go’s approach to object-oriented design is built on interfaces. Go interfaces define the capabilities of a type rather than its characteristics.
What is an Interfaces in Go?
A method set is specified by an abstract type called an interface. It lists the methods that must be implemented by a concrete type in order to meet that interface. In other words, a type is said to implicitly implement an interface if it contains all of the methods described by that interface, without requiring an explicit declaration (such as an implements keyword).
For instance, an area() method could be defined in a Shape interface:
type Shape interface {
area() float64
}
Key Concepts and Benefits
Polymorphism: Polymorphism is made possible via interfaces, which permit variables of an interface type to contain values of any concrete type that conforms to the interface. Accordingly, an interface variable may “take the form of” several concrete kinds.
Decoupling and Flexibility: Programs become more adaptable, reusable, and simpler to maintain when interfaces assist separate code from particular implementations. In order to operate on any type that satisfies an interface, functions can be built to accept an interface type as a parameter. This way, they are not restricted to a specific concrete type.
Code Organization and Reuse: They provide clear, understandable boundaries between components; Go’s standard library promotes this use by using tiny, frequently one-method interfaces like io.Reader, io.fmt.Stringer and the writer.
Defining and Implementing an Interface
Let’s look at an example using two forms that can determine their area: squares and circles.
In order to implement the area() method, first define the Shape interface and the two structs, Square and Circle:
package main
import (
"fmt"
"math"
)
// Shape interface defines the area() method
type Shape interface {
area() float64
}
// Circle struct
type Circle struct {
x, y, r float64
}
// Square struct
type Square struct {
side float64
}
// Method for Circle to calculate its area
func (c *Circle) area() float64 {
return math.Pi * c.r * c.r
}
// Method for Square to calculate its area
func (s *Square) area() float64 {
return s.side * s.side
}
func main() {
c := Circle{r: 5}
s := Square{side: 10}
// Since the methods have pointer receivers, we pass pointers to the structs.
var myShape Shape
myShape = &c
fmt.Printf("Circle area: %f\n", myShape.area())
myShape = &s
fmt.Printf("Square area: %f\n", myShape.area())
}
Output
Circle area: 78.539816
Square area: 100.000000
Using Interfaces for Polymorphism
The Shape interface type is an argument that can be sent to a function. Any concrete type (such as Square or Circle) that complies with the Shape interface can therefore be used with this function:
package main
import (
"fmt"
"math"
)
// Define the Shape interface
type Shape interface {
area() float64
}
// Define the Circle struct
type Circle struct {
x, y, r float64
}
// Define the area method for Circle
func (c *Circle) area() float64 {
return math.Pi * c.r * c.r
}
// Define the Square struct
type Square struct {
side float64
}
// Define the area method for Square
func (s *Square) area() float64 {
return s.side * s.side
}
// The totalArea function that accepts any type satisfying the Shape interface
func totalArea(shapes ...Shape) float64 {
var area float64
for _, s := range shapes {
area += s.area() // Calls the area method on the concrete type
}
return area
}
// The main function where the program execution begins
func main() {
c := Circle{x: 0, y: 0, r: 5}
s := Square{side: 10}
fmt.Println("Area of circle:", c.area())
fmt.Println("Area of square:", s.area())
fmt.Println("Total area:", totalArea(&c, &s))
}
Output
Area of circle: 78.53981633974483
Area of square: 100
Total area: 178.53981633974485
The totalArea function can process an input as long as it behaves like a Shape (that is, has an area() method), regardless of whether the input is a circle or a square.
Interface Composition
It is possible for interfaces to be made up of other interfaces. For instance, Go’s standard library’s io.ReadCloser interface combines the io.Reader and io methods.nearer. As a result, simpler, reusable interfaces can be used to build up more complicated contracts.
package main
import (
"fmt"
"math"
)
// Shape interface from the previous example
type Shape interface {
area() float64
}
// Circle struct from the previous example
type Circle struct {
x, y, r float64
}
// area method for Circle
func (c *Circle) area() float64 {
return math.Pi * c.r * c.r
}
// Square struct from the previous example
type Square struct {
side float64
}
// area method for Square
func (s *Square) area() float64 {
return s.side * s.side
}
// MultiShape struct can hold multiple shapes
type MultiShape struct {
shapes []Shape
}
// MultiShape implements the Shape interface itself
func (m *MultiShape) area() float64 {
var area float64
for _, s := range m.shapes {
area += s.area()
}
return area
}
func main() {
// Create a new MultiShape instance
multiShape := &MultiShape{
shapes: []Shape{
&Circle{x: 0, y: 0, r: 5},
&Square{side: 10},
},
}
// Call the area method on the MultiShape
fmt.Println("Total area of the multi-shape:", multiShape.area())
}
Output
Total area of the multi-shape: 178.53981633974485
Interfaces as Struct Fields
By acting as fields inside structs, interfaces enable a struct to contain many concrete types that behave similarly. A slice of Shape interfaces, for instance, may be contained in a MultiShape struct:
The Empty Interface ()
There are no methods defined in Go’s unique empty interface (interface{}). The empty interface is satisfied by all types since they implicitly implement zero methods. Because of this, interface{} is helpful for functions like fmt.Println that must accept values of any kind.
Type Assertions
You may occasionally need to access specific methods or fields from the underlying concrete type that are not included in the interface’s method set when working with an interface type. This can be done with type assertions (x.(T)). They can be used to extract a value from an interface if they are successful in determining whether it contains a specific type. A type assertion failure will cause the program to panic if it is not handled graciously.
package main
import (
"fmt"
"math"
)
// Shape interface
type Shape interface {
area() float64
}
// Circle struct
type Circle struct {
x, y, r float64
}
// area method for Circle
func (c *Circle) area() float64 {
return math.Pi * c.r * c.r
}
// Square struct
type Square struct {
side float64
}
// area method for Square
func (s *Square) area() float64 {
return s.side * s.side
}
// processShape function demonstrates type assertion
func processShape(s Shape) {
// Check if the underlying type is a *Circle
if c, ok := s.(*Circle); ok {
fmt.Printf("It's a circle with radius: %f\n", c.r)
} else if sq, ok := s.(*Square); ok {
fmt.Printf("It's a square with side: %f\n", sq.side)
} else {
fmt.Println("Unknown shape type")
}
}
func main() {
// Create instances of concrete types
c := Circle{r: 5}
s := Square{side: 10}
// Pass the interface values to the function
processShape(&c)
processShape(&s)
}
Output
It's a circle with radius: 5.000000
It's a square with side: 10.000000
Without the hassles of conventional inheritance hierarchies, Go’s distinctive approach to interfaces which places an emphasis on implicit satisfaction and composition allows programmers to create adaptable and manageable code.