What are the Struct and Interface Types in Go?

April 26, 2020

This is the fifth entry of my weekly series Learning Go. Last week I covered a few more pieces of the Slice and Map type. This week I will be talking about the Struct and Interface types.

Struct

A struct is a data structure that allows you to compose values of different types. Because of that, a struct is a great way to aggregate data. From a computer science perspective, a struct in Go is considered a composite data type.

This simply means that this is a data type which can be constructed using the language’s primitive data types (string, int, etc), or other composite types. Let’s see one in action.

In this example I will be creating a struct with primitive data types:

package main

import (
	"fmt"
)

type car struct {
	model string
	color string
	year  int
}

func main() {
	c := car{
		model: "tacoma",
		color: "white",
		year:  2020,
	}
	fmt.Println(c)
	// {tacoma white 2020}
}

In the example above, I am creating a new struct of type car.

  • first, I declare that I am creating a new type
  • then I declare an identifier for this type, in this case, our type is car
  • we declare our new type, car, to have the underlying type of struct
  • next, we list out the field names paired with their type

Anonymous struct

Like many things in programming, there is more than one way to do something. The same can be said about creating a struct. If you are wanting to use a struct for a specific scope, there is a short-hand way to declare them.

package main

import "fmt"

func main() {
	c := struct {
		model string
		color string
		year  int
	}{
		model: "tacoma",
		color: "white",
		year:  2020,
	}
	fmt.Println(c)
	// {tacoma white 2020}
}

Let me walk you through what is happening in this example:

  • we are declaring a new variable, c of type struct
  • then, inside the brackets {}, on the left-hand side, we declare our field names
  • on the right-hand side, we declare the type of each respective field name
  • last, and most importantly, inside another set of brackets {} we declare the name and the value of these field names

Important note: you must place a comma after each entry in a struct, or you will get an error from the compiler that looks a little bit like this:

syntax error: unexpected newline, expecting comma or }

Method sets

Methods are used heavily in programming and that is no different in Go. Thinking in terms of traditional Object Oriented paradigms, a method is defined and called in relation to the Class it was defined in.

In Go, a type may have a method associated with it, most commonly with a struct. Let’s take a look at an example using a method of a struct type:

package main

import (
	"fmt"
)

type toyota struct {
	model string
	color string
	year  int
}

func (t toyota) start() {
	fmt.Println("vroom vroom")
}

func main() {
	t := toyota{
		model: "tacoma",
		color: "white",
		year:  2020,
	}
	t.start()
	// vroom vroom
}
  • we define a new type with the identifier toyota with an underlying type of struct
  • using the func keyword, we create a new function
  • next we see (t toyota), pay attention to toyota here, this is what is called a receiver type - this means this method can only be called by a toyota type
  • in this example the t is a value receiver - it is possible to use a pointer receiver as well
  • using dot notation we can pull values from t - I will show you how in the next example below
package main

import (
	"fmt"
)

type toyota struct {
	model string
	color string
	year  int
}

func (t toyota) start() {
	fmt.Println("Hey! I'm a ", t.color, t.year, t.model)
}

func main() {
	t := toyota{
		model: "tacoma",
		color: "white",
		year:  2020,
	}
	t.start()
	// Hey I'm a white 2020 tacoma
}

This example is identical to the previous; however, the change to note here is what is happening inside of the start method.

  • we see that we still have a receiver type of toyota with a receiver value t
  • if we take a look at the toyota type, we see that it has three field names: model, color, and year
  • inside of func main we are creating a new variable named t
  • using a composite literal, we assign the variable t to be of type toyota and assign the values tacoma, white, and 2020 to their respective field names
  • this is where the magic happens: using dot notation, we call the start method from t
  • because t is of type toyota it has access to the start method
  • inside of start we are again using dot notation to print out the values of the fields found in toyota

Interfaces

An interface is both a type and how you name a group of methods in Go. Let’s jump right into an example to explain:

package main

import (
	"fmt"
)

type car interface {
	start() string
}

type toyota struct {
	model string
}
type subaru struct {
	model string
}

func (t toyota) start() string {
	return t.model
}

func (s subaru) start() string {
	return s.model
}

func getModel(c car) {
	fmt.Println(c.start())
}

func main() {
	t := toyota{model: "tacoma"}
	s := subaru{model: "forester"}

	getModel(t)
	// tacoma
	getModel(s)
	// forester
}
  • I start by creating a new interface - I do this by writing the type keyword, followed by the identifier car, and lastly the underlying type struct
  • next, I declare two struct types, toyota and subaru - they both have a field named model with the type string
  • I create two methods that are both called start and have value receivers and accept their respective receiver type toyota and subaru
  • I create a function named getModel that takes a value of type car as a parameter
  • inside of the getModel function, I print out the returned value of the start method
  • in the main function I declare two variables, t and s
  • using a composite literal, t is assigned to the value of type tacoma with the field name model and respective value tacoma
  • the same process is done on the next line, the only differences being the variable is named s and the type is subaru
  • you might have noticed that both the tacoma and subaru types have a method named start
  • since start is a part of the car interface, both the tacoma and subaru types can also be of type car
  • last, we invoke the getModel function twice, first by passing in t as an argument, and then by passing s as an argument
  • the value of the field name model is returned for t and s

In Summary

There are so many ways to optimize and organize your code in Go.

The struct data type helps us compartmentalize our code by common values and allows us to aggregate values of multiple types, all under one type. How cool is that?

While struct allows us to group data creatively, interface allows us to group functionality between our struct values. Thus allowing our code to have a deeper reach throughout our codebase. Now, creating methods that can run functionality across multiple struct types is a painless exercise.

Next week I will be sharing my experience with functions in Go, see you then!