Golang for Beginners: Essential Basics Explained
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,
Subscribe to my newsletter
Read articles from Udit Namdev directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Udit Namdev
Udit Namdev
25 year old passionate engineer with 5 years of professional experience.Here to share my journey as I try to learn new technologies