Interfaces in go is so powerful

Chidozie C. Okafor
6 min readDec 18, 2022

--

In Go, an interface is a collection of method signatures that define a set of behaviours that a type must implement to be considered “implementing” the interface. Interfaces in Go are very flexible and allow for types to implement multiple interfaces, as well as for interfaces to embed other interfaces.

To create an interface in Go, you simply define a set of method signatures without any implementation. For example:

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

This interface, called Reader, defines a single method called Read that takes a slice of bytes as an argument and returns an integer and an error. Any type that implements this method is considered to implement the Reader interface.

To implement an interface in Go, a type simply needs to define all the methods specified in the interface. For example, here is how the os.File type might implement the Reader interface:

type File struct {
// ...
}

func (f *File) Read(p []byte) (n int, err error) {
// ... implementation of the Read method
}

Once a type has implemented all the methods in an interface, it is considered to “implement” the interface. This means that you can use the interface as a type in your code and pass in any value that implements the interface.

One of the key benefits of using interfaces in Go is that they allow for very flexible and decoupled code. For example, you can write code that relies on the Reader interface, and then pass in any type that implements the Reader interface, whether it's a file, a network connection, or something else entirely. This allows you to write code that is not tied to a specific implementation, and makes it easier to change or swap out implementations as needed.

Another benefit of interfaces in Go is that they allow you to create hierarchies of interfaces. You can define an interface that embeds one or more other interfaces, and then any type that implements the parent interface is considered to also implement the embedded interfaces. This can be useful for creating more specialized interfaces that build on top of more general interfaces.

Overall, interfaces are a powerful and flexible feature of Go that allow you to write decoupled, flexible code that can be easily extended and modified.

Here is a comprehensive example of how you might use interfaces in Go to define and implement a set of related behaviors.

First, let’s define an interface called Shape that defines some basic behaviors for geometric shapes:

type Shape interface {
Area() float64
Perimeter() float64
}

This Shape interface defines two methods: Area and Perimeter, which calculate the area and perimeter of the shape, respectively.

Next, let’s define a struct called Rectangle that represents a rectangle with a width and a height:

type Rectangle struct {
Width float64
Height float64
}

Now, let’s implement the Shape interface for the Rectangle type by defining the Area and Perimeter methods:

func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}

With these methods defined, the Rectangle type is now considered to implement the Shape interface.

We can do the same thing for a Circle type, which has a radius:

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
}

Now, both the Rectangle and Circle types implement the Shape interface, and we can use them interchangeably in our code.

Here’s an example of how we might use these types and the Shape interface to calculate the area and perimeter of a few different shapes:

func main() {
var shapes []Shape

shapes = append(shapes, Rectangle{Width: 10, Height: 5})
shapes = append(shapes, Circle{Radius: 4})
shapes = append(shapes, Rectangle{Width: 7, Height: 3})
shapes = append(shapes, Circle{Radius: 2})

for _, shape := range shapes {
fmt.Printf("Shape: Area = %.2f, Perimeter = %.2f\n", shape.Area(), shape.Perimeter())
}
}

In this example, we create a slice of Shape interface values and append a few Rectangle and Circle values to it. We can then loop through the slice and call the Area and Perimeter methods on each element, even though they are of different types, because they all implement the Shape interface.

This is just a basic example of how you can use interfaces in Go, but it should give you a good idea of how they work and how they can be used to define and implement related behaviors.

Let’s look at more advance examples:

First, let’s define an interface called Converter that defines a set of methods for converting values between different types:

type Converter interface {
ConvertToInt(val interface{}) (int, error)
ConvertToFloat64(val interface{}) (float64, error)
ConvertToString(val interface{}) (string, error)
}

This Converter interface defines three methods: ConvertToInt, ConvertToFloat64, and ConvertToString, which take an interface value as an argument and return the converted value as an integer, float64, or string, respectively, along with an error.

Now, let’s define a struct called NumberConverter that implements the Converter interface:

type NumberConverter struct{}

func (nc NumberConverter) ConvertToInt(val interface{}) (int, error) {
switch v := val.(type) {
case int:
return v, nil
case float64:
return int(v), nil
case string:
i, err := strconv.Atoi(v)
if err != nil {
return 0, err
}
return i, nil
default:
return 0, fmt.Errorf("unsupported type %T", val)
}
}

func (nc NumberConverter) ConvertToFloat64(val interface{}) (float64, error) {
switch v := val.(type) {
case int:
return float64(v), nil
case float64:
return v, nil
case string:
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return 0, err
}
return f, nil
default:
return 0, fmt.Errorf("unsupported type %T", val)
}
}

func (nc NumberConverter) ConvertToString(val interface{}) (string, error) {
switch v := val.(type) {
case int:
return strconv.Itoa(v), nil
case float64:
return strconv.FormatFloat(v, 'f', -1, 64), nil
case string:
return v, nil
default:
return "", fmt.Errorf("unsupported type %T", val)
}
}

This NumberConverter type implements the Converter interface by defining the three methods specified in the interface. The ConvertToInt, ConvertToFloat64, and ConvertToString methods use a switch statement to determine the type of the input value and perform the appropriate conversion.

Now, let’s define a function that takes a Converter interface value as an argument and uses it to convert a value to a string:

func convertToString(converter Converter, val interface{}) (string, error) {
return converter.ConvertToString(val)
}

We can use this function with any value that implements the Converter interface, including our `NumberConverter` type. For example:

func main() {
nc := NumberConverter{}

s, err := convertToString(nc, 123)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(s)
}

s, err = convertToString(nc, 123.456)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(s)
}

s, err = convertToString(nc, "hello")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(s)
}
}

This code will output the following:

123
123.456
hello

This is just a more advanced example of how you can use interfaces in Go to define and implement related behaviors. You can define more complex interfaces with additional methods and use them to create more flexible and decoupled code.

It’s important to note that interfaces in Go are a compile-time concept, and there is no runtime cost associated with using interfaces. This means that using interfaces can be very efficient and can help you write efficient, scalable code. However, it’s still important to consider the tradeoffs and limitations of using interfaces and to use them appropriately in your code.

Few Limitations:

There are a few tradeoffs and limitations to consider when using interfaces in Go:

  1. Interfaces can add complexity to your code: Defining and implementing interfaces can add an extra layer of abstraction to your code, which can make it more complex and harder to understand. It’s important to carefully consider whether using interfaces is the best way to solve a particular problem, and to use them in a way that keeps your code as simple and straightforward as possible.
  2. Interfaces can make code less efficient: Because Go interfaces are implemented using reflection, using interfaces can result in slower code than using concrete types directly. This can be especially true for interfaces with many methods or methods with large signatures. In these cases, it may be more efficient to use concrete types instead of interfaces.
  3. Interfaces can’t enforce behavior: Go interfaces are a compile-time concept and do not enforce behavior at runtime. This means that it’s possible for a type to implement an interface without actually implementing the required behavior. It’s important to ensure that types that implement an interface are correctly implementing the required behavior, either by testing them or by using other means to enforce this.
  4. Interfaces can’t define state: Go interfaces can’t define fields or state, and can only specify method signatures. This means that if you want to define state or behavior that is shared by multiple types, you’ll need to use other means, such as inheritance or composition.

Overall, interfaces are a powerful and flexible tool in Go, but it’s important to carefully consider their tradeoffs and limitations when deciding whether and how to use them in your code.

--

--

Chidozie C. Okafor

Software Engineer & Backend Magician 🎩 | Python, Rust | TypeScript, Node.js | Golang | Kafka & GRPC