Go > Structs and Interfaces > Interfaces > Interface composition
Interface Composition in Go
This example demonstrates how to compose interfaces in Go to create more complex and specialized interfaces. Interface composition allows you to combine the behaviors of multiple interfaces into a single, unified interface, promoting code reusability and flexibility.
Defining Basic Interfaces
We start by defining two basic interfaces: Reader
and Writer
. The Reader
interface has a Read
method, and the Writer
interface has a Write
method. These interfaces represent the ability to read and write data, respectively.
package main
import "fmt"
// Reader interface for reading data
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer interface for writing data
type Writer interface {
Write(p []byte) (n int, err error)
}
Composing Interfaces
Here, we define a new interface called ReadWriter
. Instead of defining methods directly, we embed the Reader
and Writer
interfaces. This means that any type that implements the ReadWriter
interface must implement both the Read
and Write
methods.
// ReadWriter interface composing Reader and Writer
type ReadWriter interface {
Reader
Writer
}
Implementing the Composed Interface
Now we create a concrete type, File
, that implements both the Read
and Write
methods. This makes it implicitly implement the ReadWriter
interface. The Read
method reads data from the file's internal buffer, and the Write
method appends data to the buffer.
// Concrete type implementing ReadWriter
type File struct {
data []byte
pos int
}
func (f *File) Read(p []byte) (n int, err error) {
if f.pos >= len(f.data) {
return 0, fmt.Errorf("EOF")
}
n = copy(p, f.data[f.pos:])
f.pos += n
return n, nil
}
func (f *File) Write(p []byte) (n int, err error) {
f.data = append(f.data, p...)
n = len(p)
return n, nil
}
Using the Composed Interface
In the main
function, we create an instance of the File
type and assign it to a variable of type ReadWriter
. We then use the Read
and Write
methods through the interface. This demonstrates how the composed interface allows us to treat the File
type as both a Reader
and a Writer
.
func main() {
file := &File{data: []byte("Hello, world!")}
var rw ReadWriter = file
// Reading from the ReadWriter interface
buf := make([]byte, 5)
n, err := rw.Read(buf)
if err != nil {
fmt.Println("Error reading:", err)
}
fmt.Printf("Read %d bytes: %s\n", n, string(buf[:n]))
// Writing to the ReadWriter interface
n, err = rw.Write([]byte(" Added more data."))
if err != nil {
fmt.Println("Error writing:", err)
}
fmt.Printf("Written %d bytes\n", n)
// Reading again to see the appended data
buf2 := make([]byte, len(file.data))
file.pos = 0 // Reset position for reading again
n, err = rw.Read(buf2)
if err != nil {
fmt.Println("Error reading again:", err)
}
fmt.Printf("Read all data: %s\n", string(buf2[:n]))
}
Concepts Behind the Snippet
Interface composition in Go is a powerful mechanism for building complex abstractions by combining simpler interfaces. It promotes code reuse and allows you to define types that satisfy multiple interfaces without explicitly listing all the methods. It also provides a level of indirection, making your code more flexible and adaptable to change.
Real-Life Use Case
A common real-life use case is in networking or file handling. Imagine you have different types of network connections (TCP, UDP) or file formats (JSON, CSV). You can define Readable
and Writable
interfaces and then compose them into a ReadWriteConnection
or ReadWriteFile
interface. Different connection types or file format handlers can then implement these composite interfaces, providing a unified way to interact with them.
Best Practices
Interview Tip
Be prepared to explain the benefits of interface composition over inheritance. Go doesn't have traditional inheritance, and interface composition is the preferred way to achieve code reuse and polymorphism. Also, understand the difference between embedding an interface and declaring it as a field. Embedding an interface promotes its methods to the embedding type, while declaring it as a field requires explicit access.
When to Use Them
Use interface composition when you need to combine the behaviors of multiple interfaces into a single, cohesive abstraction. This is especially useful when dealing with types that have multiple responsibilities or when you want to provide a flexible and extensible API.
Memory Footprint
The memory footprint of interface composition is minimal. The composed interface itself doesn't add any significant overhead. The concrete type implementing the composed interface will have a memory footprint determined by its fields, not by the interface itself. Using interfaces adds a small runtime cost when the methods are called due to dynamic dispatch.
Alternatives
Alternatives to interface composition include:
Pros
Cons
FAQ
-
What is the difference between interface composition and inheritance?
Interface composition in Go allows you to combine interfaces, providing a way to aggregate behaviors. Unlike inheritance, it doesn't create a 'is-a' relationship but rather a 'has-a' relationship in terms of capabilities. Go doesn't support traditional inheritance. -
Can I compose interfaces with overlapping method names?
Yes, you can compose interfaces with overlapping method names. However, if a type implements the composed interface, it must provide a single implementation that satisfies both interfaces. This can be achieved using method promotion or explicit implementation.