programming

Getting to Know Structs and Interfaces in Golang

Although it is possible for us to write programs using only Go’s built-in data types, at some point it will become very tedious. Consider a program that interacts with a form like the code below.

package main

import (
	"fmt"
	"math"
)

func distance(x1, y1, x2, y2 float64) float64 {
	a := x2 - x1
	b := y2 - y1
	return math.Sqrt(a*a + b*b)
}

func rectangleArea(x1, y1, x2, y2 float64) float64 {
	l := distance(x1, y1, x1, y2)
	w := distance(x1, y1, x2, y1)
	return l * w
}

func circleArea(x, y, r float64) float64 {
	return math.Pi * r * r
}

func main() {
	var rx1, ry1 float64 = 0, 0
	var rx2, ry2 float64 = 10, 10
	var cx, cy, cr float64 = 0, 0, 5
	fmt.Println(rectangleArea(rx1, ry1, rx2, ry2))
	fmt.Println(circleArea(cx, cy, cr))
}

Keeping track of all the coordinates makes it difficult to see what the program is doing and will likely cause errors.

Structs

An easy way to make this program better is to use struct. A struct is a type that contains named fields. For example we can represent a circle like this:

type Circle struct {
  x float64
  y float64
  r float64 
}

The type keyword introduces a new type. Followed by the type name (Circle), the keyword struct to indicate that we are defining the type struct and a list of fields in curly braces. Each field has a name and type. As with functions, we can collapse fields that have the same type:

type Circle struct {
     x, y, r float64
}

Initialization

We can create instances of our new Circle type in various ways:

  var c Circle

Like other data types, this will create a local variable Cirlce which is set to zero by default. For struct zero means each field is set to the corresponding zero value (0 for int, 0.0 for float, "" for string, nil for pointer, …) We also can use the new function:

  c := new(Circle)

It allocates memory for all fields, sets each to the value zero and returns a pointer. (*Circle) More often we use it to assign a value to each field. We can do this in two ways. Like this:

  c := Circle{x: 0, y: 0, r: 5}

Or we can ignore the field names if we know their order:

  c := Circle{0, 0, 5}

Fields

We can access field using the . operator:

  fmt.Println(c.x, c.y, c.r)
  c.x = 10
  c.y = 5

Let’s change the circleArea function so that it uses Circle:

func circleArea(c Circle) float64 {
     return math.Pi * c.r*c.r
}

then inside the main function we give:

  c := Circle{0, 0, 5}
  fmt.Println(circleArea(c))

One thing to remember is that arguments are always copied in Go. If we try to change any of the fields inside the circleArea function, it will not change the original variable. Therefore, we usually write functions like this:

func circleArea(c *Circle) float64 {
     return math.Pi * c.r*c.r
}

And change the main function to:

  c := Circle{0, 0, 5}
  fmt.Println(circleArea(&c))

##Methods While this is better than the first version of this code, we can change it by using a special type of function known as method:

func (c *Circle) area() float64 {
     return math.Pi * c.r*c.r
}

Between the func keyword and the function name, we have added receiver. receiver is like a parameter it has a name and a type but by creating a function this way, we can call the function using the . operator:

  fmt.Println(c.area())

This is much easier to read, we no longer need the & operator (Go automatically knows to pass a pointer to a circle for this method) and since this function can only be used with circle, we can rename the function to just area.

Let’s do the same for the rectangle:

type Rectangle struct {
     x1, y1, x2, y2 float64
}
func (r *Rectangle) area() float64 {
     l := distance(r.x1, r.y1, r.x1, r.y2)
     w := distance(r.x1, r.y1, r.x2, r.y1)
     return l * w
}

and on main we have:

r := Rectangle{0, 0, 10, 10}
fmt.Println(r.area())

Embedded Types

Field struct usually represents interlocking. For example Circle has a radius. Suppose we have a person struct:

type Person struct {
    Name string
}
func (p *Person) Talk() {
    fmt.Println("Hi, my name is", p.Name)
}

And we want to create a new Android struct. We can do this:

type Android struct {
    Person Person
    Model string
}

Go supports struct attachment like this by using embedded types. Also known as anonymous fields, embedded types look like this:

type Android struct {
    Person
    Model string
}

We use a type (Person) and don’t give it a name. When defined this way, the Person struct can be accessed using the type name:

  a := new(Android)
  a.Person.Talk()

But we can also call any Person method directly in Android:

a := new(Android)
a.Talk()

Interfaces

Both in real life and in programming, relationships like this are commonplace. Go has a way of making these unintentional similarities explicit through a type known as Interface. Here is an example of a Shape interface:

type Shape interface {
  area() float64
}

Like a struct, an interface is created using type, followed by a name and interface. But to define Shape, we need a "method set". A method set is a list of methods that a type must have in order to “implement” an interface.

func totalArea(shapes ...Shape) float64 {
	var area float64
	for _, s := range shapes {
		area += s.area()
	}
	return area
}

we will call this function like this:

  fmt.Println(totalArea(&c, &r))

Interface can also be used as fields:

type MultiShape struct {
     shapes []Shape
}

We can even change MultiShape to Shape by giving it an area method:

func (m *MultiShape) area() float64 {
	var area float64
	for _, s := range m.shapes {
		area += s.area()
	}
	return area
}

Now MultiShape can contain Circles, Rectangles or even other MultiShapes.

Below is a more complete program

package main

import (
	"fmt"
	"math"
)

type Circle struct {
	x, y, r float64
}

func (c *Circle) area() float64 {
	return math.Pi * c.r * c.r
}

type Rectangle struct {
	l, w float64
}

func (r *Rectangle) area() float64 {
	return r.l * r.w
}

type Shape interface {
	area() float64
}

type MultiShape struct {
	shapes []Shape
}

func (m *MultiShape) area() float64 {
	var area float64
	for _, s := range m.shapes {
		area += s.area()
	}
	return area
}

func totalArea(shapes ...Shape) float64 {
	var area float64
	for _, s := range shapes {
		area += s.area()
	}
	return area
}

func main() {
	c := Circle{0, 0, 5}
	r := Rectangle{3, 4}
	fmt.Println(totalArea(&c, &r))

	m := MultiShape{}
	m.shapes = append(m.shapes, &c, &r)
	fmt.Println(m.area())
}
comments powered by Disqus