Golang for Beginners: Essential Basics Explained

·

10 min read

Hey everyone, welcome to the second part of this series, I'm done learning the basics of Golang and I'm here to explain what I've understood.

There are plenty of great sources out there for learning the basics, which you should check out for in-depth knowledge. I'm just going to cover the main points, so let's get started.

Variable declaration in Golang

So there are 4 ways of declaring variables in Golang

1. var a string;
2. var a string = "hello"
3. var a = "hello"
4. a := "hello"

Go is a static type language, meaning every variable declared needs to have a type assigned to it

You can simply declare a variable without assigning it any value as we did in 1st case, in such cases Go automatically initializes it with the zero value of the variable’s type.

We can initalize a variable with a value when decalaring it as we did in 2nd case

If a variable has an initial value, Go will automatically be able to infer the type of that variable using that initial value this is done in 3rd case. Hence if a variable has an initial value, the type in the variable declaration can be removed.

Go also provides another concise way to declare variables. This is known as short hand declaration and it uses := operator, which is used here in 4th case. a := initialvalue is the short hand syntax to declare a variable.

Short hand syntax is the one is which is used most commonly, however it can only be used inside a function, if we want to declare a global variable outside of any function, we'll have to use either of other 3 syntax

Comma ok syntax

Coming from JS background it's a little weird that there's no try catch block in Go.So how do we actually handle errors in golang? Well, here comes the comma ok syntax..
It's concept is very simple TBH, basically every function needs to return error as a return value, if there's a possibility of it, in case if there's no error it'll simply return null

If error has some value, we can handle it using condition, following is an example

func simpleFunction() (val string,err error){}

func main() {
    str,ok := simpleFunction()
    if ok!=nil {
        log.Fatal(ok)
     }

   // in case we want to ignore the error part
    str, _ :=simpleFunction()

Underscore('_') is commonly used in case we want to ignore one of the values which is coming from a function, most common example of underscore being used can be found in for loops in golang

Pointers

Pointers in Go programming allow us to work directly with memory addresses. For example, we can access and modify the values of variables in memory using pointers.

// Program to assign memory address to pointer

package main
import "fmt"

func main() {

  var name = "John Doe"
  var ptr *string

  // assign the memory address of name to the pointer
  ptr = &name

  fmt.Println("Value of pointer is", ptr)
  fmt.Println("Address of the variable", &name)

But what's the use of it? why do we even need a pointer?
Well if you come from C or C++ background, you'll already be familiar with the concept of pointers, and how can they be used
In Go, when we need to pass a variable to a function, it can be done using either of two ways, either by value or by reference.
How to we decide tho, when we need to pass by reference and when we need to pass by value.

If the argument which is getting passed gets any of it's value modified, we need to pass by reference, that's the thumb rule

Array vs Slice

An array is a collection of elements that belong to the same type. Following are 3 different ways of declaring an array.

package main

import (
    "fmt"
)


func main() {
    var a [3]int //int array with length 3
    b := [3]int{12, 78, 50} // short hand declaration to create array
    c := [...]int{12, 78, 50} // ... makes the compiler determine the length
    fmt.Println(a,b,c)
}

The size of the array is a part of the type.Because of this, arrays cannot be resized.

Another thing to note about arrays is they are value types and not reference types. This means that when they are assigned to a new variable, a copy of the original array is assigned to the new variable. If changes are made to the new variable, it will not be reflected in the original array.

Since arrays are of fixed length this creates a problem when we require a flexible array, or when we don't actually know the size of array we want. This issue can be solved using slice
A slice is a convenient, flexible and powerful wrapper on top of an array. Slices do not own any data on their own. They are just references to existing arrays.

package main

import (
    "fmt"
)

func main() {
    a := [5]int{76, 77, 78, 79, 80}
    var b []int = a[1:4] //creates a slice from a[1] to a[3]
    c := []int{6, 7, 8} //creates an array and returns a slice reference
    fmt.Println(b,c)
}

The length of the slice is the number of elements in the slice. The capacity of the slice is the number of elements in the underlying array starting from the index from which the slice is created. We can append in an slice until it's capacity is reached

Structs

A struct is a user-defined type that represents a collection of fields. It can be used in places where it makes sense to group the data into a single unit rather than having each of them as separate values.

If you're coming from python or any other OOPs language, you can think of structs as defining a class. Let's look at different ways of creating a struct variable

package main

import (
    "fmt"
)

type Student struct {
    firstName string
    lastName  string
    age       int
    class    int
}

func main() {

    //creating struct specifying field names
    emp1 := Student{
        firstName: "Sam",
        age:       12,
        class:    8,
        lastName:  "Anderson",
    }

    //creating struct without specifying field names
    emp2 := Employee{"Thomas", "Paul", 10, 6}

    //creating anonymous struct
    emp3 := struct {
            firstName string
            lastName  string
            age       int
            class    int
        }{
            firstName: "Andreah",
            lastName:  "Nikola",
            age:       13,
            class:    9,
        }

    fmt.Println("Employee 1", emp1)
    fmt.Println("Employee 2", emp2)
    fmt.Println("Employee 3", emp3)

}

Func vs Method

A function is a block of code that performs a specific task. A function takes an input, performs some calculations on the input, and generates an output.
A method is just a function with a special receiver type between the func keyword and the method name. The receiver can either be a struct type or non-struct type.


//Declaring a function
func functionname(parametername type) returntype {
 //function body
}

//Declaring a method
func (t Type) methodName(parameter list) {
}

Concurrency and Go routine.

If you're coming from NodeJS background, I'm assuming you've already heard about concurrency before. Concurrency is the capability to deal with lots of things at once.
It's different from parallelism. Parallelism is doing lots of things at the same time. It might sound similar to concurrency but it’s actually different. Let me explain the difference with a superhero reference.
We're all familiar with The Incredibles(Yes the pixar superhero family). Suppose there's a fun comptetion of who can cook 12 pancakes faster between Jack-Jack and Dash.
As soon as the competition started, Jack-Jack started duplicating himself, he had 12 clones of himself, each cooking a pancake, on the other hand Dash started cooking 12 pancakes in super speed, he was flipping all the pancakes one by one in superspeed.
Here what Jack-Jack is doing represents parallelism and Dash represents concurrency

Goroutines are lightweight threads that run functions or methods concurrently with other functions or methods, making it common for Go applications to have thousands of Goroutines running at the same time due to their low creation cost.

package main

import (
    "fmt"
    "time"
)

func printNumbers() {
    for i := 1; i <= 5; i++ {
        fmt.Println(i)
        time.Sleep(100 * time.Millisecond)
    }
}

func printLetters() {
    for c := 'a'; c <= 'e'; c++ {
        fmt.Printf("%c\n", c)
        time.Sleep(150 * time.Millisecond)
    }
}

func main() {
    go printNumbers()
    go printLetters()

    // Wait to ensure goroutines complete
    time.Sleep(1 * time.Second)
}

Wait Group

With concurrency there's always one issue, and that is how to make code synchronus. As you saw in above example we had to make our system sleep for 1 second, just so our go routines can finish their execution. But there would certainly be a better way to handle this problem rather than making system go to sleep.

Here comes WAITGROUP.Waitgroup allows you to block a specific code block to allow a set of goroutines to complete execution.
Waitgroup's working is pretty simple, it's basically a counter. Program can't exit unless waitgroup's counter hits 0.
There's 3 functions of waitgroup which are used to achieve this-

  • Add - As we know Waitgroup acts as a counter holding the number of functions or go routines to wait for. When the counter becomes 0 the Waitgroup releases the goroutines.Add is used to increment the counter by the argument that's passed to it

  • Wait - The wait method blocks the execution of the application until the Waitgroup counter becomes 0.

  • Done - Decreases the Waitgroup counter by a value of 1

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()

    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    const numWorkers = 3
    for i := 1; i <= numWorkers; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers done")
}

Defer

Defer statement is used to execute a function call just before the surrounding function where the defer statement is present returns.
Think of it as telling your program in advance what needs to be done just before the program is about to end.

Defer is used most commonly to close connections or to run a function which needs to be run at the very end, it's most commonly used with wait groups

package main

import (
    "fmt"
    "sync"
)

type sqr struct {
    length int
}

func (r sqr) area(wg *sync.WaitGroup) {
    defer wg.Done()
    if r.length < 0 {
        fmt.Printf("rect %v's length should be greater than zero\n", r)
        return
    }
    area := r.length * r.length
    fmt.Printf("rect %v's area %d\n", r, area)
}

func main() {
    var wg sync.WaitGroup
    r1 := sqr{-67, 89}
    r2 := sqr{5, -67}
    r3 := sqr{8, 9}
    sqrs := []sqr{r1, r2, r3}
    for _, v := range sqrs {
        wg.Add(1)
        go v.area(&wg)
    }
    wg.Wait()
    fmt.Println("All go routines finished executing")
}

When a function has multiple defer calls, they are pushed to a stack and executed in Last In First Out (LIFO) order. To test this out try to reverse a string, all you need to do is to loop through the string and call defer on each letter.

Channels

Channels can be considered as conduits through which Goroutines communicate. Similar to how water flows from one end of a pipe to the other, data can be transmitted from one end and received at the other using channels.

Each channel is associated with a specific type, which dictates the kind of data the channel can carry. Only data of this type can be sent through the channel.chan T represents a channel of typeT.

Sends and receives to a channel are blocking by default. What does this mean? When data is sent to a channel, the control is blocked in the send statement until some other Goroutine reads from that channel. Similarly, when data is read from a channel, the read is blocked until some Goroutine writes data to that channel.

This property of channels is what helps Goroutines communicate effectively without the use of explicit locks or conditional variables that are quite common in other programming languages.

package main

import (
    "fmt"
)

func main() {
    // Create a new channel
    message := make(chan string)

    // Goroutine to send a value into the channel
    go func() {
        message <- "Hello, Channels!"
    }()

    // Receive the value from the channel
    msg := <-message
    fmt.Println(msg)
}

That' all for this time folks, I'll start building projects next. Will update here when I'm done making some,