One of the most fundamental things you can learn in a programming language is how to create data structures to store similar data around a similar concept.

In Go, those are called structs. Custom structs let you better control the data being passed around your code. In this article, I'll show you why you'd want to use structs, how to create them, and how to extend them with methods.

Let’s assume you have a function that takes 10 parameters that describe a car with various attributes:

package main

import (
	"fmt"
)

func DescribeCar(make, model string, year int, color string, mileage float64, isElectric bool, engineType string, horsepower int, features []string, price float64) {
    // Print out the car details
    fmt.Println("Car Make:", car.Make)
    fmt.Println("Car Model:", car.Model)
    fmt.Println("Car Year:", car.Year)
    fmt.Println("Car Color:", car.Color)
    fmt.Println("Car Mileage:", car.Mileage)
    fmt.Println("Is the Car Electric:", car.IsElectric)
    fmt.Println("Car Engine Type:", car.EngineType)
    fmt.Println("Car Horsepower:", car.Horsepower)
    fmt.Println("Car Features:", car.Features)
    fmt.Println("Car Price:", car.Price)
}

func main() {
    DescribeCar("Toyota", "Camry", 2022, "Red", 15000.5, false, "V6", 268, []string{"Leather Seats", "Sunroof", "Navigation"}, 25000.00)
}

After two or three parameters in a function, the function definition starts to look pretty nasty. This can easily be cleaned up with a struct that contains all of the attributes of a car.

How to create a custom struct in Go

Creating a struct is quite simple. Using the example from above, we can create a Car struct like so:

type Car struct {
    Make        string
    Model       string
    Year        int
    Color       string
    Mileage     float64
    IsElectric  bool
    EngineType  string
    Horsepower  int
    Features    []string
    Price       float64
}

Now instead of passing in 10 parameters to a single func, we can create a new Car variable, set the values of the fields on that variable, and pass that in instead:

func DescribeCar(car Car) {
    // Print out the car details
    fmt.Println("Car Make:", car.Make)
    fmt.Println("Car Model:", car.Model)
    fmt.Println("Car Year:", car.Year)
    fmt.Println("Car Color:", car.Color)
    fmt.Println("Car Mileage:", car.Mileage)
    fmt.Println("Is the Car Electric:", car.IsElectric)
    fmt.Println("Car Engine Type:", car.EngineType)
    fmt.Println("Car Horsepower:", car.Horsepower)
    fmt.Println("Car Features:", car.Features)
    fmt.Println("Car Price:", car.Price)
}

func main() {
	car := Car{
		Make: "Toyota",
		Model: "Camry",
		Year: 2022,
		Color: "Red",
		Mileage: 15000.5,
		IsElectric: false,
		EngineType: "V6",
		Horsepower: 268,
		Features: []string{"Leather Seats", "Sunroof", "Navigation"},
		Price: 25000.00,
	}

	DescribeCar(car)
}

Extending a struct with a custom method

If the DescribeCar method is something we want to be able to do on all cars, you can also extend the struct with a method. By adding (car *Car) we’re telling Go that we want to be able to call this func on an existing object like so:

func (car *Car) DescribeCar() {
	// Print out the car details
	fmt.Println("Car Make:", car.Make)
	fmt.Println("Car Model:", car.Model)
	fmt.Println("Car Year:", car.Year)
	fmt.Println("Car Color:", car.Color)
	fmt.Println("Car Mileage:", car.Mileage)
	fmt.Println("Is the Car Electric:", car.IsElectric)
	fmt.Println("Car Engine Type:", car.EngineType)
	fmt.Println("Car Horsepower:", car.Horsepower)
	fmt.Println("Car Features:", car.Features)
	fmt.Println("Car Price:", car.Price)
}

func main() {
	car := Car{
		Make:       "Toyota",
		Model:      "Camry",
		Year:       2022,
		Color:      "Red",
		Mileage:    15000.5,
		IsElectric: false,
		EngineType: "V6",
		Horsepower: 268,
		Features:   []string{"Leather Seats", "Sunroof", "Navigation"},
		Price:      25000.00,
	}
	car.DescribeCar()
}

Using pointer for optional values

If you try and use a struct without specifying a value for one of the fields, Go will still create it, however it will use the default value. For instance, if I create a car without specifying a year, it will default the year to 0. This can especially be problematic when converting the object to JSON and expect values to be null.

To get around this, you can actually use a pointer as the field type like so:

type Car struct {
	Make       string
	Model      string
	Year       int
	Color      string
	Mileage    *float64 // Since this is a pointer, its default value is 'nil'
	IsElectric bool
	EngineType string
	Horsepower int
	Features   []string
	Price      float64
}

Now we can update the DescribeCar() method to optionally log out the value of Mileage. Notice how I’m also dereferencing the value to get the actual number instead of the memory address of the pointer:

func (car *Car) DescribeCar() {
	// Print out the car details
	fmt.Println("Car Make:", car.Make)
	fmt.Println("Car Model:", car.Model)
	fmt.Println("Car Year:", car.Year)
	fmt.Println("Car Color:", car.Color)
	if car.Mileage != nil {
		fmt.Println("Car Mileage:", *car.Mileage)
	}
	fmt.Println("Is the Car Electric:", car.IsElectric)
	fmt.Println("Car Engine Type:", car.EngineType)
	fmt.Println("Car Horsepower:", car.Horsepower)
	fmt.Println("Car Features:", car.Features)
	fmt.Println("Car Price:", car.Price)
}