Beginning Programming Lesson 05

Inheritance, Interfaces, and Type Assertion

We created complex data types (structs/classes), but what if we wanted to create another struct that expands upon another? Or you wanted to create a number of different structs that play by a certain set of rules. The example code for today can be found here.

package main

import (
	"fmt"
	"math"
)

type Polygon interface {
	Shape() string
	Perimeter() float64
	Area() float64
}

type Rectangle struct {
	Length float64
	Height float64
}

func (r Rectangle) Perimeter() float64 {
	return 2*r.Height + 2*r.Length
}

func (r Rectangle) Area() float64 {
	return r.Height * r.Length
}
func (r Rectangle) Shape() string {
	return "rectangle"
}
func (r Rectangle) String() string {
	return fmt.Sprintf("Shape: %s, Dimensions: %fx%f, Perimeter: %f, Area: %f", r.Shape(), r.Length, r.Height, r.Perimeter(), r.Area())
}

type ColoredRectangle struct {
	Rectangle
	Color string
}

func (r ColoredRectangle) String() string {
	return fmt.Sprintf("Shape: %s, Dimensions: %fx%f, Perimeter: %f, Area: %f, Color: %s", r.Shape(), r.Length, r.Height, r.Perimeter(), r.Area(), r.Color)
}

type Triangle struct {
	Sides [3]float64
}

func (t Triangle) Perimeter() float64 {
	return t.Sides[0] + t.Sides[1] + t.Sides[2]
}

func (t Triangle) Area() float64 {
	p := t.Perimeter() / 2
	return math.Sqrt(p * (p - t.Sides[0]) * (p - t.Sides[1]) * (p - t.Sides[2]))
}

func (t Triangle) Shape() string {
	return "triangle"
}

func (t Triangle) String() string {
	return fmt.Sprintf("Shape: %s, Dimensions: %fx%fx%f, Perimeter: %f, Area: %f", t.Shape(), t.Sides[0], t.Sides[1], t.Sides[2], t.Perimeter(), t.Area())
}

func printSlice(s []interface{}) {
	for i, v := range s {
		fmt.Printf("%d - %v\n", i, v)
	}
}

func main() {
	rect := Rectangle{Length: 5, Height: 4}
	tri := Triangle{Sides: [3]float64{4, 5, 6}}

	fmt.Println(rect)

	fmt.Println(tri)

	var polys []Polygon

	polys = append(polys, rect)
	polys = append(polys, tri)

	var cr ColoredRectangle
	cr.Height = 7
	cr.Length = 8
	cr.Color = "red"

	polys = append(polys, cr)

	fmt.Println(polys)

	for _, p := range polys {
		fmt.Println(p)
	}

	fmt.Printf("Shape 1 type: %s\n", polys[0].Shape())
	random := []interface{}{1, "blue", rect, tri}

	printSlice(random)

	if _, ok := random[1].(Polygon); !ok {
		fmt.Println("Not a polygon")
	}

	fmt.Println(random[2].(Polygon).Shape())

}

Interfaces

An interface in general provides a way of communication between two or more parties. A GUI (Graphical User Interface) provides a means communication between a computer and a human. In the case of Golang and many other programming languages, an interface is a definition of how a struct must behave in order to work with other portions of code that expect them to follow the definition. In our last lesson we created a struct polygon. It had a method to calculate the perimeter. What if we wanted to have it calculate area? Well, the area calculation varies from polygon to polygon. While there are a number of ways we could deal with this, let’s see how we could use interfaces to help.

Defining an Interface

type Polygon interface {
	Shape() string
	Perimeter() float64
	Area() float64
}

Here we created an interface Polygon and stated for a struct to be considered a Polygon it must have at least the three methods Shape, Perimeter, and Area implemented.

Implementing an Interface

In many languages when you define as struct or class, you must explicitly state which interfaces it is implementing. This is not the case for Golang.

type Rectangle struct {
	Length float64
	Height float64
}

func (r Rectangle) Perimeter() float64 {
	return 2*r.Height + 2*r.Length
}

func (r Rectangle) Area() float64 {
	return r.Height * r.Length
}
func (r Rectangle) Shape() string {
	return "rectangle"
}

Here we created a struct called Rectangle and since it implements all the methods defined in the Polygon interface, it is considered as a Polygon as well.

Using an Interface

So, we have a Rectangle now that is also a Polygon, but where did that get us? Lets say we wanted to create a function that performed some action on any Polygon for example:

    func PrintPolygon(p Polygon) {
        fmt.Printf("type: %s, perimeter: %f, area: %f\n", p.Shape(), p.Perimeter(), p.Area() )
    }

You could pass any instance of a struct as long as it implements the Polygon interface to this function. While the above example works well to show a usage of interfaces, there is another way to achieve a similar result

Stringer Interface

There is aa interface in the fmt package in Golang referred to as Stringer.

type Stringer interface {
    String() string
}

If you pass a variable to a method in fmt and the expected result is a string, the package will try to call the String method on the variable automatically. We can make Rectangle a Stringer by adding:

func (r Rectangle) String() string {
	return fmt.Sprintf("Shape: %s, Dimensions: %fx%f, Perimeter: %f, Area: %f", r.Shape(), r.Length, r.Height, r.Perimeter(), r.Area())
}

Now if we wanted to print Rectangle it might look like:

rect := Rectangle{Length: 5, Height: 4}
fmt.Println(rect)

Generic Interface

In Golang there is another type of interface which is referred to as simply interface. It is used when it doesn’t matter what the type of a variable is. This can be useful for when the type of the variable is irrelevant to the actions being performed on it. For example, the len function is used to get the number of items in a slice, but it doesn’t need to know what the type of the items are, or even if they are all the same type

Using interface

If you wanted to define a variable or argumetn to a function as a typeless interface, you can specify the type as interface{} Check out:

func printSlice(s []interface{}) {
	for i, v := range s {
		fmt.Printf("%d - %v\n", i, v)
	}
}

This function takes a slice and prints out the position in the slice along with the contents at that position. It doesn’t care it the slice holds, Rectangle, Polygon, int, etc, or any combination of types:

random := []interface{}{1, "blue", rect, tri}
printSlice(random)

Type Assertion

What happens if we have a variable that is an interface{} but want to treat is as a specific type, or we want to test if is is really a specific type?

if _, ok := random[1].(Polygon); !ok {
	fmt.Println("Not a polygon")
}

Here we are checking to see if the element in position 1 of the random slice is a Polygon the .(Polygon) operation has two return values, one is the variable in question now assigned the type Polygon and the other is whether the assertion was successful. In this case we only care if the assertion is successful, since we only want to see if it is a Polygon

Inheritance

What if we wanted to create a group of types that have something in common? For instance, if we wanted to create a ColoredRectangle isn’t is also a Rectangle with some extra information? We can define an new struct that inherits the member variables and methods of another:

type ColoredRectangle struct {
	Rectangle
	Color string
}

Now we have a new type called ColoredRectangle that is a Rectangle, but has an additional variable to store its color.

var cr ColoredRectangle
cr.Height = 7
cr.Length = 8
cr.Color = "red"

Here we set the Height and Width that were inherited from Rectangle. We also set its Color.

Conclusion

We learned how we can expand the usefulness and reusability of structs, along with how to start working with variables of “un-pre-determined” types. Look through the example code again. Does it seem more clear now? Can you think of a better way to implement the various Polygon structs? In the next lesson we will look at Recursion and Error Handling.


See also