Understanding the Power of Go Interfaces: A Comprehensive Guide

Jamal Kaksouri
11 min readFeb 21, 2023

--

Understanding the Power of Go Interfaces: A Comprehensive Guide

The content of this article may be edited based on your feedback!

Introduction

Go is a powerful programming language that is designed for high performance and scalability. One of the key features of Go is its support for interfaces. Interfaces in Go allow you to define a set of method signatures that any type can implement, providing a flexible and powerful way to write generic code. In this article, we will explore the concept of interfaces in Go in depth, and we will cover everything you need to know about using them effectively.

Part 1: What are Go Interfaces?

In Go, an interface is a type that defines a set of method signatures. Any type that implements all of the methods defined in the interface is said to satisfy the interface. This allows you to write code that is generic and can be used with any type that satisfies the interface.

Here’s an example of an interface in Go:

type Printer interface {
Print()
}

This interface defines a single method signature called Print(). Any type that has a Print() method can satisfy this interface. For example, consider the following struct:

type Person struct {
Name string
}

func (p Person) Print() {
fmt.Println(p.Name)
}

This struct satisfies the Printer interface, since it has a Print() method that takes no arguments and returns no values. Here's an example of how you can use this interface:

func main() {
p := Person{"John Doe"}
PrintPerson(p)
}

func PrintPerson(p Printer) {
p.Print()
}

The PrintPerson() function takes a Printer interface as its argument, which means it can be used with any type that satisfies the Printer interface. In this case, we're passing in a Person struct, which satisfies the Printer interface because it has a Print() method.

Part 2: Interface Embedding in Go

In addition to defining standalone interfaces, Go also allows you to embed interfaces within other interfaces. This is called interface embedding, and it provides a powerful way to compose interfaces.

Here’s an example of interface embedding in Go:

type Printer interface {
Print()
}

type Scanner interface {
Scan()
}

type PrinterScanner interface {
Printer
Scanner
}

In this example, we’ve defined three interfaces: Printer, Scanner, and PrinterScanner. The PrinterScanner interface embeds the Printer and Scanner interfaces, which means any type that satisfies the PrinterScanner interface must implement both the Print() and Scan() methods.

Here’s an example of a struct that satisfies the PrinterScanner interface:

type PrinterScannerImpl struct {}

func (ps PrinterScannerImpl) Print() {
fmt.Println("Printing")
}

func (ps PrinterScannerImpl) Scan() {
fmt.Println("Scanning")
}

This struct satisfies the PrinterScanner interface, since it implements both the Print() and Scan() methods. Here's an example of how you can use this interface:

func main() {
ps := PrinterScannerImpl{}
PrintAndScan(ps)
}

func PrintAndScan(ps PrinterScanner) {
ps.Print()
ps.Scan()
}

The PrintAndScan() function takes a PrinterScanner interface as its argument, which means it can be used with any type that satisfies the PrinterScanner interface. In this case, we're passing in a PrinterScannerImpl struct, which satisfies the PrinterScanner interface because it implements both the Print() and Scan() methods.

Part 3: Interface Implementation in Go

To satisfy an interface in Go, a type must implement all of the methods defined in the interface. Let’s take a closer look at how this works.

Consider the following interface:

type Calculator interface {
Add(a, b int) int
Subtract(a, b int) int
}

Any type that wants to satisfy this interface must implement the Add() and Subtract() methods, which take two integers as arguments and return an integer result.

Here’s an example of a struct that satisfies the Calculator interface:

type SimpleCalculator struct{}

func (c SimpleCalculator) Add(a, b int) int {
return a + b
}

func (c SimpleCalculator) Subtract(a, b int) int {
return a - b
}

This struct satisfies the Calculator interface because it implements both the Add() and Subtract() methods with the correct signature.

You can also satisfy an interface with a pointer receiver. For example:

type AdvancedCalculator interface {
Multiply(a, b int) int
Divide(a, b int) int
}

type CalculatorImpl struct{}

func (c *CalculatorImpl) Multiply(a, b int) int {
return a * b
}

func (c *CalculatorImpl) Divide(a, b int) int {
if b == 0 {
return 0
}
return a / b
}

In this example, the CalculatorImpl struct satisfies the AdvancedCalculator interface. Note that the methods have a pointer receiver (*CalculatorImpl) instead of a value receiver (CalculatorImpl). This is because when you call a method on a struct, Go automatically passes a pointer to the struct as the receiver, so the methods must take a pointer receiver to satisfy the interface.

Part 4: Interface Composition in Go

In addition to interface embedding, Go also supports interface composition. Interface composition allows you to create a new interface by combining two or more existing interfaces.

Here’s an example of interface composition in Go:

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

type Writer interface {
Write(p []byte) (n int, err error)
}

type ReadWriter interface {
Reader
Writer
}

In this example, we’ve defined three interfaces: Reader, Writer, and ReadWriter. The ReadWriter interface is created by embedding the Reader and Writer interfaces. Any type that satisfies the ReadWriter interface must implement both the Read() and Write() methods.

Here’s an example of a struct that satisfies the ReadWriter interface:

type FileReaderWriter struct {
filename string
}

func (f FileReaderWriter) Read(p []byte) (n int, err error) {
// read from file
return len(p), nil
}

func (f FileReaderWriter) Write(p []byte) (n int, err error) {
// write to file
return len(p), nil
}

This struct satisfies the ReadWriter interface, since it implements both the Read() and Write() methods. Here's an example of how you can use this interface:

func main() {
f := FileReaderWriter{"myfile.txt"}
ReadAndWrite(f)
}

func ReadAndWrite(rw ReadWriter) {
// read and write using the same interface
data := make([]byte, 1024)
rw.Read(data)
rw.Write(data)
}

The ReadAndWrite() function takes a ReadWriter interface as its argument, which means it can be used with any type that satisfies the ReadWriter interface. In this case, we're passing in aFileReaderWriter struct, which satisfies the ReadWriter interface.

Part 5: Empty Interfaces in Go

So far, we’ve seen examples of interfaces that define one or more methods. But Go also supports something called an empty interface, which doesn’t define any methods. An empty interface can hold any value, regardless of its type.

Here’s an example of an empty interface:

func PrintAny(value interface{}) {
fmt.Println(value)
}

This function takes an empty interface as its argument. This means that it can accept any value, regardless of its type. Here’s an example of how you can use this function:

func main() {
PrintAny(42)
PrintAny("hello")
PrintAny(true)
}

In this example, we’re calling the PrintAny() function with three different types of values: an int, a string, and a bool. Since the function takes an empty interface as its argument, it can accept any type of value.

Empty interfaces can be useful in certain situations, such as when you want to write a function that can accept any type of value as its argument.

Part 6: Bonus Section — Examples of Interfaces in Popular Go Packages

To help you get a better understanding of how interfaces are used in Go, let’s take a look at some examples from popular Go packages.

1. http.Handler interface

The http.Handler interface is used in the Go standard library's net/http package to define a handler for HTTP requests. Any type that implements the ServeHTTP() method with the correct signature can be used as an http.Handler.

type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

Here’s an example of how you can use this interface:

type MyHandler struct{}

func (h MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, world!")
}

func main() {
handler := MyHandler{}
http.ListenAndServe(":8080", handler)
}

In this example, we’re defining a new type called MyHandler that implements the http.Handler interface. We're then using this type as the handler for an HTTP server using the http.ListenAndServe() function.

2. io.Reader and io.Writer interfaces

The io.Reader and io.Writer interfaces are used in the Go standard library's io package to define input and output streams. Any type that implements the Read() or Write() method with the correct signature can be used as an io.Reader or io.Writer.

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

type Writer interface {
Write(p []byte) (n int, err error)
}

Here’s an example of how you can use these interfaces:

func main() {
data := make([]byte, 1024)
file, err := os.Open("myfile.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()

// read from file
file.Read(data)

// write to file
file.Write(data)
}

In this example, we’re using the os.Open() function to open a file and return an os.File struct, which satisfies both the io.Reader and io.Writer interfaces. We're then reading and writing data to the file using the Read() and Write() methods.

3. database/sql/driver interfaces

The database/sql/driver package is used to define database drivers for the Go standard library's database/sql package. Any database driver must implement the driver.Driver interface, which defines a method called Open() that returns a new driver.Conn interface.

type Driver interface {
Open(name string) (Conn, error)
}

Here’s an example of how you can use this interface:

func main() {
driverName := "mysql"
dataSourceName := "user:password@tcp(localhost:3306)/mydb"

db, err := sql.Open(driverName, dataSourceName)
if err != nil {
log.Fatal(err)
}
defer db.Close()

// use the database
rows, err := db.Query("SELECT * FROM mytable")
if err != nil {
log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
// process each row
}
}

In this example, we’re using the sql.Open() function to create a new database connection using the mysql driver. The Open() function returns a new sql.DB struct, which satisfies the driver.Conn interface. We're then using this connection to execute a SQL query using the db.Query() method.

4. sort.Interface interface

The sort.Interface interface is used in the Go standard library's sort package to define custom sorting for types. Any type that implements the Len(), Less(), and Swap() methods with the correct signatures can be used as a sort.Interface.

type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}

Here’s an example of how you can use this interface:

type Person struct {
Name string
Age int
}

type ByAge []Person

func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }

func main() {
people := []Person{
{"Bob", 32},
{"Alice", 24},
{"Charlie", 48},
}

sort.Sort(ByAge(people))

fmt.Println(people)
}

In this example, we’re defining a new type called ByAge that implements the sort.Interface interface. We're then using this type to sort a slice of Person structs by their age.

5. flag.Value interface

The flag.Value interface is used in the Go standard library's flag package to define custom flags for command-line programs. Any type that implements the String() and Set() methods with the correct signatures can be used as a flag.Value.

type Value interface {
String() string
Set(string) error
}

Here’s an example of how you can use this interface:

type Celsius float64

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

func (c *Celsius) Set(s string) error {
var unit string
var value float64
fmt.Sscanf(s, "%f%s", &value, &unit)
switch unit {
case "C", "°C":
*c = Celsius(value)
return nil
case "F", "°F":
*c = Celsius((value - 32) * 5 / 9)
return nil
}
return fmt.Errorf("invalid temperature %q", s)
}

func main() {
var temp Celsius
flag.Var(&temp, "temp", "the temperature")
flag.Parse()

fmt.Println(temp)
}

In this example, we’re defining a new type called Celsius that implements the flag.Value interface. We're then using this type to define a custom flag called temp that accepts temperatures in either Celsius or Fahrenheit. We're using the flag.Var() function to register the flag with the flag package, and we're using the flag.Parse() function to parse the command-line arguments and set the value of the temp flag.

FAQ

Q1: What is the difference between “Interface embedding” and “Interface composition”?

Answer:

  1. Interface Embedding:
  • Interface embedding refers to embedding one interface into another interface.
  • In Go, you can embed an interface as a method set within another interface, allowing the embedding interface to inherit the methods of the embedded interface.
  • The embedding interface gains all the methods of the embedded interface, and it can also define additional methods.
  • The embedding interface acts as a superset of the embedded interface, providing a way to extend or specialize the behavior of the embedded interface.
  • The embedded interface’s methods can be directly accessed through the embedding interface, without the need for explicit forwarding or delegation.

2. Interface Composition:

  • Interface composition refers to combining multiple interfaces into a single interface.
  • With interface composition, you define a new interface that includes multiple existing interfaces as embedded types.
  • The composed interface combines the method sets of the embedded interfaces, creating a unified set of methods.
  • The composed interface can be used to interact with any object that implements all the embedded interfaces.
  • Interface composition provides a way to define more specific, higher-level interfaces by combining existing interfaces.

In summary, interface embedding focuses on embedding one interface into another, inheriting its methods and allowing for extension, while interface composition combines multiple interfaces to create a unified set of methods, enabling higher-level abstractions. Both techniques are powerful tools for building flexible and reusable code in Go.

An additional example

  1. Interface Composition: Interface composition involves creating a new interface by combining multiple existing interfaces. This approach allows you to define a new interface that incorporates the behavior of the individual interfaces it comprises. In other words, it is a way of combining multiple interfaces to create a more specific or comprehensive interface.

For example, let’s say we have two interfaces: Flyable and Swimmable. We can compose a new interface called Bird by combining these two interfaces:

type Flyable interface {
Fly()
}

type Swimmable interface {
Swim()
}

type Bird interface {
Flyable
Swimmable
}

In this example, the Bird interface is composed of the Flyable and Swimmable interfaces. It incorporates the behavior of both interfaces, so any type that implements the Bird interface must also implement the Fly() and Swim() methods.

2. Interface Embedding: Interface embedding, also known as interface inheritance, is a mechanism in which one interface is embedded within another interface. This allows the embedding interface to inherit the methods of the embedded interface, effectively extending its behavior.

Continuing with our previous example, let’s consider an interface called Duck that embeds the Bird interface:

type Duck interface {
Bird
Quack()
}

In this case, the Duck interface embeds the Bird interface, and it adds an additional method called Quack(). As a result, any type that implements the Duck interface must implement all the methods of the Bird interface (Fly() and Swim()) as well as the Quack() method.

Differences:

  • Interface composition involves combining multiple interfaces to create a new interface with a specific set of behaviors.
  • Interface embedding is a way of inheriting methods from an embedded interface and extending it with additional methods.
  • Interface composition creates a new interface that includes the behaviors of the composed interfaces, while interface embedding allows one interface to inherit the methods of another interface.
  • Interface composition is typically used when you want to define a higher-level interface that combines multiple behaviors, while interface embedding is used to extend an existing interface with additional methods.

Conclusion

In conclusion, interfaces are a powerful feature of the Go programming language that enable polymorphism and flexibility in your code. By defining interfaces, you can write code that’s more modular, easier to test, and easier to extend. We’ve covered a lot in this article, including what interfaces are, how to define and use them, and some examples of how they’re used in popular Go packages.

If you’re new to Go, I hope this article has given you a good understanding of interfaces and how to use them in your code. If you’re already familiar with Go, I hope this article has given you some new ideas for how to use interfaces in your projects.

Remember, interfaces are a powerful tool, but they’re not always necessary. If you find that your code doesn’t need the flexibility that interfaces provide, it’s okay to stick with concrete types.

As with all things in programming, there’s no one-size-fits-all solution. It’s up to you to weigh the pros and cons of using interfaces in your code, and decide what’s best for your particular situation.

I hope you’ve enjoyed this article and that it’s helped you understand interfaces in Go a little better. If you have any questions or feedback, please feel free to leave a comment below.

Happy coding :)

--

--

Jamal Kaksouri

A Software Engineer | Freelancer | Writer | Passionate about building scalable, performant apps and continuously learning