Skip to main content

Command Palette

Search for a command to run...

From Zero to Go Hero: Learn Go Programming with Me - Part 3

Understanding Pointers, defer, switch

Updated
8 min read
From Zero to Go Hero: Learn Go Programming with Me - Part 3
D
I'm a mobile/web developer 👨‍💻 who loves to build projects and share valuable tips for programmers Follow me for Flutter, React/Next.js, and other awesome tech-related stuff 😉

Introduction

  • Welcome to Part 3 of the Learning Go with Me series. I hope you like it so far and learned from it. Because that’s the most important thing. If you have any doubts or comments please feel free to comment it. I will try to help you out.

  • In the last article, we learned lots of fundamental concepts of Go. And in this blog, I decided to just focus on the Pointers.

  • Let’s start without further ado


What is a Pointer?

  • A pointer is nothing but a variable that stores the “memory address” of another variable. What does that mean?

  • So instead of holding a direct value which we typically do like age := 24, the pointer holds the address of where that value is stored in memory.

  • You can think of pointers as post office boxes.

Mail boxes filled of leaflets and letters. Shallow DOF,Mailboxes and Lock in Rows at Entrance.

  • Each box points to a certain address/ house number. Similarly, if you have defined a variable like this:
num := 42
  • Then you can point to this variable using another variable with a pointer
ptr := &num
  • You need to first understand what ‘ * ‘ and ‘ & ’ lexical elements mean.
  1. Address Operator (&):

    • The & operator is used to get the memory address of a variable. If x is an integer variable then &x will give you pointers to x that is, a memory address where the integer x is stored.
  2. Dereferencing Operator ( * ):

    • The * operator is used to get the value stored at a memory address. If p is a pointer to an integer, then *p will give you the integer that p points to.

Example:

func main(){
    num := 42
    ptr:= &num

    fmt.Println(ptr) // 0xc000104040 - Address where num is stored
    fmt.Println(*ptr) // 43 - Value at which ptr is pointing
}
  • In this example, ptr is a pointer to num. You can get the value of num by dereferencing ptr with *ptr.

Passing Pointer to Functions

  • Pointers also come into play when you're dealing with functions.

  • If you pass a variable to a function in Go, the function gets a copy of the variable. Everything in go is a pass-by value, if the function changes the variable, it won't affect the original. But if you pass a pointer to a variable, the function can dereference the pointer to change the original variable.

  • Let’s take an example

// Without Pointer - Pass by Value
func valueSemantic(num int){
    num = 43
}

func main(){
    num := 42

    fmt.Println("Before: ", num) // 42

    valueSemantic(num)

    fmt.Println("After: ", num) // 42
}
  • This is because a copy of num is getting passed to the function valueSemantic and not the actual value. To update the value that we are passing from other functions we have to use a pointer.
// With pointer - Pass by Value
func valueSemantic(num *int){
    *num = 43
}

func main(){
    num := 42

    fmt.Println("Before: ", num) // 42

    valueSemantic(&num)

    fmt.Println("After: ", num) // 43
}
  • As you can see now it got changed. Why?

  • Because now we are not passing just the copy of the num but we are passing it an address where num is stored.

  • And in the function parameter now that we are passing an address to it we need to accept that address using a pointer. Because remember? The pointer points to another variable’s address.

  • Inside the function, we can’t just do this and change the variable:

num = 43
  • Because num is a pointer. So first we need to get the value of that pointer by dereferencing it like this *num and then assign a new value to it.
*num = 43

Pointers with Structs

  • Lastly, pointers are crucial when dealing with structs in Go. Since Go doesn’t have classes

    and objects, you can use structs to create complex types, and you can use pointers to structs to modify these structs or to avoid copying the structs around.

  • Let’s take an example:

type Person struct {
    name string
    age  int
}

func updateName(p *Person, newName string) {
    p.name = newName // Modify the struct via pointer
}

func main() {
    p := Person{name: "Alice", age: 30}

    fmt.Println("Before:", p.name) // Prints: Alice
    updateName(&p, "Bob")
    fmt.Println("After:", p.name) // Prints: Bob
}
  • We aren’t doing anything new in this. We created a struct of type Person. It has name and age fields.

  • And inside main function we are passing the address of this person p in updateName function.

  • updateName takes two parameters. Pointer to a struct person and new name.

  • But inside the function, you see we didn’t use * to deference it. Why?

  • This is because Go is automatically dereferencing that struct for us. So when you have a pointer to a struct in Go, the language provides a convenience called automatic dereferencing for accessing fields or calling methods. You don't need to explicitly dereference the pointer using * before accessing fields or methods.

For Primitive Types (e.g., int, float, etc.): You must use * to dereference the pointer and access the value.

For Structs: Go automatically dereferences pointers to structs when you access their fields or call methods.


  • In my previous blog, I forgot two concepts to discuss which are defer and switch case statements. Let’s understand them one by one

defer

  • In Go, the defer the keyword is used to ensure that a function call is executed later in a program’s execution, usually for the purposes of cleanup.

  • defer is often used where ensure and finally would be used in other languages.

  • When a function call is deferred, it's placed onto a stack. The function calls on the stack are executed when the surrounding function returns, not when the defer the statement is called.

  • They're executed in Last In, First Out (LIFO) order, so if you have multiple defer statements, the most recently deferred function will be the first one to execute when the function returns.

func main() {
    fmt.Println("Start")

    defer fmt.Println("This will print last")

    fmt.Println("End")
}

Output:

Start
End
This will print last

Multiple defer Statements

func main() {
    defer fmt.Println("First")
    defer fmt.Println("Second")
    defer fmt.Println("Third")
}

Output

Third
Second
First

// LIFO order as explained above

Real-World Example: Closing a File

func main() {
    file, err := os.Open("example.txt") // Opening a file
    if err != nil {
        fmt.Println("Error opening file:", err)
        return
    }
    defer file.Close() // Ensure the file is closed before exiting the function

    // Perform file operations here...
    fmt.Println("File opened successfully")
}
  • In real-world programming, we have many things open. We need to make sure we close those types of functions because otherwise, it will create memory leakage. Although Go has a Garbage collector that will dump those things down if not used, but it’s always a good practice to close the instance of it once the work is done.

  • And that’s where defer can be used. We use defer right after we perform operations like opening a file to make sure we don’t forget.


Switch

  • switch in Go is a powerful way to control the flow of execution by matching a value against multiple cases. It is simpler and cleaner than writing multiple if-else conditions.

How switch works

  • switch evaluates a value and executes the first matching case.

  • If no case matches, the default block if executed.

Example

func main() {
    day := "Monday"

    switch day {
    case "Monday":
        fmt.Println("Start of the workweek!")
    case "Friday":
        fmt.Println("Almost the weekend!")
    default:
        fmt.Println("Just another day.")
    }
}
  • In the above example, the switch matches day to Monday and executes the corresponding block

  • In Go we can use Switch without an Expression. So If we omit the expression, Go evaluates the first case that is true

func main() {
    x := 15

    switch {
    case x < 10:
        fmt.Println("Less than 10")
    case x < 20:
        fmt.Println("Between 10 and 20") // Prints this
    default:
        fmt.Println("20 or more")
    }
}

Fallthrough in switch

  • The fallthrough keyword forces the execution of the next case, even if the condition is not true
func main() {
    num := 2

    switch num {
    case 1:
        fmt.Println("One")
    case 2:
        fmt.Println("Two")
        fallthrough
    case 3:
        fmt.Println("Three")
    default:
        fmt.Println("Other")
    }
}

/* 
Output: 
Two 
Three
*/

Wrapping Up

  • In this article, we explored some of the fundamental concepts of Go, including pointers, defer statements, and switch statements.

  • We learned that pointers store the memory address of variables, enabling functions to modify original data without creating copies.

  • The defer statement schedules function calls to execute after the surrounding function completes.

  • The switch statement offers a good control flow based on variable values.

  • I think this is pretty much it for you to know to get started. So from the next article, we will start building out Book CRUD API. There are still a few things remaining to explain and I will be explaining on the go as it will make much more sense when we are building something.

  • I hope you have learned something from this article. If you have any feedback or queries, feel free to ask them in the comments. I’ll be happy to answer all your questions

  • See you in the next article, until then….