Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 15

Implementing Polymorphic Interfaces

Recommended resources

Hello! Welcome to the third module of our course, "Interfaces and Program Design."

In the previous module, we worked with structs to group data, methods to attach behavior to that data, and pointers to share and modify it effectively. We now have the tools to create custom data types with their own logic. This lesson introduces one of Go's most powerful and elegant features, which builds directly on that foundation: interfaces.

Today's learning outcome is to define and implement interfaces to create abstract, polymorphic behavior. In practical terms, this means writing code that is decoupled from specific implementations, making it more flexible, testable, and easier to extend. For a DevOps engineer, this is the key to building tools that can work with different cloud providers, notification services, or configuration formats without requiring a major rewrite of your core logic.

We will cover:

  • What an interface is and how to define one.
  • How Go's implicit implementation of interfaces works.
  • How interfaces enable polymorphism, allowing one piece of code to work with many different data types.
  • A practical example of refactoring rigid code to a flexible, interface-based design.

1. What is an Interface? The Contract

In Go, an interface is a type that defines a set of method signatures. Think of it as a contract. Any type that provides definitions for all the methods in the contract is said to "satisfy" or "implement" the interface.

The key difference from many other languages is that this implementation is implicit. You don't need to explicitly declare that a type implements an interface (e.g., struct MyType implements MyInterface). If your type has the methods defined in the interface, it automatically fulfills the contract. This "if it walks like a duck and quacks like a duck, it is a duck" philosophy makes Go's interfaces incredibly lightweight and flexible.

Let's start with a foundational article that introduces this concept.

Polymorphism in Golang using Interfaces | OOP in Go

The article 'Polymorphism in Golang using Interfaces' from Golangbot provides a clear definition and a simple starting point for understanding how interfaces work.

Read the introductory section, up to and including the bolded sentence: 'A variable of type interface can hold any value which implements the interface.' This sets the stage for the main idea.

The syntax for defining an interface is straightforward:

// A Shape interface defines the behavior expected of a shape.
type Shape interface {
    Area() float64
    Perimeter() float64
}

This Shape interface declares a contract. Any type, to be considered a Shape, must have both an Area() method that returns a float64 and a Perimeter() method that also returns a float64.

The image below provides a great visual summary of this concept.

Go Interfaces: Structs and Methods Implementing a Shape Interface
This diagram shows two concrete types, `Circle` and `Rectangle`. Because both have `Area()` and `Perimeter()` methods matching the `Shape` interface's signature, they both implicitly implement the `Shape` interface.

2. Achieving Polymorphism with Interfaces

The true power of interfaces comes from polymorphism—the ability for a single piece of code to work with values of multiple different types. A function can accept an interface type as an argument. When you call that function, you can pass in any concrete type that satisfies the interface. The function doesn't know or care about the specific type; it only interacts with the methods defined in the interface contract.

Let's see this in action with a complete example.

Polymorphism in Golang using Interfaces | OOP in Go

We'll continue with the Golangbot article, which builds a fantastic, easy-to-understand example of polymorphism.

Read from 'Polymorphism using an interface' down to the first complete program and its output. As you read, follow these steps in the code: The Interface: Note the Income interface with its calculate() and source() methods. The Concrete Types: See how the FixedBilling and TimeAndMaterial structs are defined. The Implementation: Observe how methods with the same names (calculate, source) are defined for both structs. This is where they implicitly implement the Income interface. The Polymorphic Function: Pay close attention to calculateNetIncome. Its parameter is a slice of Income interfaces. It can call income.calculate() and income.source() without knowing if income is a FixedBilling or TimeAndMaterial struct.

The magic happens in this function:

func calculateNetIncome(ic []Income) {
    var netincome int = 0
    for _, income := range ic {
        // At this point, Go knows the concrete type of 'income'
        // and calls the correct method for that type.
        fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
        netincome += income.calculate()
    }
    // ...
}

When this function is called with a slice containing both FixedBilling and TimeAndMaterial objects, Go's runtime determines the actual (concrete) type of each element in the slice and calls the appropriate method.


3. The Power of Extensibility

The polymorphic design we just saw has a huge benefit: your system is now incredibly easy to extend. Imagine your organization adds a new revenue stream. With an interface-based design, you can add support for this new stream without touching the core calculateNetIncome function at all.

This principle is directly applicable to the DevOps tools you want to build. You could write a core deployment function that operates on a Deployer interface. Then you could create KubernetesDeployer, EC2Deployer, and LocalDockerDeployer structs. Your core logic would work with all of them seamlessly.

Polymorphism in Golang using Interfaces | OOP in Go

The next section of the article demonstrates this extensibility perfectly.

Read the section 'Adding a new income stream to the above program'. Notice how the new Advertisement struct is created and implements the Income interface. The only change needed is to add instances of it to the slice in main. The calculateNetIncome function requires no modification.

Test your understanding!

Let's extend the Income example. Imagine the organization also earns money from stock dividends. Define a StockDividend struct and implement the Income interface for it.

The StockDividend struct should have fields for ticker (string) and dividendPerShare (int), and shareCount (int).

  • The source() method should return the ticker symbol.
  • The calculate() method should return the total dividend (dividendPerShare * shareCount).

How would you define this struct and its methods?

Show answer

Here's the new struct and its methods:

// StockDividend represents income from stock dividends.
type StockDividend struct {
    ticker           string
    dividendPerShare int
    shareCount       int
}

// calculate returns the total dividend amount.
func (sd StockDividend) calculate() int {
    return sd.dividendPerShare * sd.shareCount
}

// source returns the stock's ticker symbol.
func (sd StockDividend) source() string {
    return sd.ticker
}

You could then add it to the incomeStreams slice in main without changing calculateNetIncome:

func main() {
    // ... existing projects
    dividend1 := StockDividend{ticker: "GOOG", dividendPerShare: 1, shareCount: 100}
    
    incomeStreams := []Income{project1, project2, project3, bannerAd, popupAd, dividend1}
    calculateNetIncome(incomeStreams)
}

4. A Practical Refactoring Scenario

So far, we've designed a system with interfaces from the ground up. But often, you'll need to refactor existing, more rigid code to make it more flexible. This is a common and practical task.

The following video shows a great example of this. An API handler has a hardcoded notification mechanism. The presenter refactors it to use an interface, allowing different notification systems (e.g., email, Slack, or just logging) to be swapped in and out. This is a form of dependency injection, a crucial pattern for building testable and modular applications.

A Practical Example How To Use Interfaces In Golang

Watch this clip from 'A Practical Example How To Use Interfaces In Golang' by Anthony GG. It's a pragmatic demonstration of refactoring to an interface.

Watch from 02:20 to 13:51. The video walks through the entire process: The Problem (02:20): A hardcoded notification system that's difficult to change. Defining the Interface (03:53): The AccountNotifier interface is created as a contract for notification behavior. Using the Interface (06:31): The handler is changed to hold a variable of the interface type, not a concrete type. Creating a Concrete Type (08:37): A simpleAccountNotifier struct is created that fulfills the interface contract. Swapping Implementations (10:54): The presenter shows how easily a different implementation (betterAccountNotifier) can be used without changing the handler's logic.

This example highlights the core benefit for you as a tool-builder. Your main command logic can remain stable, while the specific implementations of how it interacts with external systems can be easily changed or mocked for testing.


Conclusion

Today you've learned about Go's interfaces, one of its most defining features. They provide a powerful way to write abstract code that is flexible, extensible, and decoupled from concrete details.

Key Takeaways:

  • An interface is a contract defining a set of method signatures.
  • A type implicitly implements an interface if it has all the methods in the contract.
  • Interfaces enable polymorphism: a function can operate on any type that satisfies a given interface, leading to flexible and reusable code.
  • Using interfaces allows you to decouple your application's core logic from its dependencies (like databases, notifiers, or API clients), making your code easier to maintain and test.

Next Up

You've now seen how to define your own interfaces. But one of the best ways to learn idiomatic Go is to see how interfaces are used in the standard library. In the next lesson, we will do just that by exploring two of the most important interfaces in all of Go: io.Reader and io.Writer. You'll learn how they are used to build composable, streaming data pipelines—an essential skill for writing efficient CLI tools and services.