Structs in Go are used to group related data together under one type. They are similar to classes (sort of) in other languages. In this post, we will explore how to define and use structs in Go.

Defining a Struct

To define a struct, you use the type keyword followed by the name of the struct you’d like to create. You then list the fields of the struct. Here’s an example of a struct that represents a car:


type Car struct {
    Model string
    Year  int
}

Creating an Instance of a Struct

Now that we have a defined it, we can create an instance of it and then we can access its fields using . operator :

myCar := Car{}
myCar.Model = "Tesla"
myCar.Year = 2022

We can also create an instance directly and assign values to its fields:


mycar := Car{
    Model: "Tesla",
    Year:  2022,
}

Now, mycar.Model will be “Tesla” and mycar.Year will be “2022”.

Nested Structs

You can create nested structs by defining a struct within another struct.

type Person struct {
    Name string
    Age  int
    Car  Car
}

type Car struct {
    Model string
    Year  int
}

Here, the Person struct has a field Car that is of type Car. You can then create an instance of Person and access its nested Car field:


newPerson := Person{
    Name: "Alice",
    Age:  30,
    Car: Car{
        Model: "Tesla",
        Year:  2022,
    },
}
fmt.Println(newPerson.Car.Model) // Output: Tesla

You can literally define nested structs as well, for example :

type Person struct {
    Name string
    Age  int
    Car struct {
        Model string
        Year  int
    }
}

Anonymous Struct

You can also define anonymous structs, which are structs without a name. These are useful when you need a struct for a short period of time and don’t want to define a named type. Hence, they cannot be used outside the function where they are defined.


person := struct {
    Name string
    Age  int
}{
    Name: "Alice",
    Age:  30,
}

Embedded Structs

You can embed a struct within another struct to create a new type that inherits the fields and methods of the embedded struct.

type Car struct {
    Model string
    Year  int
}

type Person struct {
    Name string
    Age  int
    Car
}

Now, the Person struct has a Car field, which is of type Car. You can access the fields of the embedded Car struct directly from an instance of Person:

p := Person{
    Name: "Alice",
    Age:  30,
    Car: Car{
        Model: "Tesla",
        Year:  2022,
    },
}

We can access the fields of the embedded Car struct directly, for example:

// instead of p.Car.Model
fmt.Println(p.Model) // Output: Tesla

Empty struct

You can create empty structs, which are structs with no fields. These are mostly useful (so far, for me) to create a map with no values. If you only want to check a key exists, where values are not meaningful, use empty structs. For example:


// named empty struct
empty := struct {}{}

myMap := make(map[string]struct{})

myMap["key"] = struct{}{}

//you can check if a key exists in the map
if _, ok := myMap["key"]; ok {
    fmt.Println("Key exists")
}

Parsing JSON data with Struct

In Go, you can use structs to parse JSON data. For example, if https://pokeapi.co/api/v2/location-area returns the following JSON data :

{
    "count": 1054,
    "results": [
        {
            "name": "canalave-city-area",
            "url": "https://pokeapi.co/api/v2/location-area/1/"
        },
        {
            "name": "eterna-city-area",
            "url": "https://pokeapi.co/api/v2/location-area/2/"
        }
        ...
    ]
    ...
}

We can create a struct to represent this data:

type LocationResp struct {
    Count int `json:"count"`
    Results []struct {
        Name string `json:"name"`
        URL  string `json:"url"`
    } `json:"results"`
}

Now, in order for us to parse the JSON data into the struct, we will use json.Unmarshal() function:


resp, err := http.Get("https://pokeapi.co/api/v2/location-area")

body, err := io.ReadAll(resp.Body)

location := LocationResp{}

err  = json.Unmarshal(body, &location)
if err != nil {
    log.Fatal(err)
}

//now we can access the fields of the location struct

fmt.Println(location.Count) // Output: 1054

for _, result := range location.Results {
    fmt.Println(result.Name, result.URL)
}

One thing to be aware of is json.Unmarshal() takes a byte slice and a pointer to the struct where the data will be unmarshalled.

Struct as Function Argument

You can pass structs to functions as arguments. For example:

func printCar(c Car) {
    fmt.Printf("%s - %d",c.Model, c.Year) // Output: Tesla - 2022
}

func main() {
    myCar := Car{
        Model: "Tesla",
        Year:  2022,
    }

    printCar(myCar)
}

Structs as method receivers : Value

You can define methods on structs by using a receiver. A receiver is a parameter that is passed to a method. Here’s an example of a method that prints the details of a car:

func (c Car) printCar() {
    fmt.Printf("%s - %d",c.Model, c.Year)
}

You can then call this method on an instance of the Car struct:

myCar := Car{
    Model: "Tesla",
    Year:  2022,
}

myCar.printCar()  // Output: Tesla - 2022

Structs as Method Receivers : Pointer

The previous example is a value receiver. That means, whenever printCar() is called, it gets a copy of the Car struct. If you want to modify original the struct, you can use a pointer receiver:


func (c *Car) setModel(model string) {
    c.Model = model
}

func (c *Car) printCar() {
    fmt.Printf("%s - %d",c.Model, c.Year)
}

func main() {
    myCar := Car{
        Model: "Tesla",
        Year:  2022,
    }

    myCar.setModel("BMW")

    myCar.printCar() // Output: BMW - 2022
}

In pointer receiver, we use * before the type of the receiver. This means that the method will receive a reference to the struct, and any changes made to the struct will be reflected in the original struct.

Notice how we created an instance of the Car struct in the beginning and set the model to “Tesla”. Then we called the setModel() method on the instance and passed “BMW” as an argument. The method then changed the model of the car to “BMW”. Without the pointer receiver, the model would have remained “Tesla”.

Difference Between Value and Pointer Receivers

When you define a method on a struct, you can use either a value receiver or a pointer receiver. A value receiver receives a copy of the struct, while a pointer receiver receives a reference to the struct.

Pointer receivers are used when you want to modify the original struct.

Here’s a link to go playground with code which demonstrates the difference between value and pointer receivers.

Struct as Function Return Type

You can also return a struct from a function :


func newCar(model string, year int) Car {
    return Car{
        Model: model,
        Year:  year,
    }
}

func (c Car) printCar() {
    fmt.Printf("%s - %d",c.Model, c.Year)
}

func main() {
    myCar := newCar("Tesla", 2022)

    myCar.printCar() // Output: Tesla - 2022
}

Exported and Unexported Fields in Struct

In Go, if the first letter of a field, method or struct name is uppercase, it is exported; if lowercase, it is unexported.

// internals/foobar/foobar.go
package foobar

import "fmt"

type car struct { // unexported struct
	model string // exported
	year  int    // unexported
}

func (c car) printCar() { // unexported method
	fmt.Printf("%s - %d", c.model, c.year)
}
//main.go
package main

import (
	"mypackage/internals/foobar"
)

func main() {
	myCar := foobar.car{
		model: "Tesla",
		year:  2022,
	}

	myCar.printCar()
}

As we are trying to access the unexported fields and methods of the struct, we will get an error :

./main.go:8:18: undefined: foobar.car

This can be fixed by exporting the struct and its fields and methods by making the first letter of the struct, fields and methods uppercase.

Structs Comparison

You can compare two structs using the == operator. Two structs are considered equal if all their fields are equal. Here’s an example:

func main() {
	myCar := Car{
		model: "Tesla",
		year:  2022,
	}

	myCar2 := Car{
		model: "BMW",
		year:  2023,
	}
	fmt.Println(myCar == myCar2) // Output: false
}

I hope this post helps to explain the basics of structs in Go based on what I have learned so far. I’ll try to keep updating it as I learn more.


References