Lesson 3

Understanding Interfaces in Go: How to Define and Implement Them

Topic Overview and Goals

Welcome to our deep dive into one of the powerful facets of Go programming - interfaces. Think of these as a job description that defines a role's required skills, just as interfaces specify the functions that our Go types need to implement. By the end of this lesson, you will be able to confidently create effective interfaces in Go.

Understanding What Interfaces Are

An interface, in computing, is a communication point between objects; it defines behaviors that an object can implement. Consider a soccer player who must kick a ball, sprint, and follow game rules — these behaviors are akin to methods in an interface.

Internally, an interface in Go is represented by a tuple (type, value), comprising the concrete type of the value it holds and the value itself. This design enables Go's runtime to perform dynamic type checking and allows interfaces to be versatile. When a value is assigned to an interface, Go stores this tuple, facilitating type assertions and interface conversions by preserving the concrete type's information.

One important thing to note is that an interface in Go retains its type even when holding a nil value; thus, it's only truly nil if both the type and value are nil. When not checked properly, this can lead to subtle bugs.

Declaring Interfaces in Go

To declare interfaces in Go, we use the type keyword, followed by the interface's name, the interface keyword, and a set of methods enclosed within {} brackets. Take a look at the example below:

Go
1type Player interface { 2 Play() 3 Score() int 4}

Here, we've created a Player interface with two methods: Play and Score. Any type that implements this interface must define these methods.

How Interfaces Interact with Go Types

А type implements an interface by implementing its methods. In Go, there is no explicit declaration that a type implements an interface. To demonstrate this, let's create a struct, Footballer, that implements the Player interface:

Go
1type Footballer struct { 2 Name string 3 Goals int 4} 5 6// Implements the Play method of the Player interface 7func (f Footballer) Play() { 8 fmt.Println(f.Name, "is playing football!") 9} 10 11// Implements the Score method of the Player interface 12func (f Footballer) Score() int { 13 return f.Goals 14}

By defining the Play and Score methods for Footballer, it now implements the Player interface.

Understanding Go's Interface Conversions and Type Assertions

A type assertion provides access to an interface value's underlying concrete value. An interface can hold any type of value and sometimes you might want to get the actual type of the value that is being held in the interface. Here's where type assertions become necessary.

t := i.(T) This statement asserts that the interface value i holds the concrete type T and assigns the underlying T value to the variable t. Let's consider an example:

Go
1type Stringer interface { 2 String() string 3} 4 5type Student struct { 6 Name string 7} 8 9func (s Student) String() string { 10 return s.Name 11} 12 13func main() { 14 var s Stringer = Student{"John Doe"} 15 value := s.(Student) // Type assertion to obtain the actual struct value 16 fmt.Println(value) 17}

In this example, value := s.(Student) uses a type assertion to extract the struct value from an interface.

Empty Interface and its Usage

An empty interface in Go has no methods. Since all types inherently implement no methods, every type implements the empty interface. Here's how to use it:

Go
1func PrintAnything(val interface{}) { 2 fmt.Println(val) 3} 4 5func main() { 6 PrintAnything(23) // print an integer 7 PrintAnything("Hello") // print a string 8 PrintAnything(63.15) // print a float 9}

The PrintAnything function, which accepts arguments of any type, exemplifies the usage of the empty interface.

The Practical Benefits of Interfaces in Go

Understanding interfaces theoretically is beneficial, but observing their practical applications illustrates their true power in Go programming. Interfaces foster flexibility and decoupling in code design, making it easier to manage and extend. Let's explore this through an example:

Consider an application that sends notifications. The initial requirement is to send these notifications via email, but soon, you'll need to incorporate SMS and push notifications. This is where interfaces shine. You can define a Notifier interface and then implement this interface for different notification types.

First, define the Notifier interface:

Go
1type Notifier interface { 2 Notify(message string) error 3}

Implement the interface for an Email notifier:

Go
1type EmailNotifier struct { 2 EmailAddress string 3} 4 5func (e EmailNotifier) Notify(message string) error { 6 // Logic to send an email notification 7 fmt.Printf("Email sent to %s: %s\n", e.EmailAddress, message) 8 return nil 9}

Later on, adding an SMS notifier becomes seamlessly easy, without altering the core logic of your notification system:

Go
1type SMSNotifier struct { 2 PhoneNumber string 3} 4 5func (s SMSNotifier) Notify(message string) error { 6 // Logic to send an SMS notification 7 fmt.Printf("SMS sent to %s: %s\n", s.PhoneNumber, message) 8 return nil 9}

This design provides tremendous flexibility. You can add as many notifier types as the application requires over time without modifying the existing notification logic. This exemplifies the principle of "programming to interfaces, not implementations," facilitating easy maintenance and scalability in software projects.

Lesson Summary and Getting Ready for Practice

Great work! You've learned about Go's interfaces, how to define them and how types implement them, as well as interface conversions, type assertions, and the usage of the empty interface. Apply your newfound knowledge of the interfaces in Go during our next practice session! Get ready to ace it!

Enjoy this lesson? Now it's time to practice with Cosmo!

Practice is how you turn knowledge into actual skills.