Learning Go - Structs

Intro

Structs in Golang is a composite data type that helps us define our own data types. You may recall having struct keyword in C & C+ where we can define the custom data type. C++ structs are bit different from C and in Go it’s completely different. Here’s how we can use struct to define our own data types:

1
2
3
4
5
6
7
package person 

// define a person data type
type Person struct {
Name string
Age uint8
}

Thinking about Go structs as classes will be wrong because we cannot define methods inside structs (we can associate methods though called as explicit functions via value or pointer receivers) and Go does not support inheritance.

Go has visibility restrictions based on how we name the struct type and their fields. For example:

1
2
3
4
5
6
package person 

type person struct {
name string
age uint8
}

In the above example, the person struct is private to the package and also the fields name, age are private to the package and cannot be accessed outside the package.

Creating Structs

We have already seen how we define structs in Go. Now let’s see how we can use them. There are several ways we can create variables of the structs definitions. Let’s explore them one by one.

Zero Value

When we simply create an uninitialized variable of the struct type, we call it a zero value struct. If we do something like: var p Person then we get a Person variable with zero value which means the fields are having the default values of the respective data type. The default values are "" for string and 0 for uint8.

Named Field Initialization

Here we declare the variable and initialize them with the field names. In the code example below, we can see that we have created a variable of type Person which is john and then we are using the field names to assign the respective values to it.

1
2
3
4
john := Person {
name: "John",
age: 21,
}

This is readable and cleanest approach that is also the preferred way to creating struct variables in Go!.

Positional Initialization

Alternatively, we can skip the field names while initializing the struct variables and use positional values. In the code example below, we can see that we are passing string value at position 0 and integer value at position 1. Go will automatically assign them to the respective fields.

1
tom := Person{"Tom", 20}

This is not an ideal way as it can create confusions about the field meanings. We should avoid this kind of variable creations of structs in Go.ß

Using new

We can also use the Go function new for creating a struct variable. It will return a pointer struct type.

1
jerry := new(Person)

This is very rare and if required a pointer type then we generally see use of

1
jerry := &Person{}

Reading & Updating

Reading struct values in Go is simple, but updating them correctly requires understanding how Go handles values and pointers. Structs are copied by default, so updates inside functions or methods require pointers to affect the original data.

Reading Structs

This is pretty simple, we can use the . (dot) syntax to access the values of the struct fields.

1
2
3
4
5
6
7
8
9
func main() {
tom := Person{
Name: "Tom",
Age: 25,
}

fmt.Println(tom.Name) // Tom
fmt.Println(tom.Age) // 25
}

Even if we have a pointer, reading values is easy.

1
2
3
4
func main() {
jerry := &Person{Name: "Jerry", Age: 20}
fmt.Println(jerry.Name) // same as (*p).Name
}

Updating Structs

There are a few different ways we can update the structs. They are:

  • Direct update
  • Updating via functions
  • Struct Methods

Direct Update

We can directly assign new values to the structs.

1
2
3
4
5
6
7
8
func main() {
p := Person{Name: "Bob", Age: 30}

p.Age = 31
p.Name = "Robert"

fmt.Println(p.Name) // Robert
}

Updating inside Functions

A common mistake happens when updating struct values inside a function. Let me explain with code example:

1
2
3
4
5
6
7
8
9
func updateAge(p Person) {
p.Age = 40
}

func main() {
p := Person{Name: "Bob", Age: 25}
updateAge(p)
fmt.Println(p.Age) // still 25
}

To update the original struct, we must pass a pointer of the struct type.

1
2
3
4
5
6
7
8
9
func updateAge(p *Person) {
p.Age = 40
}

func main() {
p := Person{Name: "Bob", Age: 25}
updateAge(&p)
fmt.Println(p.Age) // 40
}

Struct Methods

We can associate methods (behaviors) to our structs. Struct methods lets us attach behavior to data, making code easier to understand and maintain. Go is not a traditional object oriented language, but it still allows us to write clean, organized, and modular code.

In Go, methods are functions with a receiver. When a method has a struct as its receiver, it is called a struct method.

The syntax is:

1
2
3
func (receiver Type) methodName() {
// method body
}

Let’s see how we can add methods for the structs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main 

type Person struct {
Name string
Age int
}

// value receiver
func (p Person) Greet() string {
return "Hello, my name is " + p.Name
}

func (p Person) IsAdult() bool {
return p.Age >= 18
}

// pointer receiver
func (p *Person) Birthday() {
p.Age++
}

Value Receivers

When a method uses a value receiver, it works on a copy of the struct.

1
2
tom := Person{Name: "Tom", Age: 25}
fmt.Println(p.Greet())

The above code cannot modify any fields of the tom. On execution it will print Hello, my name is Tom.

Pointer Receivers

Alternatively, when we use pointer receivers allow methods to modify the original struct.

1
2
p.Birthday()
fmt.Println(p.Age) // 26

Encapsulation

Struct methods help achieve encapsulation by controlling how data is accessed or modified.

1
2
3
4
5
6
7
8
9
10
11
type BankAccount struct {
balance float64
}

func (b *BankAccount) Deposit(amount float64) {
b.balance += amount
}

func (b BankAccount) Balance() float64 {
return b.balance
}

In the above code, if you look carefully then you will notice that balance is an unexported field for the BankAccount struct.

Best Practices

Here are a few things we should keep in mind while using struct methods:

  • Keep methods small and focused
  • Use pointer receivers consistently
  • Avoid mixing value and pointer receivers unnecessarily
  • Attach methods only when they logically belong to the struct

Nested Structs

A nested struct means defining a struct inside another struct, without creating a separate named type. This is also called an inline struct. Nested structs are very useful when our data has a natural structure, like a person having an address. For example:

1
2
3
4
5
6
7
8
9
10
11
package main 

type Person struct {
Name string
Age uint8
Address struct {
City string
State string
Country string
}
}

This pattern is useful when the nested data belongs only to one struct and has no meaning outside of it. (But mostly in read world Address has a distinct meaning and should be a separate struct).

Using Nested Struct

We can see via an example how we create and use the nested structs in Go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

type Person struct {
Name string
Age uint8
Address struct {
City string
Country string
}
}

func main() {
alice := Person{
Name: "Alice",
Age: 25,
Address: struct {
City string
Country string
}{
City: "London",
Country: "UK",
},
}

fmt.Println(alice.Name)
fmt.Println(alice.Address.City)
}

Strictly nested structs are best when:

  • The inner data is only relevant to one struct
  • We don’t need to reuse the inner structure elsewhere
  • We want to keep your code local and simple

Anonymous Structs

An anonymous struct is a struct without a name, created and used directly where it is needed. Unlike regular structs, anonymous structs are not defined as reusable types. They are mainly used for short lived, local, or one-time data structures. Ideally we should keep them short and should not add nesting or embedding.

1
2
3
4
5
6
7
person := struct {
Name string
Age int
}{
Name: "Bob",
Age: 15,
}

Anonymous structs are very common in Go and can be found at places like function returns, tests and JSON responses.

Outro

Now that we have covered a good volume of Go data types and inbuilt data structures, we can move to the concepts of Interfaces in Go! Stay Tuned.