Hello everyone 👋, Today I have something interesting for you.

I was recently contributing to Porter - a CNCF sandbox project and Once I fixed the issue and was done with the pull request, I was motivated to make more contributions to the project. And, since the project was mainly written in Golang, I thought this is a good opportunity to learn Golang.

While learning Golang myself, I made some notes which are more than sufficient to get familiar with the syntax and necessary concepts of Golang. So, Let’s get started.

History of Golang

How is memory allocated in Golang?

When Go starts a program, It requests a block of memory from operating system, cuts it into small chunks and manages it itself.

The basic unit of Go memory management is the mspan, which consists of several pages, each mspan can allocate a specific size of object.

Go’s memory allocator allocates objects in three categories based on their size:

Data types in Golang

Go has a concept of types that is either explicitly declared or implicitly inferred.

It is a fast, statically typed, compiled language that feels like dynamically typed, interpreted language.

Variables

Now, Let’s talk about how we can use variables inside Golang.

Variables are declared using var keyword followed by variable_name and its type such as int, string etc.

// The syntax for declaring a variable is :
// var <variable_name> <data_type>
var str string

// After declaring we can initialise variables like this
str = "Hello World!"

Declaring + Initialising Variables

// The syntax for declaring and initialising a variable is :
// var <variable_name> <data_type> = <value>

var str string = "Hello"
var num int = 10
var b bool = true
var decimal float64 = 69.420
// Variables of same data type can be declared together like this
var f,b string = "foo","bar"

// Variables of different data type can also be declared together like this
var (
    str string = "foo"
    num int = 10
)

We can also use the walrus operator := to declare and initialise variables. We do not explicitly mention type of variable, It is implicitly assigned a type by the compiler.

str := "foo"

Constants

Constants are variables whose value is initialised once and can never be changed again. They are untyped unless explicitly given a type at the time of declaration, which allows flexibility.

Constants need to be initialised at the time of declaration as the concept of zero value doesn’t apply to constants. And, they can not be initialised using the := operator.

// const <const_name> <data_type> = value
const PI float64 = 3.14

// const <const_name> = value
const PI = 3.14

Printing Variables

Variables can be printed to console by using methods provided by fmt package such as Print,Printf,Println etc. Code to print a variable in go :

package main
import "fmt"

func main() {
  var hello string = "Hello"
  var money float64 = 69.420

  fmt.Print(str , " World")
  fmt.Println(str , " World")
  // Difference between Print and Println is that Print method doesn't add
  // newline after printing a statement while Println does.

  fmt.Printf("%s World! I have $ %f in my account.", hello, money);
  // Printf stands for Print Formatter and it takes a template string with
  // format specifiers and Object args(s).
}

Printf Format specifiers

Format specifiers are prefixed with a % symbol. There are different types of format specifiers for printing different types of variables.

VerbDescription
%vdefault format
%Ttype of the value
%dintegers
%ccharacter
%qquoted characters/string
%splain string
%ttrue or false
%ffloating numbers
%0.2ffloating numbers upto 2 decimal places

Variable Scope

Scopes are defined using Blocks which are declared using braces { }.

{
  //outer block
  {
    //inner block
  }
}

Inner block can access variables declared in Outer block but vice versa is not true. e.g:-

func main() {
  city := "Tokyo"
  {
    country := "Japan"
    fmt.Println(country)
    fmt.Println(city)
  }
  // fmt.Println(country) // this line will give error as Outer block cannot
  // access variable `country` defined in inner block
  fmt.Println(city)
}

Local vs Global Variables

Local variables are declared inside a function or a block, also inside loops or conditional statements. These are only available inside and are not accessible outside the function or block.

package main
func main() {
  name := "Zoro" // `name` is a local variable only available inside main func
}

Global variables are declared outside of a function/block and are available throughout the lifetime of a program. These variables are declared at the top of the program outside of any function or block and cannot be declared using the walrus := operator, these have to be declared in the standard way.

package main
import "fmt"

var name string = "Zoro"
// `name` is a global variable available for use anywhere in the program

func main() {
  fmt.Println(name)
}

Concept of Zero Value

When you declare a variable and do not initialise it, It is given a default value by the compiler known as zero value which is different for different data types. e.g:-

Data TypesZero values
booltrue
int0
float640.0
string""
etc.

How to take user input?

fmt package provides a function Scanf which is used to take input from user.

More on Types

Checking Types

We can check data types of variable using %T or reflect.TypeOf provided by reflect package.

Type Casting/Conversion

The process of converting one data type to another is called Type Casting or Conversion. However, It does not guarantee that the value will remain intact.

package main
import "fmt"

func main() {
    var f float64 = 69.420
    var i int = int(f)
    fmt.Println("Integer Value: ",i)
}
 go run main.go
Integer Value: 69

Here the decimal value got truncated while converting float to integer.

Type Conversion can also be done by methods provided by a package strconv

Conditionals

If-else and else if

if condition_1 { // parenthesis () around the condition is optional
    // execute when condition_1 is true
} else if condition_2 { // else should start from the same line where if block ends
    // execute when condition_2 is true
} else {
    // if none of the condition is true
}

Switch Case

The expression doesn’t need to be in parenthesis and break; statement is also not needed.

switch expression {
  case value_1:
    // execute when expression equals to value_1
  case value_2, value_3:
    // execute when expression equals to value_2 or value_3
  default:
    // execute when no matches found
}

the fallthrough keyword

It is used to force the execution flow of switch case.

switch expression {
  case value_1:
    // execute when expression equals to value_1
  case value_2:
    // execute when expression equals to value_2
    fallthrough // It forces to execute the successive(next) case block
  case value_3:
    // execute when expression equals to value_3 or value_2 since fallthrough is
    // used
  default:
    // execute when no matches found
}

Switch with conditions

We don’t give expression to switch statement and instead we can give conditions for each cases.

switch {
  case condition_1:
    // execute when condition_1 is true
  case condition_2:
    // execute when condition_2 is true
  default:
    // execute when no conditions are true
}

Looping in Go

There is only one loop in Go that is for loop.

for initialisation; condition; post {
  // statement(s)
}

First initialisation happens, then if condition is true, statements inside loop are executed and after execution is completed, post statement (mostly increment of decrement) is executed.

Arrays in Go

An Array is a collection of similar data elements stored at contiguous memory locations.

In Golang, Arrays are of fixed length, And since, we have pointers in Go, we can get the address of any element of array.

// Array Declaration
// var <array_name> [<size_of_array>] <data_type>

var num [5]int

// Array Declaration and Initialisation
// var <array_name>[<size_of_array>]<data_type> = [<size_of_array>]<data_type>{<values>}

var num [5]int = [3]int{6,9,4,2,0}

// Shorthand
// <array_name> := [<size_of_array>]<data_type>{<values>}

num := [3]int{6,9,4,2,0}

// Shorthand using ellipsis(...)
// <array_name> := [...]<data_type>{<values>}

num := [...]int{6,9,4,2,0}

Length of Array

To calculate length of an Array, we can use builtin len() function.

fruits := [4]string { "this", "is", "a", "test" }
fmt.Println(len(fruits))

Looping through Array

arr := [3]int {6,9,4,2,0}

for i := 0; i < len(arr[i]); i++ {
  fmt.Println(arr[i]);
}
arr := [3]int {6,9,4,2,0}

for index, element := range arr {
  fmt.Println(index, "->", element)
}

// If we don't need index, we can replace it with underscore `_`
for _, element := range arr {
  fmt.Println(element)
}

MultiDimensional Array

// 2D Array
arr := [3][2]int {{1, 2}, {3, 4}, {5, 6}}

fmt.Println(arr[2][1]) // This will print 6

Slices

A Slice is a continous segment of an underlying array, which offer more flexibility as they are variable typed (elements can be added or removed).

Slices have three major components

Creating a Slice

// <slice_name> := []<data_type>{<values>}
grades := []int{10, 20, 30}

Creating slice from an array from start_index till the end_index (end_index is not included).

// array[start_index : end_index]
arr := [8]int{2, 3, 4, 5, 6, 7, 8, 9}

arr[1:5] // [3, 4, 5, 6]
// array -> | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
//                ^
//                Slice Pointer
// slice -> | 3 | 4 | 5 | 6 | -> length = 4, capacity = 7

If we omit the start_index, it will be 0 by default and if we omit the end_index, it will be the length of array by default.

// array[start_index : end_index]
arr := [5]int{11, 12, 13, 14, 15}

arr[ :3] // [11, 12, 13]
arr[2: ] // [13, 14, 15]
arr[ : ] // [11, 12, 13, 14, 15]

Using make function

// slice := make([]<data_type>, length, capacity)
slice := make([]int, 5, 8)

Since Slices are references to the original array, If we mutate the slice it will aslo mutate the original array from which the slice was created.

arr := [5]int{11, 12, 13, 14, 15}

slice = arr[ :3] // [11, 12, 13]
slice[1] = 1000

// Now the slice will become, slice - [11, 1000, 13]
// And, the original array, arr - [11, 1000, 13, 14, 15]

Appending to Slice

append function is used to append a slice or values of the same type as that of the slice to the slice.

If the number of total values after appending become more than the capacity of the slice, then the slice’s capacity will be doubled.

Deleting from Slice

To delete an element from a slice, we can just create a new slice which doesn’t contain the element to be deleted.

arr := [5]int {10, 20, 30, 40, 50}

i := 2 // index of element to be deleted

slice_1 := arr[:i] // [10, 20]
slice_2 := arr[i+1:] // [40, 50]

new_slice := append(slice_1, slice_2...)
// Now new_slice consists of [10, 20, 40, 50] and the element at index 2 got
// deleted

Copy from a Slice

// func copy(dst, src []Type) int
// num := copy(dest_slice, src_slice)

src_slice := []int{10, 20, 30, 40, 50}
dest_slice := make([]int, 3)

num := copy(dest_slice, src_slice)

Maps in Go

Maps are unordered collection of key-value pairs, implemented using hash tables.

// var <map_name> map[<key_data_type>]<value_data_type>
var my_map map[string]int // creates a nil map

// <map_name> := map[<key_data_type>]<value_data_type>{<key_value_pairs>}
my_map := map[string]string{"h": "ello", "w": "orld"}

// <map_name> := make(map[<key_data_type>]<value_data_type>, <initial_capacity>)
my_map := make(map[string]int)

Access Items of Map

my_map := map[string]string{"h": "ello", "w": "orld"}

fmt.Println(my_map["h"]) // Output : ello
// value, found := map_name[key]
my_map := map[string]string{"h": "ello", "w": "orld"}

value, found := my_map["h"]
fmt.Println(found, value) // Output: true, ello

Adding/Deleting a key-value pair

my_map := map[string]string{"h": "ello", "w": "orld"}

my_map["t"] = "est" // adds a key-value pair

fmt.Println(my_map) // Output: map[h:ello w:orld t:est]

delete(my_map, "w") // deletes a key-value pair

fmt.Println(my_map) // Output: map[h:ello t:est]

Clearing/Truncating a map

Items can be removed one by one using a for loop or Reinitializing the map can also clear all items from the map.

my_map := map[string]string{"h": "ello", "w": "orld"}
my_map := make(map[string]string) // reinitializing with make truncates the map

Functions

Functions are self-contained units of code which do a certain task, and help us divide a program into organisable and reusable chunks.

// func <function_name>(<params>) <return_type> {
//   function body
// }

func addNumbers(a int, b int) int {
  sum := a + b
  return sum;
}

addNumbers(10, 20)

Return Type - A function can or can not have return value(s).

// standard return values
func operation(a int, b int) (int, int) {
  sum := a + b
  diff := a - b
  return sum, diff
}

// named return values
func operation(a int, b int) (sum int, diff int) {
  sum := a + b
  diff := a - b
  return
}

func main() {
  sum, difference := operation(20, 10)
  fmt.Println(sum, " ", difference)
}

Variadic functions

Variadic functions have variable number of parameters.

// func <function_name>(param1 type, param2 type, param3 ...type) <return type>

func sumNumbers(numbers ...int) int

func sumNumbers(str string, numbers ...int) int

The Variadic parameter numbers is a slice of all the arguments passed to the function.

func printDetails(student string, subjects ...string) string {
  fmt.Println("hi ", student, " /n Here are your subjects - ")
  for _, sub := range subjects {
    fmt.Println(sub, " ")
  }
  return sum
}

func main() {
  fmt.Println(printDetails("Rohan", "English"))
  fmt.Println(printDetails("Sohan", "Mathematics", "English", "Science"))
}

Anonymous function

Anonymous functions are functions declared without any named indentifier to refer to it.

func main() {
  x := func(l int, b int) int { // Anonymous function
    return l * b
  }
  fmt.Printf("%T , %v", x, x(20, 50))
}

// Output: func(int, int) int, 1000

Defer Statement - A defer statement delays the execution of a function until the surrounding function returns.

func printString(str string) {
  fmt.Println(str)
}
func printNumber(int num) {
  fmt.Println(num)
}

func main() {
  defer printNumber(101) // delays the function execution
  printString("GoLang")
}

// Output:
// GoLang
// 101

Pointers

A Pointer is a variable that holds the memory address of another variable. It points to the address where the memory is allocated and provides ways to find or even change the value located at that address.

Address and Dereference operator

// Declaring a Pointer
// var <pointer_name> *<data_type>
var ptr_i *int
var ptr_s *string

// Initialising a Pointer
// var <pointer_name> *<data_type> = &<variable_name>
i := 10
var ptr_i *int = &i // ptr is a pointer which holds the address of variable i

// Shorthand
s := "hello"
ptr_s = &s

We can get or modify the value of a variable using * operator on a pointer that holds the address of that variable.

s := "hello"
ps := &s

fmt.Println(s, *ps)

*ps := "world"

fmt.Println(s, *ps)

// Output:
// hello hello
// world world

Passing by value

Function is called directly passing the value of variable as argument, so modifying the variable inside the function doesn’t affect the original variable as the parameter is copied into another location of memory.

All basic data types such as int, bool, string etc are Pass by value by default.

func modify(n int) {
  n += 10
}

func main() {
  num := 15
  fmt.Println(num)
  modify(num)
  fmt.Println(num)
}

// Output:
// 15
// 15

Passing by Reference

By using pointers, we can pass the references of variables as arguments and when their value is modified the value of original variable is also modified as the memory address of the original variable is passed using pointer.

Slices are references to an underlying Array so they are Pass by reference, Maps are also Pass by reference by default.

func modify(n *int) {
  *n += 10
}

func main() {
  num := 15
  fmt.Println(num)
  modify(&num)
  fmt.Println(num)
}

// Output:
// 15
// 25

Structs

Structs are used to create custom data types by grouping various data elements together and referencing them through a single variable name.

Note: Structs are also Pass by value by default, so to modfiy values on Struct use pointer to follow Pass by reference.

Declaration and Initialisation

// Declaration
// type <struct_name> struct {
//   list of fields
// }

type Student struct {
  name string
  rollNo int
  marks []int
  grades map[string]int
}

// Initialising
// var <variable_name> <struct_name>
var s Strudent

// Initialising using new keyword
// <variable_name> := new(<struct_name>)
st := new(Strudent)

st := Student{
  name: "Rohan",
  rollNo: 10,
  marks: {80, 74, 96}
}

Accessing Fields

// <variable_name>.<field_name>

type Student struct {
  name string
  rollNo int
  marks []int
  grades map[string]int
}

var st Student

st.name = "Sohan"
fmt.Println(st.name, st.rollNo)

Equality : Two structs will be equal only if they are created from same Struct definition and they also have same values.

Method

A Method is a function that has a defined reciever (extra parameter after func keyword).

func (s Student) getMarks() float64 {
  // code
}
func (s *Student) getMarks() float64 {
  // code
}

Example:

type Circle struct {
  radius float64
  area float64
}

func (c *Circle) calcArea() {
  c.area = 3.14 * c.radius * c.radius
}

func main() {
  c := Circle{radius: 5}
  c.calcArea()
  fmt.Printf("%+v", c)
}

// Output: {radius:5 area:78.5}

Method sets

A set of methods that are available to a data type and are useful to encapsulate functionality.

type Student struct {
  name string
  grades []int
}

func (s *Student) displayName() {
  fmt.Println(s.name)
}

func (s *Student) calculatePercentage() float64 {
  sum := 0
  for _, v := range s.grades {
    sum += v
  }
  return float64(sum*100) / float64(len(s.grades)*100)
}

func main() {
  s := Student{name: "Rohan", grades: []int{90, 75, 84}}
  s.displayName()
  fmt.Printf("%.2f%%", s.calculatePercentage())
}

Interfaces

An interface is like a blueprint for a method set. It specifies a method set and is a powerful way to introduce modularity in Go. It describes all the methods of a method set by providing the function signature for each method but does not implement them.

// type <interface_name> interface {
//  method signatures
// }

type FixedDeposit interface {
  getRateOfInterest() float64
  calcReturn() float64
}

Implementing Interface

type shape interface {
  area() float64
  perimeter() float64
}

type square struct {
  side float64
}

func (s square) area() float64 {
  return s.side * s.side
}

func (s square) perimeter() float64 {
  return 4 * s.side
}

type rect struct {
  length, breadth float64
}

func (r rect) area() float64 {
  return r.length * r.breadth
}

func (r rect) perimeter() float64 {
  return 2*(r.length+r.breadth)
}

func printData(s shape) {
  fmt.Println(s)
  fmt.Println(s.area())
  fmt.Println(s.perimeter())
}

func main() {
  r := rect{length: 3, breadth: 4}
  s := square{side: 5}
  printData(r)
  printData(c)
}

Final Remarks

So, These were the important concepts of Golang, And I know this was a lot but if you made it till here, then Congrats 🥳.

If you liked this blog, found any mistakes or just want to say Hi, feel free to send an email or personal message me on X/Twitter.

Now, I will see you in the next blog. Until then keep learning and take care, Bye.