Video Coming Soon...
23: Struct and Methods: Interfaces
WARNING This exercise is in DRAFT status, so there may be errors. If you find any, please email me at help@learncodethehardway.com so I can fix them.
We're now going to create a better "universal Fruit and Animal" printer using the interfaces feature of Go. You can think of an interface as a protocol that says, "If a type has this method, then I can use that type without knowing anything else about it." The Stringer interface is the prime example of this:
type Stringer interface {
String() string
}
This looks similar to a type X struct
but you use type X interface
instead. When you do this Go creates a "fake type" that is valid as long as the target struct
implements a String() string
function. For example, if I want my Fruit
to follow the Stringer
protocol then I could have this:
func (fruit Fruit) String() string {
return Fruit.Name
}
DODGY BULLSHIT WARNING I have to more deeply research this as I think this is why but I actually can't find a solid explanation.
You may notice that (fruit Fruit)
doesn't have a pointer syntax (fruit *Fruit)
. This is because an interface
is implemented as a "fat pointer" which is just a pointer with extra gear, and adding another *
on it would cause problems when you use the interface. You'd have to do extra to use a String() string
that also did (fruit *Fruit)
, so just remember:
When it's an interface, don't use a "method pointer style" function.`.
Interfaces vs. type switch
This is why I called that function in Exercise 22 a BadPrinter
. If you need to change the behavior depending on the type it's better to use an interface
because as you add more types you'll have to constantly expand the switch
.
With an interface
you just make a function that does the thing and that's it. Need to fix the function, go to the implementation of the interface
rather than digging around in the guts of some 7000 lines long switch statement.
If that's the case then what are type switch
good for? I usually see them in systems that have events you need to handle differently. In the tcell package it's used to indicate if an event is a keyboard, mouse, or screen resize event.
In that case an interface
won't help much because your program has to take what tcell
gives you and make a decision based on it. The alternative for tcell
would be a large number of int
constants, but then you couldn't get the event data easily from tcell
. A type switch
that decides on the type and also gets the value is far better.
The Code
This code demonstrates using an interface
to print Fruit
and Animal
types:
View Source file ex23/main.go Only
package main
import (
"fmt"
)
type Printer interface {
Print()
}
type Animal struct {
Name string
Age int
Species string
Weight float64
}
type Fruit struct {
Name string
Taste string
Smell string
}
func (what Animal) Print() {
fmt.Println("-- Animal named", what.Name)
fmt.Println(what.Name, "is", what.Age, "years old.")
fmt.Println(what.Name, "is a", what.Species)
fmt.Println(what.Name, "weighs", what.Weight)
}
func (what Fruit) Print() {
fmt.Println("-- Fruit named", what.Name)
fmt.Println(what.Name, "tasts like", what.Taste)
fmt.Println(what.Name, "smells like", what.Smell)
}
func PrintThing(out Printer) {
out.Print()
}
func main() {
joey := Animal{"Joey", 3, "Kangaroo", 100.0}
PrintThing(joey)
durian := Fruit{"Durian", "Heaven", "Dead bodies and onions."}
PrintThing(durian)
}
The Breakdown
type Printer interface {
-- This creates aninterface
named Printer. Anything that has these functions will be considered useable as aPrinter
.Print()
-- The only funtion thatPrinter
requires is aPrint()
function.}
-- This ends thePrinter
interface.func (what Animal) Print() {
-- We define thePrint()
forAnimal
and that is all we need for it to work with thePrinter
interface.func (what Fruit) Print() {
-- Same but forFruit
.func PrintThing(out Printer) {
-- This is a function that takes anything that implements thePrinter
interface. As long as yourstruct
has a method namedPrint()
thenPrintThing
knows how to use it.out.Print()
-- Here's where we callPrint()
, and Go figures out which one to call based on the type forout
.
This code then demonstrates calling PrintThing()
on both a Fruit
and an Animal
.
The Practice
- Break It -- Create a new type and try passing it to
PrintThing()
but do not have aPrinter
interface for it. Remember, the only thingPrintThing()
requires is that your type have aPrint()
function, so just don't have that and you should get an error. - Change It -- Now make your new type correctly implement
Print()
soPrinter
work. - Recreate It -- Take some time to describe this code to yourself and then recreate it, or if you have your own idea that can use this then make that from your own description.
Study Drills
- Study the
Stringer
interface and use that as well with these types. - Try changing the
Print()
methods to take pointers to their types and see how that changes your code.
Register for Learn Go the Hard Way
Register today for the course and get the all currently available videos and lessons, plus all future modules for no extra charge.