Go Interfaces, Polymorphism & Collection
Interfaces and polymorphism are fundamental concepts in Go that allow for flexible and decoupled code design. This blog post will explore how interfaces enable polymorphism and how they can help decouple your code from specific data implementations using a practical example.
What is Polymorphism?
Polymorphism is the ability of code to change its behavior based on the concrete data it operates on. It allows a single piece of code to work with different types of data, providing flexibility and reusability. Polymorphism lets you decouple your code from specific data implementations, making it more maintainable and adaptable to change.
The Role of Interfaces
An interface in Go is a type that specifies a set of method signatures without implementing them. It is a valueless type, meaning you cannot construct, manipulate, or transform an interface directly. Instead, it acts as a layer of decoupling from the concrete data itself. An interface defines behavior based on what concrete data can do, not what it is.
Let’s look at an example to understand this better. Suppose we have an Reader interface:
package interfaces
// Reader is an interface that defines the act of reading data
type Reader interface {
Read(b []byte) (int, error)
}This interface has a single method, Readwhich any type can implement. The Reader interface itself does not contain any data or implementation details.
Discovering Interfaces
When defining interfaces, it’s important not to design them upfront but to discover them through the concrete implementations you have. Interfaces should represent behavior, and their names should reflect actions, like verbs: a Reader reads, a Writer writes, and a Printer prints.
Concrete Implementations
Let’s define two concrete types that implement the Reader interface:
package interfaces
import "fmt"
// File defines a system file.
type File struct {
name string
}
// Read implements the reader interface for a file
func (File) Read(b []byte) (int, error) {
s := "<rss><channel><title> Learning Go </title></channel></rss>"
copy(b, s)
return len(s), nil
}
// Pipe defines a named pipe network connection.
type Pipe struct {
name string
}
// Read implements the reader interface for a network connection.
func (Pipe) Read(b []byte) (int, error) {
s := `{name: "Devpriya", title: "Developer"}`
copy(b, s)
return len(s), nil
}The compiler can perform static code analysis to determine if a concrete type implements an interface. If a type defines all the methods specified in an interface, it automatically implements that interface. There’s no need for explicit configuration in code; it’s all about convention.
Get Devpriya Shivani’s stories in your inbox
Join Medium for free to get updates from this writer.
In our example, both File and Pipe types implement the Read method, making them concrete implementations of the Reader interface.
Polymorphic Functions
We can now define a polymorphic function that accepts any type of implementation of the Reader interface:
package interfaces
import "fmt"
// retrieve can read any device and process the data.
func retrieve(r Reader) error {
data := make([]byte, 100)
length, err := r.Read(data)
if err != nil {
return err
}
fmt.Println(string(data[:length]))
return nil
}
func Interfaces() {
// Create instances of File and Pipe
f := File{name: "testfile"}
p := Pipe{name: "testpipe"}
// Use the retrieve function with both types
fmt.Println("Reading from File:")
if err := retrieve(f); err != nil {
fmt.Println("Error:", err)
}
fmt.Println("Reading from Pipe:")
if err := retrieve(p); err != nil {
fmt.Println("Error:", err)
}
}This function is polymorphic because it operates on any concrete data type that satisfies the Reader interface. The function doesn't care whether it receives a File or a Pipe; it only cares that the provided type can Read.
Realizing Polymorphism
We realize polymorphism when we call the retrieve function with different types:
func Interfaces() {
// Create instances of File and Pipe
f := File{name: "testfile"}
p := Pipe{name: "testpipe"}
// Use the retrieve function with both types
fmt.Println("Reading from File:")
if err := retrieve(f); err != nil {
fmt.Println("Error:", err)
}
fmt.Println("Reading from Pipe:")
if err := retrieve(p); err != nil {
fmt.Println("Error:", err)
}
}When retrieve is called with a File, it reads from a file. When called with a Pipe, it reads from a pipe. This demonstrates how polymorphism allows the same function to exhibit different behaviors based on the concrete data it operates on.
Decoupling with Interfaces
By using interfaces, we achieve a precise layer of decoupling. The retrieve function doesn't need to know about the specific details of the File or Pipe types. It only relies on the behavior defined by the Reader interface. This makes our code more flexible and easier to extend with new types in the future.
Complete Code
package main
import "fmt"
type Reader interface {
Read(b []byte) (int, error)
}
type File struct {
name string
}
func (File) Read(b []byte) (int, error) {
s := "<rss><channel><title> Learning Go </title></channel></rss>"
copy(b, s)
return len(s), nil
}
type Pipe struct {
name string
}
func (Pipe) Read(b []byte) (int, error) {
s := `{name: "Devpriya", title: "Developer"}`
copy(b, s)
return len(s), nil
}
func retrieve(r Reader) error {
data := make([]byte, 100)
length, err := r.Read(data)
if err != nil {
return err
}
fmt.Println(string(data[:length]))
return nil
}
func main() {
f := File{name: "testfile"}
p := Pipe{name: "testpipe"}
fmt.Println("Reading from File:")
if err := retrieve(f); err != nil {
fmt.Println("Error:", err)
}
fmt.Println("Reading from Pipe:")
if err := retrieve(p); err != nil {
fmt.Println("Error:", err)
}
}Output:
Reading from File:
<rss><channel><title> Learning Go </title></channel></rss>
Reading from Pipe:
{name: "Devpriya", title: "Developer"}Interface Collection in Go
Interfaces in Go provide a powerful way to create flexible and reusable code by defining a set of methods that a type must implement. One of the interesting aspects of Go interfaces is their ability to allow collections of different concrete types that share common behavior.
Creating a Collection of Interface Types
Let’s see how we can create a collection of Printer interface values and work with it.
package main
import "fmt"
// Printer displays information
type Printer interface {
print()
}
// User defines a user in the program
type User struct {
name string
}
// print displays the user's name
func (u User) print() {
fmt.Printf("User Name: %s\n", u.name)
}
func main() {
// Create a value of type User
u := User{"Devpriya"}
// Add the values and pointers to the slice of printer interface values.
entities := []Printer{
// Store a copy of the user value in the interface value
u,
// Store a copy of the address of the user value in the interface
// value.
&u,
}
// Change the name field on theuser value
u.name = "Deva"
// Iterate over the slice of entities and call print against the copied
// interface value.
for _, e := range entities {
e.print()
}
// When we store a value,
// the interface value has it's own copy of the value.
// Changes to the original value be not be seen.
}Output:
User Name: Devpriya
User Name: DevaThis distinction becomes evident when we change the name field of the User value and then iterate over the slice to call the print method on each element.
- The first output corresponds to the value stored in the interface slice, which remains unchanged (
Devpriya). - The second output corresponds to the pointer stored in the interface slice, which reflects the change to the original value (
Deva).
Conclusion
Interfaces and polymorphism are powerful tools in Go that enable you to write flexible, decoupled code. By defining behavior through interfaces and leveraging polymorphic functions, you can create reusable and maintainable code that adapts to changes in concrete data implementations. Furthermore, by leveraging interfaces and their ability to hold different concrete types, you can create flexible and reusable collections in Go. This feature, combined with an understanding of value and pointer semantics, allows you to write more robust and maintainable code. As you work with Go, keep these principles in mind to make the most of its powerful type system and enhance the design and robustness of your programs.










