Learning Golang – Zero To Hero

Rahul Kapoor
30 min readDec 23, 2022
GoLang gopher chilling with his guitar
Golang gopher image

Google Engineers and some of the great minds in the field of computer science — Rob Pike, Robert Griesemer and Ken Thompson developed Go while waiting for other programs to compile.

Check these people out and it will help you out in understanding why Go is not just another language and why it is important and why they developed it. If you don’t like reading Wiki pages check out videos on YT about them speaking about Go.

Although Go is like a Swiss Army Knife which is fast and built for concurrency and modern systems, not aligning with other languages which are adding so many features and eventually doing the same thing, and building web services at scale etc. The best thing I liked about Go is its Ease of Programming!

A lot of companies are using Go including Google, Uber, Twitch, Dropbox, Soundcloud, Dailymotion, Docker and the list goes on and on…

The goals of the Go project were to eliminate the slowness and clumsiness of software development at Google, thereby making the process more productive and scalable. The language was designed by and for people who write — and read and debug and maintain — large software systems.

When builds are slow, there is time to think. The origin myth for Go states that it was during one of those 45-minute builds that Go was conceived. It was believed to be worth trying to design a new language suitable for writing large Google programs such as web servers, with software engineering considerations that would improve the quality of life of Google programmers.

Although the discussion so far has focused on dependencies, many other issues need attention. The primary considerations for any language to succeed in this context are:

  • It must work at scale, for large programs with large numbers of dependencies, with large teams of programmers working on them.
  • It must be familiar, roughly C-like. Programmers working at Google are early in their careers and are most familiar with procedural languages, particularly from the C family. The need to get programmers productive quickly in a new language means that the language cannot be too radical.
  • It must be modern. C, C++, and to some extent Java are quite old, designed before the advent of multicore machines, networking, and web application development. There are features of the modern world that are better met by newer approaches, such as built-in concurrency.

Go at Google: Language Design in the Service of Software Engineering

Here on Enters Go.

All the topics will be theory + code examples. And to make sure things don’t get too boring I have divided related topics into separate articles and you are free to jump here and there.

Topics we will be covering:

  1. Hello World program
  2. Variables, Values and Types
  3. Scope
  4. Blank Identifier
  5. Constants
  6. Pointers
  7. Control Flow
  8. Rune (pronounced as Roon)
  9. Functions
  10. Defer keyword
  11. Data structures: array, slice, map and struct
  12. Interfaces
  13. Concurrency
  14. Channels
  15. Error Handling

Without wasting any time setting up our IDE to run Go (though you should try setting up any of the GoLand, VSCode etc IDE later for Go on your system to understand more about GoRoot, GoPath, etc configurations like done here with GoLand for mac), we can directly deep dive into it by running our example programs on Go Playground.

1. Hello World program

Basic hello world program with use of fmt and uuid packages

fmt package print examples: https://goplay.tools/snippet/rgsRbQT3r1X

another package uuid example: https://goplay.tools/snippet/erRlOLWshvw

2. Variables, values and types

shorthand declaration of variables and types inferred: https://goplay.tools/snippet/TCjYLSQ3Xlw

In the example above, it does 3 things declare, assign and initialize in shorthand form.

above example in expanded form: https://goplay.tools/snippet/tiX10S1tVQU

Note: Here we used %T which is to get the underlying type for the value and %v to get its value.

Everything in go by default is zero-valued for that type: https://goplay.tools/snippet/gN7dCJo1kYK

In the example above for int and float it is 0, for string it is an empty string, for bool it will be false.

3. Scope

Package level scope: https://goplay.tools/snippet/BKlT1UKLWIZ

Block level scope (it throws an error inside foo for the variable as it was defined in main func block and not passed down to foo func block): https://goplay.tools/snippet/8PmvcQQL6GV

Note: Ordering matters in block level scope but not in package level scope: https://goplay.tools/snippet/K5CuaNHYncC
Here we got error for x (defined with block-level scope and incorrect order) but not for y (defined anywhere at the package level, in any order)

Another example which produces an error: https://goplay.tools/snippet/nx8PTY_8jSL

4. Blank (_) identifier in Go

In Go, we can throw/drop away something if we don't want to use it by telling the compiler with the blank _ idenitifier.

Example: We can use the HTTP package and the Get func of it returns a response and error. So here we dropped the error which is returned (though error handling must be done).

package main

import (
"fmt"
"io"
"net/http"
)

func main() {
resp, _ := http.Get("https://example.com/")
data, _ := io.ReadAll(resp.Body)
defer resp.Body.Close() //defer keyword: call this just before main is exited
fmt.Println(string(data))
}

5. Constants

Example of declaring const in different ways in Go: https://goplay.tools/snippet/yQLswQqJIEx

An untyped constant has a default type, an implicit type that it transfers to a value if a type is needed where none is provided. To read more about it read this amazing blog post by Rob Pike (one of Go creators): https://go.dev/blog/constants

6. Pointers

Points to the memory address of a variable. If we have say var x int = 10

and then we need to define a pointer to the memory address of x, we can do as var y *int = &x (also known as referencing)

Here *int means type pointer to int and &x will provide us the memory address of x. To extract the value from y we can further use *y also known as de-referencing.

Example shows how pointer works in Go: https://goplay.tools/snippet/ZfopZcDXcan

Passing data across functions with and without pointers: https://goplay.tools/snippet/iwbwJOLut7q

7. Control Flow

The Go for loop is similar to—but not the same as—C's. It unifies for and while and there is no do-while. There are three forms, only one of which has semicolons.

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

example of go for loops: https://goplay.tools/snippet/eFnBssYSw4u

a basic example of nested loops: https://goplay.tools/snippet/yPkX2O9mZ57

Odd Even Utils covering both concepts of for loop and if-else conditions: https://goplay.tools/snippet/FbYwjsslQ2i

Switch in Go has no break statements after every case (though it may have break to exit a case logic early)

coffee-code-sleep example: https://goplay.tools/snippet/ze2nd7-s2B1

Type switch: A switch can also be used to discover the dynamic type of an interface variable. Such a type switch uses the syntax of a type assertion with the keyword type inside the parentheses.

Example: https://goplay.tools/snippet/EhQjnfs5ves

8. Rune

In the past, we only had one character set, and that was known as ASCII. There, we used 7 bits to represent 128 characters, including upper and lowercase English letters, digits, and a variety of punctuation and device-control characters. Due to this character limitation, the majority of the population is not able to use their custom writing systems.

To solve this problem, Unicode was invented. Unicode is a superset of ASCII that contains all the characters present in today’s world writing system. It includes accents, diacritical marks, control codes like tab and carriage return, and assigns each character a standard number called “Unicode Code Point”, or in Go language, a “Rune”.

Rune is an alias of type int32 because Go uses UTF-8 encoding.

Finding the rune of a character in go:

package main

import (
"fmt"
)

func main() {
val := 'a'
fmt.Printf("Character: %c, Value: %v, Unicode: %U and Type: %T", val, val, val, val)
}
Output:
Character: a, Value: 97, Unicode: U+0061 and Type: int32

We get the value in output as 97 (as in the Unicode Chart if you go and have a look ‘a’ character corresponds to 97) and the type is int32

9. Functions

Functions are generally the block of codes or statements in a program that gives the user the ability to reuse the same code which ultimately saves the excessive use of memory, acts as a time saver and more importantly, provides better readability of the code.

So basically, a function is a collection of statements that perform some specific task and return the result to the caller.

func function_name(Parameter-list)(Return_type){
// function body.....
}

Keywords explained:

  • func: It is a keyword in Go language, which is used to create a function.
  • function_name: It is the name of the function.
  • Parameter-list: It contains the name and the type of the function parameters.
  • Return_type: It is optional and it contain the types of the values that function returns. If you are using return_type in your function, then it is necessary to use a return statement in your function. In Go, we can have multiple return
Function structure explained in Go

Example to add 2 nums: https://goplay.tools/snippet/2O79-KztqyE

Example to concat first and last name using functions: https://goplay.tools/snippet/3YwxoidRhsJ

Note: In the example above, we used Sprint from fmt package: Sprint formats using the default formats for its operands and returns the resulting string.

In Go, we can vary the number of arguments we are passing to a function. This function can be defined with variadic params.

// Variadic function to join strings
func joinstr(elements ...string) string {
return strings.Join(elements, "-")
}

Here to joinstr() we can pass zero or more arguments (elements of type string as mentioned elements …string) and it will simply join all of them with — and return a string.

elements are a slice of type string: []string (this can be validated by placing a fmt for type and value above return)

Example to average nums using variadic params:

Here we used a for-range, where idx will index and value will be the element on that index in data

for idx, value := range data {
//logic
}

https://goplay.tools/snippet/gkWaQyCevNh

Slice can also be passed to variadic functions where pass them as data…

Example below:

package main

import "fmt"

func avgUtils(elements ...float64) float64 {
var sum float64
for _, val := range elements {
sum += val
}

return (sum / float64(len(elements)))
}

func main() {
data := []float64{1, 2, 3, 4, 5}
result := avgUtils(data...)
fmt.Println(result)
}

Another way of writing functions is by func expressions:

We write an anonymous function and assign it to a variable, then fire that variable with var_name()

package main

import (
"fmt"
)

func main() {
greet := func() {
fmt.Println("Hello World!")
}

greet()
fmt.Printf("Type of greet: %T", greet)
}

Output:

Hello World!
func()

There is another concept of Closure Functions in Go. As seen above we have anonymous functions in Go.

An anonymous function can form a closure. A closure is a special type of anonymous function that references variables declared outside of the function itself.

package main

import (
"fmt"
)

var counter int = 0

func main() {
incVal := func() int {
counter++
return counter
}

fmt.Println(incVal())
fmt.Println(incVal())
}

We get the output:
1
2

But here our data being counter is not isolated, as it is declared at package level scope anything outside our anonymous function can also update it without even calling incVal().

To solve this problem we can write it as below:

package main

import (
"fmt"
)

func counterUtils() func() int {
var counter int = 0

return func() int {
counter++
return counter
}
}

func main() {
incVal := counterUtils()

fmt.Println(incVal())
fmt.Println(incVal())
}

This will also give us output:
1
2
But with our counter being isolated and can only be updated by calling incVal()

You might be thinking that why we got an output of 1 and 2 rather than 1 and 1, as counter is being initialised to 0 on every call?

Photo by bruce mars on Unsplash

counterUtils() does 2 things here:

a.) initialises the counter variable to 0 and stores it in memory.
b.) returns a function which has access to counter and it memory address.

So when we call incVal() multiple times, we are calling the function that was returned which knows about counter and its memory address.

Callback in Go

In golang, we can also pass functions as an argument to a function just like we pass int, float64, string and other types.

In the below example, we are passing a callback func which takes an int and simply prints it.

package main

import (
"fmt"
)

func printNumbers(data []int, callbackFunc func(int)) {
for _, val := range data {
callbackFunc(val)
}
}

func main() {
data := []int{1, 2, 3, 4, 5}
printNumbers(data, func(val int) {
fmt.Println(val)
})
}

Output:
1
2
3
4
5

10. Defer keyword in Go

In the Go language, defer statements delay the execution of the function or method or an anonymous method until the nearby functions returns.

In other words, defer function or method call arguments evaluate instantly, but they don’t execute until the nearby functions returns.

Without defer:

package main

import (
"fmt"
)

func sayHi() {
fmt.Println("Hie there")
}

func sayBie() {
fmt.Println("Bie now")
}

func main() {
sayHi()
sayBie()
}

Output:
Hie there
Bie now

With defer:

package main

import (
"fmt"
)

func sayHi() {
fmt.Println("Hie there")
}

func sayBie() {
fmt.Println("Bie now")
}

func main() {
//computes instantly but delays execution of sayHi till nearby func is about to end
defer sayHi()
sayBie()
}

Output:
Bie now
Hie there

Another example to use defer on file close below:

package main

import (
"fmt"
"os"
)

func main() {
f, err := os.Open("testfile.txt")
if err != nil {
fmt.Println(err)
return
}

defer f.Close()
//...do something with file f
}

Here we called defer on f.Close() and immediately called it so that after we are done working with the file it will close it before its nearby (main func here) ends.

11. Data structures in Go (mainly discussing array, slice, map and struct)

Arrays in Go are much similar to other programming languages. In the program, sometimes we need to store a collection of data of the same type, like a list of student marks. Such type of collection is stored in a program using an Array.

An array is a fixed-length sequence that is used to store homogeneous elements in the memory. Due to their fixed length array are not much popular as Slice in Go.

Array with a memory of 5 int allocated to it in the example below:

package main

import (
"fmt"
)

func main() {
var arr [5]int

for i := 10; i < 15; i++ {
arr[i-10] = i
}

for idx := 0; idx < len(arr); idx++ {
fmt.Printf("Array has value: %v at index: %v\n", arr[idx], idx)
}
}

If we try to allocate more data to it beyond its limit, we get an index out-of-bound error.

In short, arrays in Go are numbered sequences to store elements and are not dynamically sized.

Slice is more powerful, flexible, and convenient than an array, and is a lightweight data structure. It is a variable-length sequence which stores elements of a similar type, you are not allowed to store different type of elements in the same slice. It is just like an array having an index value and length, but the size of the slice is resized they are not in fixed-size just like an array.

Internally, a slice and an array are connected with each other, a slice is a reference to an underlying array.

Declaration of a slice: We don't specify size of the slice as it is dynamic and can grow/shrink as per the requirement.

[]T
or
[]T{}
or
[]T{value1, value2, value3, ...value n}

example:
var my_slice []int
var my_slice_1 = []string{“Geeks”, “for”, “Geeks”}

Another example of a slice:

package main

import "fmt"

func main() {
var my_slice = []string{"coffee", "code", "sleep"}
fmt.Println("My Slice data:", my_slice)

//printing slice data with for-range loop
for _, val := range my_slice {
fmt.Println(val)
}
}

As we already know that the slice is the reference of the array so you can create a slice from the given array.

Syntax we use:

array_name[low:high]

Note: The default value of the lower bound is 0 and the default value of the upper bound is the total number of the elements present in the given array.

Example below:

package main

import "fmt"

func main() {
var my_arr = [5]string{"coffee", "code", "sleep", "eat", "gym"}
my_slice := my_arr[1:] //default value of low_bound is 0 and high_bound is total elements

fmt.Println("Printing slice before updation:")
for _, val := range my_slice {
fmt.Println(val)
}

my_arr[4] = "workout" //updating array element
fmt.Println("Printing slice after updation:")
for _, val := range my_slice {
fmt.Println(val)
}
}

Output:
Printing slice before updation:
code
sleep
eat
gym
Printing slice after updation:
code
sleep
eat
workout

Note: Though we updated the array’s last element from “gym” to “workout” it got reflected in the slice as well, as the slice is referencing our array.

Another way to create a slice:

You can also create a slice using the make() function which is provided by the go library. This function takes three parameters, i.e, type, length, and capacity. Here, capacity value is optional.

It assigns an underlying array of size = capacity defined and returns a slice pointing to this underlying array of size = length defined.

var my_slice = make([]T, length, capacity)

The example below explains length and capacity:

package main

import "fmt"

func main() {
var my_slice = make([]int, 2, 5)
my_slice[0] = 10
my_slice[1] = 20

//appending more elements to our slice as the underlying array has size 5
my_slice = append(my_slice, 30)

fmt.Println(my_slice)
}

Output:
10
20
30

As the underlying array is of size 5, at first our slice was provided of size 2 but we can append new elements as the underlying array can have 5 elements.

What will happen once we exceed the underlying array size of 5 as well?

Photo by Laurenz Kleinheider on Unsplash
package main

import "fmt"

func main() {
var my_slice = make([]int, 2, 5)
my_slice[0] = 1
my_slice[1] = 2

fmt.Printf("make initial size of slice: %v and capacity of slice: %v\n", len(my_slice), cap(my_slice))

for i := 3; i < 10; i++ {
my_slice = append(my_slice, i)
fmt.Printf("size of slice: %v and capacity of slice: %v\n", len(my_slice), cap(my_slice))
}

}

Output

make initial size of slice: 2 and capacity of slice: 5
size of slice: 3 and capacity of slice: 5
size of slice: 4 and capacity of slice: 5
size of slice: 5 and capacity of slice: 5
size of slice: 6 and capacity of slice: 10
size of slice: 7 and capacity of slice: 10
size of slice: 8 and capacity of slice: 10
size of slice: 9 and capacity of slice: 10

If you see in the above example, when the elements exceed the underlying array size its capacity is doubled by Go to accommodate more elements.

Appending one slice to another slice and deleting an element from a slice:

package main

import (
"fmt"
)

func main() {
var slice1 = []string{"eat", "sleep"}
var slice2 = []string{"code", "workout"}

//appending one slice to another slice
slice1 = append(slice1, slice2...)
fmt.Println(slice1)

//deleting elements from a slice
slice1 = append(slice1[0:1], slice1[2:]...)
fmt.Println(slice1)
}

Output:
[eat sleep code workout]
[eat code workout]

Here deleting is simply appending the slice with itself, leaving that particular element (here sleep) we want to delete.

2 - Dimensional Slice

The example below makes a 2-d slice: a slice where each element stores a slice of string here ([]string):

package main

import (
"fmt"
)

func main() {
//it will be a 2d slice: a slice where each element stores a []string
my_slice := make([][]string, 0)

user1 := make([]string, 3)
user1[0] = "User1"
user1[1] = "26"
user1[2] = "8.5"

my_slice = append(my_slice, user1)

user2 := make([]string, 3)
user2[0] = "User2"
user2[1] = "32"
user2[2] = "9.5"

my_slice = append(my_slice, user2)

fmt.Println(my_slice)
}

Output:
[ [User1 26 8.5] [User2 32 9.5] ]

Another example of 2-d slice:

https://goplay.tools/snippet/7ATbHIzw1NF

A slice is a reference type with 3 things: header, length and capacity.

Note: with the simple declaration

var s []int

does not allocate memory and s points to nil, while

s := make([]int, 0)

allocates memory and s points to memory to a slice with 0 elements.

Usually, the first one is more idiomatic if you don’t know the exact size of your use case.

Golang Maps: A map is a powerful, ingenious, and versatile data structure. Golang Maps is a collection of unordered pairs of key-value. It is widely used because it provides fast lookups and values that can retrieve, update or delete with the help of keys.

Declaration:

var map_name map[key_type]value_type

Example:

package main

import (
"fmt"
)

func main() {
my_map := map[int]string{
1: "Dog",
2: "Cat",
3: "Cow",
4: "Bird",
5: "Rabbit",
}

fmt.Println(my_map)
}

Output:
map[1:Dog 2:Cat 3:Cow 4:Bird 5:Rabbit]

Example of the map using make:

package main

import (
"fmt"
)

func main() {
my_map := make(map[int]string)

my_map[0] = "coffee"
my_map[1] = "code"
my_map[2] = "sleep"

//iterate over map using a for-range loop
for key, val := range my_map {
fmt.Printf("%v - %v\n", key, val)
}
}

Output:
0-coffee
1-code
2-sleep

If we place a new string to existing key, it will simply override it

Check the presence of a key in a map or not:

package main

import (
"fmt"
)

func main() {
my_map := make(map[int]string)

my_map[0] = "coffee"
my_map[1] = "code"
my_map[2] = "sleep"

//check existence of a key
val, ok := my_map[3]
if !ok {
fmt.Println("key is not present")
} else {
fmt.Println(val)
}
}

As key 3 is not present above my_map it will go inside the if and print out “key is not present” in the output.

Deleting a key-value inside a map:

package main

import (
"fmt"
)

func main() {
my_map := make(map[int]string)

my_map[0] = "coffee"
my_map[1] = "code"
my_map[2] = "sleep"

//check existence of a key
val, ok := my_map[2]
if !ok {
fmt.Println("key is not present")
} else {
fmt.Println(val)
delete(my_map, 2) //delete a key inside map
}

val1, ok1 := my_map[2]
if !ok1 {
fmt.Println("key is not present")
} else {
fmt.Println(val1)
}
}

Output:
sleep
key is not present

Maps are also of reference types. So, when we assign an existing map to a new map, both maps still refer to the same underlying data structure. So, when we update one map it will reflect in another map.

Example: https://goplay.tools/snippet/U6lifv1BLaD

Hash tables are important data structures used in programming. Go provides a built-in map type that implements a hash table.

Struct: In Go, we can create our own types as below:

package main

import (
"fmt"
)

type data int

func main() {
var age data = 26
fmt.Printf("My age is of type: %T and value: %v", age, age)
}

Output:
My age is of type: main.data and value: 26

A structure or struct in Golang is a user-defined type that allows to group/combine items of possibly different types into a single type. Any real-world entity which has some set of properties/fields can be represented as a struct. This concept is generally compared with the classes in object-oriented programming.

For Example, an address has a name, city, state, and Pincode. It makes sense to group these properties into a single structure address as shown below.

type Address struct {
name string
city string
state string
pincode int
}
or 
type Address struct {
name, city, state string
pincode int
}

Now to initialise a variable of type struct and fill in the properties:

var my_address = Address{
name: "221-B",
city: "mohali",
state: "punjab",
pincode: 160061,
}

To access a particular field of a struct we can use the dot (.) operator followed by the field we want to access. Like my_address.name or my_address.state, etc.

Struct Nesting: We use this concept in the nested structure where a structure is a field in another structure, simply by just adding the name of the structure into another structure and it behaves like the Anonymous Field to the nested structure. And the fields of that structure (other than nested structure) are part of the nested structure, such type of fields are known as Promoted fields.

type x struct{
// Fields
}

type y struct{
// Fields of y structure
x
}

Example below:

package main

import (
"fmt"
)

type Details struct {
name string
age int
gender string
}

type Student struct {
branch string
year int
Details
}

func main() {
student1 := Student{
branch: "CSE",
year: 2018,
Details: Details{
name: "User1",
age: 26,
gender: "Male",
},
}

fmt.Println(student1)
}

Output:
{CSE 2018 {User1 26 Male}}

Now, the fields of the details structure, i.e, name, age, and gender are promoted to the student structure and known as promoted fields. Now, you can directly access them with the help of the student structure

There is also a concept of Tags with structs in Go.

import (
"fmt"
)

type T1 struct {
F1 int `json:"f_1"`
}

func main() {
//nothing to add here
}

Tags add meta information used either by the current package or external ones.

Composition in Go: In the below code snippet we have created two structs: details and game. The struct details comprise generic information about games. The struct game is a composite struct, which has fields of its own and those details as well. This composition has been achieved through type embedding as a result of which the first struct becomes a reusable piece of code.

It is interesting to note that the methods which have been defined on the struct details are accessible to objects of Type game, simply because game is composed of details.

package main

import "fmt"

// We create a struct details to hold
// generic information about games
type details struct {
genre string
genreRating string
reviews string
}

// We create a struct game to hold
// more specific information about
// a particular game
type game struct {
name string
price string
// We use composition through
// embedding to add the
// fields of the details
// struct to the game struct
details
}

// this is a method defined
// on the details struct
func (d details) showDetails() {
fmt.Println("Genre:", d.genre)
fmt.Println("Genre Rating:", d.genreRating)
fmt.Println("Reviews:", d.reviews)
}

// this is a method defined
// on the game struct
// this method has access
// to showDetails() as well since
// the game struct is composed
// of the details struct
func (g game) show() {
fmt.Println("Name: ", g.name)
fmt.Println("Price:", g.price)
g.showDetails()
}

func main() {

// defining a struct
// object of Type details
action := details{"Action", "18+", "mostly positive"}

// defining a struct
// object of Type game
newGame := game{"XYZ", "$125", action}

newGame.show()
}

Output:
Name: XYZ
Price: $125
Genre: Action
Genre Rating: 18+
Reviews: mostly positive

Another example: https://goplay.tools/snippet/mrsUpP8kdCn

JSON is one of the most popular data interchange formats. It is simplistic, human-readable, and very flexible. It is an excellent choice for APIs and most data transfer.

The encoding and decoding JSON information in Go is provided by the encoding/json package. It is part of the standard library; hence you do not need to install it.

You will require to import it, though, before you can use it.

The following describes the syntax for the Marshal function as defined in the package.

func Marshal(v any) ([]byte, error)

The function takes any data type as the argument. The function returns a byte slice, and an error is encountered during the marshal process.

The following describes the syntax for the Unmarshal function as defined in the package.

func Unmarshal(data []byte, v any) error

Example:

package main

import (
"encoding/json"
"fmt"
"log"
)

type Developer struct {
Name string `json:"dev_name"`
Age int `json:"dev_age"`
Activity string `json:"dev_activity"`
}

func main() {
user := Developer{
Name: "Developer1",
Age: 26,
Activity: "Code",
}

//Marshal process
result, err := json.Marshal(user)
if err != nil {
log.Fatal(err)
} else {
fmt.Println(string(result)) //need to convert to string as Marshal returns a []byte
}

//Unmarshall process
var dev Developer
err = json.Unmarshal(result, &dev)
if err != nil {
log.Fatal(err)
} else {
fmt.Println(dev)
}
}

Output:
{“dev_name”:”Developer1",”dev_age”:26,”dev_activity”:”Code”}
{Developer1 26 Code}

JSON Encoder and Decoder process in Go:

It is similar to Marshal and Unmarshal process as seen above but to the stream of reader/writer.

package main

import (
"encoding/json"
"fmt"
"os"
"strings"
)

type Developer struct {
Name string `json:"dev_name"`
Age int `json:"dev_age"`
Activity string `json:"dev_activity"`
}

func main() {
user := Developer{
Name: "Developer1",
Age: 26,
Activity: "Code",
}

//encoding process
json.NewEncoder(os.Stdout).Encode(user)

//decoding process
var dev Developer
rdr := strings.NewReader(`{"dev_name":"Developer2","dev_age":32,"dev_activity":"Sleep"}`)
json.NewDecoder(rdr).Decode(&dev)
fmt.Println(dev)
}
func NewEncoder(w io.Writer) *Encoder

NewEncoder takes in Writer interface which wraps the write method and returns *Encoder on which we called Encode():

type Writer interface {
Write(p []byte) (n int, err error)
}

So we passed os.Stdout where it retuns *File which further has a write method to it, hence it implicitly implements the Writer interface (unlike other languages) and can be passed to NewEncoder()

func (enc *Encoder) Encode(v any) error

Encode writes the JSON encoding of v to the stream, followed by a newline character.

Similarly, it's done for Decoder.

12. Interfaces

Go language interfaces are different from other languages. In Go language, the interface is a custom type that is used to specify a set of one or more method signatures and the interface is abstract, so you are not allowed to create an instance of the interface.

Syntax:

type interface_name interface{
// Method signatures
}

How to implement interfaces?

In the Go language, it is necessary to implement all the methods declared in the interface for implementing an interface. The go language interfaces are implemented implicitly. And it does not contain any specific keyword to implement an interface just like other languages.

Example:

package main

import (
"fmt"
"math"
)

type Square struct {
side float64
}

type Circle struct {
radius float64
}

type Shape interface {
area() float64
}

func (sq Square) area() float64 {
return sq.side * sq.side
}

func (c Circle) area() float64 {
return math.Pi * c.radius * c.radius
}

func printArea(shape Shape) {
fmt.Println(shape.area())
}

func main() {
s := Square{side: 10}
c := Circle{radius: 5}
//we can pass anything that implements Shape interface
printArea(s)
printArea(c)
}

Here func has a receiver (sq Square) so it directly becomes a part of the Square struct. Also, method signature is area() float64 which is similar to the Shape interface so it implements the interface.

Now func printArea(shape Shape) we can pass anything to this func that implements the Shape interface so we passed s of type Square struct.

Example with error (implemented only 1 method of interface and not the other): https://goplay.tools/snippet/gNjTYL2SGr7

Correct use Example: https://goplay.tools/snippet/LOtoe6-Ndw7

Note: When an interface contains zero methods, such a type of interface is known as an empty interface. So, all the types implement the empty interface.

interface{}

Sort Package by implementing all the methods of the sort.Interface interface we can sort our data by using sort.Sort(data Interface).

type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}

Another way is to use sort.Slice without the need to implement above methods.

sort.Slice(x any, less func(i int, j int) bool)

Example of sorting using Interface methods and sort.Sort(): https://goplay.tools/snippet/3BFaiFepk8g

Example of sorting using sort.Slice(): https://goplay.tools/snippet/kGoQQnx_RFC

Reverse sort a []string:

package main

import (
"fmt"
"sort"
)

type People []string

func main() {
s := People{"Kohli", "Dhoni", "Sky", "Jadeja"}

//used 2nd way to sort our data
sort.Slice(s, func(i, j int) bool {
return s[i] < s[j]
})
fmt.Println(s)

var result []string
//it was sorted but in ascending order, so we simply reverse data
for i := len(s) - 1; i >= 0; i-- {
result = append(result, s[i])
}
fmt.Println(result)
}

We have method receivers of 2 types: value and pointer receiver.

For a better understanding of this kindly follow the below article:

13. Concurrency

Concurrency is an ability of a program to do multiple things at the same time.

Go has rich support for concurrency using goroutines and channels.

https://blog.knoldus.com/achieving-concurrency-in-go

A simple example without the use of concurrency to print numbers from different func foo & bar:

package main

import (
"fmt"
)

func foo() {
for i := 0; i <= 50; i++ {
fmt.Printf("Foo data: %v\n", i)
}
}

func bar() {
for i := 51; i <= 100; i++ {
fmt.Printf("Bar data: %v\n", i)
}
}

func main() {
foo()
bar()
}

It will run sequentially printing first all numbers from func foo and then from func bar.

You can consider a Goroutine like a light weighted thread. The cost of creating Goroutines is very small compared to the thread. Every program contains at least a single Goroutine and that Goroutine is known as the main Goroutine. All the Goroutines are working under the main Goroutines if the main Goroutine terminated, then all the goroutine present in the program also terminated.

A goroutine is a function that runs independently of the function that started it. Sometimes Go developers explain a goroutine as a function that runs as if it were on its own thread.

Above example can achieve concurrency with use of goroutines and waitgroup:

package main

import (
"fmt"
"sync"
)

var wg = sync.WaitGroup{} //waitgroup declaration

func foo() {
for i := 0; i <= 100; i++ {
fmt.Printf("Foo data: %v\n", i)
time.Sleep(1 * time.Millisecond)
}
wg.Done() //marked as done which decrements the counter by 1
}

func bar() {
for i := 101; i <= 200; i++ {
fmt.Printf("Bar data: %v\n", i)
time.Sleep(3 * time.Millisecond)
}
wg.Done() //marked as done which decrements the counter by 1
}

//main also run in its goroutine
func main() {
wg.Add(2) //counter set to 2 for below goroutines

go foo()
go bar()

wg.Wait() //waits till the counter goes back to 0
}

Output: https://goplay.tools/snippet/49YkMJny5mk

A few numbers from func foo are printed then from func bar then from foo and so on… till both reach the wg.Done() state.

WaitGroup is used to wait for all the goroutines launched here to finish. We launch several goroutines and increment the WaitGroup counter for each. Each time a waitgroup is marked as Done, it decrement the counter. We block until the WaitGroup counter goes back to 0; all the workers notified they’re done.

We also added wg.Wait() for the above goroutines to be marked as Done first, as the func main() also run in its own goroutine so to avoid exiting it.

Note: If something wrong happens here at goroutines you might see the below error:

fatal error: all goroutines are asleep - deadlock!

Race condition in Go:

A race condition in Go occurs when two or more goroutines have shared data and interact with it simultaneously. This is best explained with the help of an example. Suppose two functions that increment an integer by 1 are launched in two separate goroutines and act on the same variable:

package main

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

var wg sync.WaitGroup
var counter int

func printStuff(val string) {
for i := 1; i <= 25; i++ {
x := counter
x++
time.Sleep(100 * time.Millisecond)
counter = x
fmt.Printf("%v has counter: %v\n", val, counter)
}

wg.Done()
}

func main() {
wg.Add(2)
go printStuff("foo")
go printStuff("bar")
wg.Wait()
fmt.Printf("Final value of counter: %v\n", counter)
}

We can check for race condition in go using race detector and executing below command:

go run -race source_file.go

For the above code snippet it gives: Found 1 data race(s)

We can also see inconsistency in the counter data in output: https://goplay.tools/snippet/6E8H-Se31xe

To prevent race conditions we use Mutex in Go:

package main

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

var wg sync.WaitGroup
var mutex sync.Mutex //mutex declared
var counter int

func printStuff(val string) {
for i := 1; i <= 25; i++ {
mutex.Lock() //locked before access shared data: counter
x := counter
x++
time.Sleep(100 * time.Millisecond)
counter = x
fmt.Printf("%v has counter: %v\n", val, counter)
mutex.Unlock() //unlocked after processing + update is done on counter
}

wg.Done()
}

func main() {
wg.Add(2)
go printStuff("foo")
go printStuff("bar")
wg.Wait()
fmt.Printf("Final value of counter: %v\n", counter)
}

Output shows consistency in counter:

https://goplay.tools/snippet/QF31FWvnS_a

Also on checking for race condition we get: No race found

To read more on mutex

14. Channels

Channels are the pipes that connect concurrent goroutines. You can send values into channels from one goroutine and receive those values into another goroutine.

Image from https://www.geeksforgeeks.org/channel-in-golang/

In Go language, a channel is created using chan keyword and it can only transfer data of the same type, different types of data are not allowed to transport from the same channel.

Syntax:

var Channel_name chan Type

or

channel_name := make(chan Type)

Channel work with two principal operations: One is sending and Second is receiving.

Both operations are collectively known as communication. And the direction of <- operator indicates whether the data is received or send. In the channel, the send and receive operation block until another side is not ready by default. It allows goroutine to synchronize with each other without explicit locks or condition variables.

//Sending operation
Mychannel <- element

//Receiving operation
element := <-Mychannel

Example:

package main

import (
"fmt"
"time"
)

func main() {
ch := make(chan int) //declaring a channel using make

//sending data to a channel
go func() {
for i := 1; i <= 5; i++ {
ch <- i
}
}()

//receiving data from channel
go func() {
for {
fmt.Println(<-ch)
}
}()

time.Sleep(1000 * time.Millisecond)
}

You can also close a channel with the help of close() function. This is an in-built function and sets a flag which indicates that no more value will send to this channel.

Example below: You can comment one way and test the other. It can be done both with for {…} and use of ok flag or with for range loop.

package main

import "fmt"

func foo(ch chan string) {
for i := 1; i <= 3; i++ {
ch <- "coffee & code"
}
close(ch) //channel is closed now and we can no longer send data to it
}

func main() {
ch := make(chan string)
go foo(ch)

//waits in main goroutine till it receives all the data and close() marks ok flag to be false
for val := range ch {
fmt.Println(val)
}
}

Output:
It will receive and print coffee & code 3 times and then exit.

We can also pass data via the same channel from multiple places as well: https://goplay.tools/snippet/b0DktD4pReN

Can also be done without the use of WaitGroup and using another done channel of type bool instead: https://goplay.tools/snippet/4WzLFBgF8dk

As we know that a channel is a medium of communication between concurrently running goroutines so that they can send and receive data to each other. By default a channel is Bidirectional but you can create a Unidirectional Channel also. A channel that can only receive data or a channel that can only send data is a unidirectional channel.

Syntax:

//to send data to channel
ch1 := make(chan<- string)

//to receive data from channel
ch2 := make(<-chan string)

In Go language, you are allowed to convert a bidirectional channel into a unidirectional channel, or in other words, you can convert a bidirectional channel into a receive-only or send-only channel, but vice versa is not possible.

package main

import "fmt"

// changed bidirectional channel to send-only channel
func getData(ch chan<- string) {
//sending data to send-only channel
ch <- "coffee & code"
}

func main() {
ch := make(chan string)

go getData(ch)

//receive data from the channel
fmt.Println(<-ch)
}

Another example where we are passing and returning channels: https://goplay.tools/snippet/ArqQ6IgJb8y

There is a pattern called Fan In/Fan Out:

It’s a way to converge and diverge data into a single data stream from multiple streams or from one stream to multiple streams or pipelines.

Fan In is used when a single function reads from multiple inputs and proceeds until all are closed . This is made possible by multiplexing the input into a single channel.

We created 2 text files named: text1.txt and text2.txt with some data in them in the same directory as our app.go.

package main

import (
"bufio"
"fmt"
"log"
"os"
"sync"
)

func main() {
ch1 := readData("text1.txt")
ch2 := readData("text2.txt")

//receive data from multiple channels and place it on result channel - FanIn
chRes := fanIn(ch1, ch2)

//some logic with the result channel
for val := range chRes {
fmt.Println(val)
}
}

func readData(file string) chan string {
f, err := os.Open(file)
if err != nil {
log.Fatal(err)
}

ch := make(chan string) //channel declared

//returns a scanner to read from f
fileScanner := bufio.NewScanner(f)
fileScanner.Split(bufio.ScanLines) //scanning it by line-by-line token

//loop through the fileScanner based on our token split
go func() {
for fileScanner.Scan() {
val := fileScanner.Text() //returns the recent token
ch <- val //passed the token value to our channel
}
close(ch) //closed the channel when all content of file is read
f.Close() //closed the file
}()

return ch
}

func fanIn(ch1, ch2 chan string) chan string {
chRes := make(chan string)
var wg sync.WaitGroup
wg.Add(2)

//reads from 1st channel
go func() {
for val := range ch1 {
chRes <- val
}
wg.Done()
}()

//reads from 2nd channel
go func() {
for val := range ch2 {
chRes <- val
}
wg.Done()
}()

go func() {
wg.Wait() //waits till the goroutines are completed and wg marked Done
close(chRes) //close the result channel
}()

return chRes
}

Fan Out is used when multiple functions read from the same channel. The reading will stop only when the channel is closed. This characteristic is often used to distribute work amongst a group of workers to parallelize the CPU and I /O.

package main

import (
"fmt"
"sync"
)

func generator(nums ...int) <-chan int {
out := make(chan int) //channel is declared

go func() {
for _, n := range nums {
out <- n //send data to channel
}
close(out) //closed the channel
}()

return out //channel returned
}
func main() {
fmt.Println("Start Fan Out ")
c1 := generator(1, 2, 3)
c2 := generator(4, 5, 6)

var wg sync.WaitGroup
wg.Add(2)

//logic for data received from both channels till they are marked as closed
go func() {
for num := range c1 {
fmt.Println(num)
}
wg.Done()
}()

go func() {
for num := range c2 {
fmt.Println(num)
}
wg.Done()
}()

wg.Wait() //waiting for the above goroutines to marked as Done
}

To read more about this check my other article: Golang Fan-In Fan-Out Concurrency Pattern

15. Error Handling

We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. It also tends to encourage programmers to label too many ordinary errors, such as failing to open a file, as exceptional.

Go takes a different approach. For plain error handling, Go’s multi-value returns make it easy to report an error without overloading the return value.

The idiomatic way of handling errors in Go is to compare the returned error to nil. A nil value indicates that no error has occurred and a non-nil value indicates the presence of an error.

Different ways to handle errors in Go:

package main

import (
"log"
"os"
)

func main() {
_, err := os.ReadFile("myfile.txt")
if err != nil {
//simply prints the error
//fmt.Println(err)

//prints the error to the standard logger
//log.Println(err)

//prints the error to the standard logger and calls os.Exit(1) to exit the program with status code
//log.Fatalln(err)

//panic(err)
}
}

Custom Error — Idiomatic error handling:

func New(text string) error
package main

import (
"errors"
"fmt"
"log"
)

var (
ErrInvalidDivideByZero = errors.New("error occured, can't divide it by 0")
)

func main() {
res, err := divideNums(10, 0)
if err != nil {
log.Fatal(err)
} else {
fmt.Println(res)
}
}

func divideNums(num1 float64, num2 float64) (float64, error) {
if num2 == 0 {
//New returns an error that formats as the given text.
//Each call to New returns a distinct error value even if the text is identical.
return 0, ErrInvalidDivideByZero
}
result := num1 / num2
return result, nil
}

From our fmt package we can use Errorf which helps in providing context to the error as well:

func Errorf(format string, a ...any) error

In our above example we can do as below when num2 == 0:

return 0, fmt.Errorf("can't divide number: %v with %v\n", num1, num2)

Once you are done with the above topics, you can directly jump in and start building basic projects in Go to get hands-on, learn more about some of the popular packages, build API and see how multiple things connect. Freecodecamp.org YT course is highly recommended and has 11 free projects: video link here

--

--