Golang Structs

Golang Structs

Hey there! Let's learn structs in Golang.

Struct

Struct (aka structure) is used to create custom data types. In Golang we have primitive data types like int, float, complex, boolean, etc, and composite data types, such as arrays, maps, etc. But, what if we want a combination of int and float data types together as a single data type? You guessed it; we use struct.

We use Struct to create a combination of different data types(essentially a combination of fields). Its syntax is as follows

type structName struct {

field1 data type

field2 data type

....

}

In the line type structName struct, the words type and struct are keywords, and structName is the name that we'd give to our custom struct. Followed by fields and their data types.

package main 

type customStruct struct {
    num int 
    balance float32
}

The above struct customStruct is a custom data type holding int and float32 data types. So far so good, but how do we use that struct?

Initialization of struct

So far we have a custom struct defined; we know only after we initialize we can access it because that's when memory allocation is done. To initialize a struct we can do the following ways:

  • Struct literal

  • new() function

  • general

Struct literal

If we provide values right away when we assign struct to some variable

package main 
import "fmt"
type customStruct struct {
    num int 
    balance float32
}

func main() {
    obj := customStruct{12, 1300.25}
    fmt.Println(obj)      // prints {12 1300.25}
}
  • The variable obj is known as an object/instance of the struct customStruct

  • We can even use field names while specifying arguments in the declaration as shown below, which helps when we don't want to follow the order when providing values. This is known as Name syntax.

package main 
import "fmt"
type customStruct struct {
    num int 
    balance float32
}

func main() {
                        // order doesn't matter
    obj := customStruct{balance:1300.25, num:12}
    fmt.Println(obj)      // prints {12 13.25}
}

New() function

We could also use new() build-in function to initialize a stuct.

package main 
import "fmt"

type employee struct {
    name string
    age int 
    salary float32
}

func main() {
    obj := new(employee)

    fmt.Println(obj)      // prints &{ 0 0}
}

As you can see new() allocates memory and returns us the reference to that object(we'll cover reference vs value).

Of course, when a data type is initialized, default values will be created. That's why &{ 0 0} in output (default values for string is "", and 0 for int and float)

To assign values to fields, we use dot notation as shown below.

package main 
import "fmt"

type employee struct {
    name string
    age int 
    salary float32
}

func main() {
    obj := new(employee)
    // objectName.fieldName = value
    obj.name = "Satvik"
    obj.age = 25
    obj.salary = 75000.00

    fmt.Println(obj)      // prints &{Satvik 25 75000}
}

General declaration

package main 
import "fmt"

type employee struct {
    name string
    age int 
    salary float32
}

func main() {
    var obj employee
    obj.name = "Satvik"
    obj.age = 25
    obj.salary = 75000.00

    fmt.Println(obj)      // prints {Satvik 25 75000}
}

Accessing struct fields

Just like we assigned values to object fields, we use the same syntax to access them.

package main 
import "fmt"

type employee struct {
    name string
    age int 
    salary float32
}

func main() {
    obj := employee{
        name: "Satvik",
        age: 25,
        salary: 75000.00,
    }

    fmt.Println(obj.name)    // prints Satvik
    fmt.Println(obj.age)     // prints 25 
    fmt.Println(obj.salary)  // prints 75000.50
}
  • if you've observed, I've provided field/key names(ex: name: "Satvik") while declaring the object, it's best practice to use key names while using literal declaration for creating an object of a struct.

Update struct fields

Updating struct fields is just as same as assigning values to stuct fields

package main 
import "fmt"

type employee struct {
    name string
    age int 
    salary float32
}

func main() {
    obj := employee{
        name: "Satvik",
        age: 25,
        salary: 75000.00,
    }
    obj.name = "Mahi"
    fmt.Println(obj.name)    // prints Mahi
    fmt.Println(obj.age)     // prints 25 
    fmt.Println(obj.salary)  // prints 75000
}

Pointer vs Value struct instances

Let's first see what a pointer struct looks like...

package main 
import "fmt"

type employee struct {
    name string
    age int 
    salary float32
}

func main() {
    obj := &employee{
        name: "Satvik",
        age: 25,
        salary: 75000.00,
    }
    fmt.Println(obj)  // prints &{Satvik 25 75000}
}

As shown above, before struct literal I've used ampersand (&) symbol, that makes the struct object obj a pointer, meaning the variable obj now stores the address of the struct rather than the struct value.

If you can recall, even when we'd used new() to initialize struct the output did have &, that says, even new() is returning us a pointer.

Wonder why & is being printed in the output? It's because Go is dereferencing the obj pointer for us, to get the struct values being referenced by that address. (If not, we'd need to do that by ourselves by *obj)

Observe the following.

package main

import (
    "fmt"
    "reflect"
)

type employee struct {
    name   string
    age    int
    salary float32
}

func main() {
    obj1 := employee{
        name:   "Satvik",
        age:    25,
        salary: 75000.00,
    }
    obj2 := &employee{
        name:   "Satvik",
        age:    25,
        salary: 75000.00,
    }
    fmt.Println(reflect.ValueOf(obj1).Kind()) // prints struct
    fmt.Println(reflect.ValueOf(obj2).Kind()) // prints ptr
}

I've declared two objects for the employee struct with different syntaxes, obj1 and obj2 where the latter use pointer-based. If you have observed the output of the two prints statements, obj1 kind is struct whereas obj2 kind is ptr, which states that obj2 is holding an address. (we use reflect package to determine the data types and kind of variables)

Why bother with pointer vs value struct

In Golang everything is pass by value. We can pass by value, a copy of an object, or pass by value, an address of an object.

The point here is, when to use value struct and when to go with pointer struct; it depends. Does it matter if we just use value struct rather than pointer struct? Yes. Why? It's because of Go's pass by value concept, using direct value struct may cause unintended behaviour in code.

Check out this in-detail blog https://www.ardanlabs.com/blog/2013/09/iterating-over-slices-in-go.html by William Kennedy to understand the caveats of using value struct.

Copying structs

We can copy struct of one instance both by value and pointer reference

package main

import (
    "fmt"
    "reflect"
)

type employee struct {
    name   string
    age    int
    salary float32
}

func main() {
    obj := employee{
        name:   "Satvik",
        age:    25,
        salary: 75000.00,
    }
    objCopy1 := obj            // coping uisng value of obj
    objCopy1.name = "Steve"
    fmt.Println(obj.name)      // Satvik
    fmt.Println(objCopy1.name) // Steve 

    objCopy2 := &obj           // copy using reference of obj
    objCopy2.name = "Emma"
    fmt.Println(obj.name)      // Emma
    fmt.Println(objCopy2.name) // Emma
}

As you can see the difference, one that copied by value objCopy1 := obj didn't have any effect on the original instance when modified the copied instance objCopy1, but the one copied with using the address of obj affected original instance when altered copied instance objCopy2.

Nested Structs

We can nest a struct inside another struct to create more complex yet more readable code.

package main

import (
    "fmt"
    "reflect"
)

type address struct {
    location string
    country  string
}

type employee struct {
    name    string
    age     int
    salary  float32
    address address
}

func main() {
    obj1 := employee{
        name:    "Satvik",
        age:     25,
        salary:  75000.00,
        address: address{location: "Goa", country: "India"},
    }
    fmt.Println(obj1.address.location) // prints Goa
}

In the above code, we have nested address struct in employee struct thus obj1 holds all the data needed of an employee.

Just like we create a struct literal, we even assign values to nested struct the same way. And accessing and updating values is with dot notation.

Methods to struct

A method is basically a function but bound to a specific struct. If you know OOP paradigm, this is just as same as method of class, where class here is a struct. I mean Go doesn't support OOP paradigm, but this struct and methods of struct kinda resemble the same.

Let's create a printInfo method to employee struct

package main

import (
    "fmt"
    "reflect"
)

type employee struct {
    name   string
    age    int
    salary float32
}

func (e employee) printInfo() {
    fmt.Println("Name is", e.name)
    fmt.Println("Age is", e.age)
    fmt.Println("Salary is", e.salary)
}

func main() {
    obj := employee{
        name:   "Satvik",
        age:    25,
        salary: 75000.00,
    }

    obj.printInfo() 
}

// output
// Name is Satvik
// Age is 25
// Salary is 75000

If you've observed the method printInfo() syntax is almost same as a general function, but with (e employee) prefixed to printInfo(). That prefix is what differentiates the general function from a method, and also indicates that this method is bound to type employee struct.

In func (e employee) printInfo() { } , the e is random, we can choose any variable as long the as the type employee is mentioned correctly. That's basically just like an argument we pass to a function.

When we call the method printInfo() using the synatx obj.printInfo(), the obj is passed as pass by value to that method, and that's how we can access the name, age, salary fields using the dot notation by calling e.name, e.age, etc.

Pointer struct to methods

If we want to make modifications to struct fields, we'd pass the struct as pass by address, rather than pass by value.

package main

import (
    "fmt"
    "reflect"
)

type employee struct {
    name   string
    age    int
    salary float32
}

func (e *employee) changeName() {
    e.name = "Changed"
}

func main() {
    obj := employee{
        name:   "Satvik",
        age:    25,
        salary: 75000.00,
    }

    obj.changeName() 
    fmt.Println("Name", obj.name)  // Name Changed
}

Observe the syntax func (e *employee) changeName(){ } , this time the type of e is a pointer struct. So, the obj is passed as passed by address to changeName() method, hence the modifications persist after the method call.

That's it for structs. Hope you learned something new. Peace out!