In Go, interfaces define a set of method signatures. Interfaces are types themselves, but they are used to define what other types (such as structs) can do.

A concrete type that implements all of the methods of an interface “satisfies” that interface.

Interfaces are useful because they let us write functions that can accept a general interface type rather than a specific concrete type, which makes code more flexible. The cost of this is that it can also make the code more abstract.

Example

Here’s an example demonstrating the concept. We can define a Shape interface that has a single method: Area(), which requires no arguments and returns a float64. We can then define concrete types (structs) for Circle and Rectangle. Since both have an Area() method that accept no arguments and return a float64, they satisfy the Shape interface, which means we can pass either a Circle or Rectangle to the printArea() function (which takes a Shape as its one argument).

package main
 
import "fmt"
 
// Define the Shape interface
type Shape interface {
    Area() float64
}
 
// Define a struct for a Circle
type Circle struct {
    Radius float64
}
 
// Implement the Area method for Circle
func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}
 
// Define a struct for a Rectangle
type Rectangle struct {
    Width  float64
    Height float64
}
 
// Implement the Area method for Rectangle
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}
 
// A function that works with any type that satisfies the Shape interface
func printArea(s Shape) {
    fmt.Printf("The area is: %f\n", s.Area())
}
 
func main() {
    circle := Circle{Radius: 5}
    rectangle := Rectangle{Width: 10, Height: 5}
 
    // Both Circle and Rectangle satisfy the Shape interface, so they can be passed to printArea
    printArea(circle)
    printArea(rectangle)
}

A Less Contrived Example

Here’s a slightly less contrived example that also illustrates the concept of dependency injection. Imagine a case where we need to interact a database, and we have concrete services that handle database interactions for various data models, Foo and Bar:

//defining some data models
type Foo struct {
	x string
}
 
type Bar struct {
	y    string
	foos []*Foo
}
 
//defining some services to interact with the database w/r/t these data models
type FooService struct {
	db *sql.DB
}
 
type BarService struct {
	db *sql.DB
}
 
//write methods
func (fs FooService) CreateFoo(f *Foo) {
	//implementation details
}
 
func (bs BarService) CreateBar(b *Bar) {
	//implementation details
}
 

One thing we need to consider here is that our Bar data model includes a slice of Foos. So when write a Bar to our database using the CreateBar method, we’ll probably end up calling the CreateFoo method to write each of Bar’s constituent Foos to the database. Or at least that’s one approach. It could look something like this:

func (bs BarService) CreateBar(b *Bar) {
	//code to write `Bar` to the db
	//etc.
	
	//code to loop through and write Foo's to db
	fs := FooService{db: bs.db}
	
	for _, foo := range b.foos {
		fs.CreateFoo(foo)	
	}
}

Our issue then ends up being how do we ensure atomicity of the entire CreateBar operation if we choose to loop through multiple iterations of CreateFoo within the method call?

We want to use a database transaction (i.e. *sql.Tx) to solve this issue, which groups multiple operations together and only succeeds if all of the constituent operations succeed. But our code doesn’t currently allow this.

Interfaces to the rescue, though! We can implement a querier interface, then refactor our FooService struct to take a querier rather than a *sql.DB:

type querier interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
 
type FooService struct {
	db querier
}

This works because both *sql.Tx and *sql.DB implement the methods defined by our querier interface. So we can resolve our atomicity issue by beginning a database transaction in the CreateBar() method, then passing this transaction to FooService, e.g.:

func (bs BarService) CreateBar(b *Bar) {
	ctx := context.Background()
	
	tx, err := bs.db.BeginTx(ctx, nil)
	//handle error
	
	defer() tx.Rollback()
	
	//code to write Bar to the db
	//etc
	
	//create a FooService using the transaction	
	txFS := FooService{db: tx}
	
	for _, foo := range b.foos {
		txFS.CreateFoo(foo)	
	}
 
	//etc
}

Interface Composition

ADD THIS