Golang refresher

Dilip Kumar
42 min readJan 6, 2025

--

Go, also known as Golang, was invented by Robert Griesemer, Rob Pike, and Ken Thompson at Google. The development of Go began in 2007, and the language was officially announced to the public in November 2009.

Go was created to address the shortcomings of existing languages and meet the needs of modern software development. Its focus on simplicity, efficiency, concurrency, and scalability has made it a popular choice for building everything from small utilities to large-scale distributed systems. By solving real-world problems faced by developers, Go has become a key player in the programming language landscape.

Golang vs C++

Basic Syntax

Package Declaration

Every Go file belongs to a package. Packages help organize code and avoid naming conflicts.

// In a file named `main.go`
package main
// In a file named `greetings.go` (inside a `mypackage` directory)
package mypackage

Import Statements

To use code from other packages (standard library or third-party). Following is syntax

import "fmt" // Single import

import (
"fmt"
"math"
"github.com/someuser/somepackage"
) // Multiple imports

Following are few example.

package main

import (
"fmt"
"time"
)

func main() {
fmt.Println("The time is", time.Now())
}

Alias for Import Statements

We can use alias for better code readability and avoid same name collision.

Without alias

"k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"k8s.io/apimachinery/pkg/apis/meta/v1/client"

////
objectMeta := v1.ObjectMeta{
Name: dnsRecodSetName,
Namespace: projectID,
}
client1 := client.Client // This will throw error collision error
client2 := client.Client // This will throw error collision error

With alias

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrclient "sigs.k8s.io/controller-runtime/pkg/client"
apiclient "k8s.io/apimachinery/pkg/apis/meta/v1/client"

////
objectMeta := metav1.ObjectMeta{
Name: dnsRecodSetName,
Namespace: projectID,
}
client1 := ctrclient.Client
client2 := apiclient.Client

Variables

Following are few example to declare variables.

var age int = 30         // Declare with type and initial value
var name string = "Alice" // Type is inferred
var isAdult bool // Declared without an initial value, gets the zero value (false for bool)

// Short variable declaration (only inside functions)
city := "New York"

// Multiple declarations
var x, y int = 10, 20

Zero Values Variables declared without a value get a “zero value” based on their type:

  • 0 for numeric types
  • "" (empty string) for strings
  • false for booleans
  • nil for pointers, functions, interfaces, slices, channels, and maps
package main

import "fmt"

func main() {
var message string = "Hello, Go!"
count := 10
price := 99.99
isValid := true

fmt.Println(message, count, price, isValid)
}

Constants

To define values that cannot be changed.

const pi float64 = 3.14159
const statusOK = 200
const ( // Grouped constants
StatusOK = 200
StatusNotFound = 404
)
package main

import "fmt"

const daysInWeek = 7

func main() {
fmt.Println("There are", daysInWeek, "days in a week.")
}

Data Types

Common Types:

  • int: Integer (e.g., -3, 0, 10) - various sizes: int8, int16, int32, int64
  • uint: Unsigned integer (e.g., 0, 5, 100) - various sizes: uint8, uint16, uint32, uint64, uintptr
  • float32, float64: Floating-point numbers (e.g., 3.14, -2.5)
  • string: Text (e.g., "Hello", "Go")
  • bool: Boolean (true or false)
  • complex64, complex128: Complex numbers (less common)
  • byte: Alias for uint8 (often used for raw data)
  • rune: Alias for int32 (represents a Unicode code point)
package main

import "fmt"

func main() {
var age int = 30
var price float64 = 99.99
var name string = "Alice"
var isAdult bool = true
var initial rune = 'J'

fmt.Println(age, price, name, isAdult, initial)
}

Operators

  • Arithmetic: +, -, *, /, % (modulo)
  • Comparison: == (equal to), != (not equal to), > (greater than), < (less than), >= (greater than or equal to), <= (less than or equal to)
  • Logical: && (AND), || (OR), ! (NOT)
  • Bitwise: & (AND), | (OR), ^ (XOR), &^ (AND NOT), << (left shift), >> (right shift)
  • Assignment: =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
package main

import "fmt"

func main() {
a := 10
b := 3

sum := a + b
difference := a - b
product := a * b
quotient := a / b
remainder := a % b

fmt.Println("Sum:", sum) // 13
fmt.Println("Difference:", difference) // 7
fmt.Println("Product:", product) // 30
fmt.Println("Quotient:", quotient) // 3
fmt.Println("Remainder:", remainder) // 1

isEqual := (a == b)
isGreater := (a > b)

fmt.Println("Is equal:", isEqual) // false
fmt.Println("Is greater:", isGreater) // true
}

Formatting in golang

in Go, it is a standard practice to add an extra line break between the package declaration and the import statement, as well as between other top-level declarations (e.g., import, const, var, func, etc.). This is part of Go's official formatting conventions and is enforced by the gofmt tool, which is the standard tool for formatting Go code.

Here’s an example of a properly formatted Go file:

package main

import "fmt"

const greeting = "Hello, World!"

func main() {
fmt.Println(greeting)
}
  1. Line break after package: There is a blank line between package main and import "fmt".
  2. Line break after import: If there are other top-level declarations (e.g., const, var, func), they are separated by a blank line.

What Happens If You Don’t Add the Line Break?

If you omit the extra line break, gofmt (or go fmt) will automatically add it for you. For example, if you write:

package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}

Running gofmt will reformat it to:

package main

import "fmt"

func main() {
fmt.Println("Hello, World!")
}

How to Use gofmt:

gofmt -w filename.go
gofmt -w .
gofmt -l .

Language type

Go (Golang) is not a loosely typed language. It is a statically typed and strongly typed language. Let me explain what this means and how it differs from loosely typed languages.

Statically Typed

  • In Go, the type of a variable is known at compile time.
  • You must declare the type of a variable when you define it, or the type is inferred at the time of declaration.
  • Once a variable’s type is set, it cannot change.
var x int = 10 // x is explicitly declared as an int
y := 20 // y is inferred as an int

If you try to assign a value of a different type to x or y, the compiler will throw an error:

x = "Hello" // Error: cannot use "Hello" (type string) as type int

Strongly Typed

  • Go enforces strict type rules. You cannot perform operations between incompatible types without explicit type conversion.
  • For example, you cannot add an int and a float64 without converting one of them.
var a int = 10
var b float64 = 5.5

// This will cause a compilation error
// result := a + b // Error: invalid operation: a + b (mismatched types int and float64)

// You need to explicitly convert types
result := float64(a) + b
fmt.Println(result) // Output: 15.5

Comparison with Loosely Typed Languages

  • Loosely typed languages (e.g., JavaScript, PHP) allow variables to change types dynamically and perform implicit type conversions.
  • For example, in JavaScript, you can do this:
let x = 10;      // x is a number
x = "Hello"; // x is now a string
let y = x + 5; // y is "Hello5" (implicit type conversion)
  • In Go, such behavior is not allowed. Types are fixed and must be explicitly handled.

Type Inference in Go

While Go is statically typed, it does support type inference for shorthand variable declarations (:=). The compiler infers the type based on the assigned value.

x := 10      // x is inferred as int
y := 3.14 // y is inferred as float64
z := "Hello" // z is inferred as string

However, once the type is inferred, it cannot change:

x = "Hello" // Error: cannot use "Hello" (type string) as type int

Dynamic Typing in Go?

Go does not support dynamic typing. However, it provides some flexibility through interfaces and type assertions:

  • Interfaces: Allow you to define behavior without specifying the exact type.
  • Type Assertions: Allow you to check and convert types at runtime.
package main

import "fmt"

func printValue(val interface{}) {
switch v := val.(type) {
case int:
fmt.Println("Integer:", v)
case string:
fmt.Println("String:", v)
default:
fmt.Println("Unknown type")
}
}

func main() {
printValue(42) // Output: Integer: 42
printValue("Hello") // Output: String: Hello
printValue(3.14) // Output: Unknown type
}

Control Structures

if, else if, else Statements

To execute different blocks of code based on whether a condition is true or false. Following is syntax.

if condition1 {
// Code to execute if condition1 is true
} else if condition2 {
// Code to execute if condition1 is false and condition2 is true
} else {
// Code to execute if both condition1 and condition2 are false
}

Following is one example.

package main

import "fmt"

func main() {
age := 15

if age < 13 {
fmt.Println("You are a child.")
} else if age >= 13 && age < 20 {
fmt.Println("You are a teenager.")
} else {
fmt.Println("You are an adult.")
}
}

Short Statements: You can include a short statement (like a variable declaration) before the condition, which is often used for error handling:

Syntax

if short_statement; condition {
// Code to execute if condition is true
} else {
// Code to execute if condition is false
}
  • The short_statement is executed before evaluating the condition.
  • Variables declared in the short_statement are only accessible within the if or else block.

Following is example.

if err := someFunction(); err != nil {
// Handle the error
} else {
// Proceed if there was no error
}

// err is not accessible here.

Using Multiple Short Statements: You can use multiple short statements separated by commas.

package main

import "fmt"

func main() {
if x, y := 10, 20; x < y {
fmt.Println("x is less than y") // Output: x is less than y
} else {
fmt.Println("x is greater than or equal to y")
}
}

Error handling

package main

import (
"errors"
"fmt"
)

func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}

func main() {
if result, err := divide(10, 2); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result) // Output: Result: 5
}

if result, err := divide(10, 0); err != nil {
fmt.Println("Error:", err) // Output: Error: division by zero
} else {
fmt.Println("Result:", result)
}
}

switch Statements

To select one of many code blocks to execute, similar to a series of if/else if but often more concise and readable, especially when dealing with multiple possibilities.

Syntax

switch expression { // The expression is optional
case value1:
// Code to execute if expression == value1
case value2, value3: // Multiple values can be matched
// Code to execute if expression == value2 or expression == value3
default: // Optional
// Code to execute if no other case matches
}

Example of basic switch case

package main

import "fmt"

func main() {
day := "Friday"

switch day {
case "Monday":
fmt.Println("Start of the work week.")
case "Tuesday":
fmt.Println("Taco Tuesday!")
case "Wednesday":
fmt.Println("Hump day.")
case "Thursday":
fmt.Println("Almost there...")
case "Friday":
fmt.Println("TGIF!")
default:
fmt.Println("It's the weekend!")
}
}

switch without an expression (like an if/else if/else chain):

package main

import "fmt"

func main() {
score := 85

switch {
case score >= 90:
fmt.Println("Grade: A")
case score >= 80:
fmt.Println("Grade: B")
case score >= 70:
fmt.Println("Grade: C")
case score >= 60:
fmt.Println("Grade: D")
default:
fmt.Println("Grade: F")
}
}

switch with a short statement:

switch num := calculateValue(); { // num is only scoped to the switch
case num < 0:
fmt.Println("Negative")
case num > 0:
fmt.Println("Positive")
default:
fmt.Println("Zero")
}

Fallthrough

In Go, switch cases do not fall through to the next case by default (unlike C/C++/Java). If you want fallthrough behavior, use the fallthrough keyword explicitly:

switch day {
case "Monday":
fmt.Println("Ugh.")
fallthrough // Execution will continue to the next case
case "Tuesday":
fmt.Println("At least it's not Monday.")
default:
fmt.Println("Some other day.")
}

for Loops

Go’s only looping construct. Used for repeating a block of code.

Syntax: Go has only one looping construct, the for loop, but it's versatile and can be used in different ways.

// 1. The most common form:
for initialization; condition; post {
// Code to execute repeatedly
}

// 2. Equivalent to a while loop in other languages
for condition {
// Code to execute repeatedly
}

// 3. An infinite loop (use break to exit)
for {
// Code to execute repeatedly
}

// 4. Looping over a collection with `range`
for index, value := range collection {
// Code that uses the index and value
}

Traditional for loop example:

package main

import "fmt"

func main() {
for i := 0; i < 5; i++ {
fmt.Println(i)
}
}
// Output:
// 0
// 1
// 2
// 3
// 4

while loop style:

package main

import "fmt"

func main() {
count := 0
for count < 3 {
fmt.Println(count)
count++
}
}
// Output:
// 0
// 1
// 2

Infinite loop (with break):

package main

import "fmt"

func main() {
i := 0
for { // Infinite loop
fmt.Println(i)
i++
if i >= 5 {
break // Exit the loop when i is 5
}
}
}

Looping with range:

package main

import "fmt"

func main() {
numbers := []int{2, 4, 6, 8, 10}

// Get both index and value
for index, value := range numbers {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}

// Only get the value (ignore the index using the blank identifier "_")
for _, value := range numbers {
fmt.Println("Value:", value)
}

// Only get the index
for index := range numbers {
fmt.Println("Index:", index)
}

// Looping over a string (get runes/characters)
message := "Hello"
for index, char := range message {
fmt.Printf("Index: %d, Character: %c\n", index, char)
}
}

break

Exits the innermost for, switch, or select statement immediately.

for i := 0; i < 10; i++ {
if i == 5 {
break // Stop the loop when i is 5
}
fmt.Println(i)
}
// Output: 0 1 2 3 4

continue

Skips the rest of the current iteration of the innermost for loop and proceeds to the next iteration.

for i := 0; i < 5; i++ {
if i == 2 {
continue // Skip the iteration when i is 2
}
fmt.Println(i)
}
// Output: 0 1 3 4

Labels (with break and continue): You can use labels to break or continue from outer loops when you have nested loops:

outerLoop:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outerLoop // Breaks out of both loops
}
fmt.Println(i, j)
}
}
// Output:
// 0 0
// 0 1
// 0 2
// 1 0

goto (Use Sparingly!)

Transfers control to a labeled statement within the same function.

goto labelName
// ...
labelName: // Statement

Example

package main

import "fmt"

func main() {
i := 0

loopStart: // Label
if i < 5 {
fmt.Println(i)
i++
goto loopStart // Jump back to the label
}
}

Functions

Fundamentals of Go Functions

What are they? Functions are blocks of reusable code that perform specific tasks. They are essential for organizing code, promoting modularity, and avoiding repetition (DRY — Don’t Repeat Yourself).

Declaration

func functionName(parameter1 type, parameter2 type, ...) returnType {
// Function body (code to be executed)
// ...
return value // If the function has a return type
}
  • func keyword: Indicates a function definition.
  • functionName: A descriptive name for your function (use camelCase).
  • parameters: A comma-separated list of input values (optional). Each parameter has a name and a type.
  • returnType: The type of value the function returns (optional). If omitted, the function doesn't return a value.
  • return statement: Used to return a value from the function. The type of the returned value must match the returnType.

Basic Function Example

package main

import "fmt"

// Adds two integers and returns the sum.
func add(x int, y int) int {
return x + y
}

func main() {
result := add(5, 3)
fmt.Println("Sum:", result) // Output: Sum: 8
}

Multiple Parameters of the Same Type:

func add(x, y int) int { // Shorthand when parameters share the same type
return x + y
}

No Parameters:

func greet() {
fmt.Println("Hello!")
}

Variadic Functions: Functions that accept a variable number of arguments. The variadic parameter must be the last one in the list.

func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}

func main() {
s1 := sum(1, 2, 3) // Pass individual arguments
s2 := sum(4, 5, 6, 7, 8) // Pass more arguments
nums := []int{10, 20, 30}
s3 := sum(nums...) // Pass a slice using the ... operator to unpack it
fmt.Println(s1, s2, s3) // Output: 6 30 60
}

Multiple Return Values

Go functions can return more than one value. This is often used to return both a result and an error value.

func divide(x, y int) (int, error) {
if y == 0 {
return 0, fmt.Errorf("division by zero")
}
return x / y, nil
}

func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result) // Output: Result: 5
}

_, err = divide(5, 0) // Use _ to ignore the result and only check for the error
if err != nil {
fmt.Println("Error:", err) // Output: Error: division by zero
}
}

Named Return Values(naked return statement): You can give names to the return values. This can improve readability, especially for functions with multiple return values. It also allows you to use a “naked” return statement.

func split(sum int) (x, y int) { // x and y are named return values
x = sum * 4 / 9
y = sum - x
return // "Naked" return - implicitly returns x and y
}

func main() {
a, b := split(17)
fmt.Println(a, b) // Output: 7 10
}

Function as Values and Arguments (First-Class Functions)

Functions as Values: In Go, functions are first-class citizens. You can assign them to variables, pass them as arguments to other functions, and return them from functions.

package main

import "fmt"

func add(x, y int) int {
return x + y
}

func subtract(x, y int) int {
return x - y
}

// apply is a higher-order function that takes a function as an argument
func apply(x, y int, operation func(int, int) int) int {
return operation(x, y)
}

func main() {
// Assign functions to variables
var op func(int, int) int
op = add
fmt.Println(op(2, 3)) // Output: 5

op = subtract
fmt.Println(op(5, 2)) // Output: 3

// Pass functions as arguments
sum := apply(4, 6, add)
difference := apply(8, 3, subtract)
fmt.Println(sum, difference) // Output: 10 5

// Anonymous function (function literal) passed directly
product := apply(3, 4, func(x, y int) int {
return x * y
})
fmt.Println(product) // Output: 12
}

Anonymous Functions (Function Literals): Functions without a name. Often used for short, inline operations or when you need a function only once.

greet := func(name string) {
fmt.Println("Hello,", name)
}
greet("Alice") // Output: Hello, Alice

Closures: Anonymous functions that “remember” values from their surrounding scope, even after the outer function has returned.

func adder() func(int) int {
sum := 0 // sum is "captured" by the inner function
return func(x int) int {
sum += x
return sum
}
}

func main() {
myAdder := adder()
fmt.Println(myAdder(5)) // Output: 5
fmt.Println(myAdder(3)) // Output: 8 (sum is remembered)
fmt.Println(myAdder(10)) // Output: 18
}

Deferred Function Calls

defer keyword: Schedules a function call to be executed after the surrounding function returns, either normally or through a panic.

Common Uses:

  • Cleanup: Closing files, releasing resources, unlocking mutexes.
  • Ensuring actions are performed regardless of how the function exits.
  • LIFO (Last-In, First-Out) Order: Deferred calls are executed in reverse order of their defer statements.
package main

import "fmt"

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

defer fmt.Println("Deferred 1")
defer fmt.Println("Deferred 2")
defer fmt.Println("Deferred 3")

fmt.Println("Ending")
}
// Output:
// Starting
// Ending
// Deferred 3
// Deferred 2
// Deferred 1

Example with File Handling:

func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // Ensure the file is closed when the function exits

// ... process the file ...

return nil
}

init() Functions

Special functions within a package that are executed automatically when the package is initialized.

  • Cannot be called explicitly.
  • No parameters or return values.
  • A package can have multiple init() functions (even in the same file). They are executed in the order they appear in the source code.

Common Uses:

  • Initializing package-level variables.
  • Setting up connections to databases or other resources.
  • Registering handlers or types.
package mypackage

import "fmt"

var myVariable int

func init() {
fmt.Println("Initializing myVariable")
myVariable = 10
}

func init() {
fmt.Println("Another init function in mypackage")
}

// ... other code in the package ...

Recursion

Base Case: A condition that stops the recursion.

Recursive Step: The function calls itself with a modified version of the input, moving towards the base case.

func factorial(n int) int {
if n == 0 { // Base case
return 1
}
return n * factorial(n-1) // Recursive step
}

Data Structures

Arrays

Ordered, fixed-size collections of elements of the same type. Following are ways to declare it.

var arr1 [5]int         // Array of 5 integers, initialized to 0
arr2 := [3]string{"apple", "banana", "cherry"} // Array literal
arr3 := [...]int{1, 2, 3, 4, 5} // Compiler determines the length
  • Accessing Elements: Use zero-based indexing: arr1[0], arr2[1], etc.
  • Length: len(arr1) returns the number of elements in the array.
  • Fixed Size: Once declared, the size of an array cannot be changed.
package main

import "fmt"

func main() {
var scores [3]int
scores[0] = 85
scores[1] = 92
scores[2] = 78

fmt.Println(scores) // Output: [85 92 78]
fmt.Println(scores[1]) // Output: 92
fmt.Println(len(scores)) // Output: 3

// Array literal
names := [2]string{"Alice", "Bob"}
fmt.Println(names) // Output: [Alice Bob]
}

Multidimensional Arrays: Arrays of arrays.

matrix := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Println(matrix[1][2]) // Output: 6

Value Type: Arrays are value types. When you assign an array to a new variable or pass it to a function, a copy of the array is created.

arr1 := [3]int{1, 2, 3}
arr2 := arr1 // arr2 is a copy of arr1
arr2[0] = 10
fmt.Println(arr1) // Output: [1 2 3] (arr1 is unchanged)
fmt.Println(arr2) // Output: [10 2 3]

Slices

Dynamically sized, flexible views into an underlying array. Much more commonly used than arrays in Go.

var s1 []int       // A nil slice (no underlying array)
s2 := []int{1, 2, 3, 4, 5} // Slice literal
s3 := make([]int, 5) // Creates a slice of length 5, capacity 5, initialized to 0
s4 := make([]int, 0, 10) // Length 0, capacity 10 (pre-allocate space)

Relationship to Arrays: A slice doesn’t store data itself; it describes a portion of an underlying array.

Length and Capacity:

  • len(s): The number of elements in the slice.
  • cap(s): The maximum number of elements the slice can hold without reallocation (the size of the underlying array from the slice's starting point).
package main

import "fmt"

func main() {
numbers := []int{1, 2, 3, 4, 5} // Slice literal

fmt.Println(numbers) // Output: [1 2 3 4 5]
fmt.Println(len(numbers)) // Output: 5
fmt.Println(cap(numbers)) // Output: 5

// Slicing (creating sub-slices)
slice1 := numbers[1:4] // From index 1 up to (but not including) index 4
fmt.Println(slice1) // Output: [2 3 4]
fmt.Println(len(slice1)) // Output: 3
fmt.Println(cap(slice1)) // Output: 4 (capacity is from index 1 to the end of the original array)

slice2 := numbers[:3] // From the beginning up to index 3
slice3 := numbers[2:] // From index 2 to the end
slice4 := numbers[:] // A copy of the entire slice
fmt.Println(slice2, slice3, slice4) // Output: [1 2 3] [3 4 5] [1 2 3 4 5]
}

Appending

s := []int{1, 2, 3}
s = append(s, 4) // Append a single element
s = append(s, 5, 6, 7) // Append multiple elements
s2 := []int{8, 9, 10}
s = append(s, s2...) // Append another slice (use ... to expand it)
fmt.Println(s) // Output: [1 2 3 4 5 6 7 8 9 10]

make vs. new:

  • make: Used to create slices (and maps and channels). It initializes the data structure and returns a ready-to-use value.
  • new: Allocates memory but doesn't initialize it. It returns a pointer to the zero value of the type. Less commonly used for slices.

Reference Type: Slices are reference types. When you assign a slice to a new variable, both variables point to the same underlying array.

s1 := []int{1, 2, 3}
s2 := s1 // s2 refers to the same underlying array as s1
s2[0] = 10
fmt.Println(s1) // Output: [10 2 3] (s1 is modified because they share the underlying array)
fmt.Println(s2) // Output: [10 2 3]

Copying: Use the copy function to create an independent copy of a slice:

src := []int{1, 2, 3}
dest := make([]int, len(src))
numCopied := copy(dest, src) // Copies elements from src to dest
fmt.Println(dest, numCopied) // Output: [1 2 3] 3

Maps

Unordered collections of key-value pairs, where keys are unique and used to retrieve values. Similar to dictionaries (Python), hash tables, or associative arrays.

var m1 map[string]int        // A nil map (cannot add keys until initialized)
m2 := make(map[string]int) // Creates an empty map using make
m3 := map[string]string{ // Map literal
"name": "Alice",
"city": "New York",
"country": "USA",
}
  • Key Type: Can be any comparable type (types that support == and != operators), such as numbers, strings, booleans, arrays, structs (if their fields are comparable), and pointers. Slices, maps, and functions cannot be keys.
  • Value Type: Can be any type.
package main

import "fmt"

func main() {
// Create an empty map using make
ages := make(map[string]int)

// Add key-value pairs
ages["Alice"] = 30
ages["Bob"] = 25
ages["Charlie"] = 35

fmt.Println(ages) // Output: map[Alice:30 Bob:25 Charlie:35] (order is not guaranteed)

// Access values using keys
fmt.Println(ages["Alice"]) // Output: 30

// Check for key existence (comma ok idiom)
age, ok := ages["Bob"]
if ok {
fmt.Println("Bob's age:", age) // Output: Bob's age: 25
}

age, ok = ages["David"] // David is not in the map
if !ok {
fmt.Println("David's age not found") // Output: David's age not found
}

// Update a value
ages["Alice"] = 31

// Delete a key-value pair
delete(ages, "Charlie")

// Iterate over a map
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
// Output (order not guaranteed):
// Alice is 31 years old
// Bob is 25 years old
}
  • Zero Value: The zero value of a map type is nil. A nil map behaves like an empty map when reading, but attempts to write to a nil map will cause a runtime panic. Always initialize maps using make before writing to them.
  • Reference Type: Maps are reference types.

Strings

Sequences of bytes, typically used to represent text. In Go, strings are immutable (cannot be changed after creation).

var str string = "Hello, Go!"
str2 := "Another string"
  • UTF-8: Go strings are encoded using UTF-8, which means each character can take 1 to 4 bytes.
  • Immutability: You cannot modify individual characters of a string directly (e.g., str[0] = 'J'). To modify a string, you create a new one.
package main

import (
"fmt"
"strings"
)

func main() {
message := "Hello, Go!"

fmt.Println(message) // Output: Hello, Go!
fmt.Println(len(message)) // Output: 10 (number of bytes, not necessarily characters)
fmt.Println(message[0]) // Output: 72 (byte value of 'H')
fmt.Println(string(message[0])) // Output: H (convert byte back to string)

// String concatenation
s1 := "Hello"
s2 := "World"
s3 := s1 + ", " + s2
fmt.Println(s3) // Output: Hello, World

// Substrings (using slicing)
sub := message[0:5] // From index 0 up to (but not including) index 5
fmt.Println(sub) // Output: Hello

// String methods (many available in the strings package)
fmt.Println(strings.ToUpper(message)) // Output: HELLO, GO!
fmt.Println(strings.Contains(message, "Go")) // Output: true
fmt.Println(strings.ReplaceAll(message, "o", "0")) // Output: Hell0, G0!
}

Runes: Go has a rune type, which is an alias for int32. It represents a Unicode code point. Use runes when working with individual characters, especially when dealing with non-ASCII text.

str := "नमस्ते" // Hindi greeting
for _, r := range str {
fmt.Printf("%c ", r) // Output: न म स ् त े
}

String Conversions:

  • string(b): Converts a byte slice []byte to a string.
  • []byte(s): Converts a string s to a byte slice.
  • string(r): Converts a rune r to a string.
  • []rune(s): Converts a string s to a rune slice.
  • strconv Package: Use the strconv package to convert strings to numbers and vice-versa (e.g., strconv.Itoa, strconv.Atoi, strconv.ParseFloat, strconv.FormatFloat).

Structs — User-Defined Composite Types

Structs are composite data types that group together values (fields) of different types under a single name. They are used to create custom data structures.

type Person struct {
FirstName string
LastName string
Age int
Address struct { // Embedded struct
Street string
City string
Country string
}
}

Following is example.

package main

import "fmt"

type Person struct {
FirstName string
LastName string
Age int
Address Address // Composition using another struct
}

type Address struct {
Street string
City string
Country string
}

func main() {
// Initialize a struct
p1 := Person{
FirstName: "Alice",
LastName: "Smith",
Age: 30,
Address: Address{
Street: "123 Main St",
City: "New York",
Country: "USA",
},
}

// Access fields using dot notation
fmt.Println(p1.FirstName) // Output: Alice
fmt.Println(p1.Address.City) // Output: New York

// Update fields
p1.Age = 31

// Create a struct with some fields initialized (others get zero values)
p2 := Person{FirstName: "Bob"}
fmt.Println(p2) // Output: {Bob 0 { }}

// Pointer to a struct
p3 := &Person{FirstName: "Charlie", LastName: "Brown", Age: 25}
fmt.Println((*p3).FirstName) // Output: Charlie (accessing through a pointer)
fmt.Println(p3.FirstName) // Output: Charlie (shorthand for struct pointers)

p3.Age = 26 // Modifying through a pointer modifies the original struct
}
  • Methods: You can associate functions (called methods) with structs to give them behavior.
  • Composition (Embedding): A struct can embed another struct, promoting code reuse and a form of inheritance-like behavior.
  • Tags: Struct fields can have tags, which are string literals providing metadata. Commonly used for serialization/deserialization (e.g., JSON, XML), ORM mapping, validation, etc.
type User struct {
ID int `json:"id" validate:"required"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // omitempty tag means the field will be omitted if it's empty
}

Object oriented concept

Go (Golang) is not a purely object-oriented programming (OOP) language like Java or C++, but it does support some object-oriented concepts in a unique way. Go avoids traditional OOP features like classes and inheritance, instead favoring composition and interfaces. Here’s an overview of how Go implements object-oriented concepts:

Structs Instead of Classes

In Go, structs are used to define custom types that group together fields (attributes). Structs are similar to classes in other languages but do not support methods directly within the struct definition.

type Person struct {
Name string
Age int
}

Methods on Structs

Go allows you to define methods on structs, which are functions with a special receiver argument. The receiver can be a struct or a pointer to a struct.

func (p Person) Greet() string {
return "Hello, my name is " + p.Name
}

func (p *Person) Birthday() {
p.Age++
}
  • Value Receiver: (p Person) operates on a copy of the struct.
  • Pointer Receiver: (p *Person) operates on the original struct, allowing modifications.

Composition Over Inheritance

Go does not support inheritance. Instead, it encourages composition by embedding structs within other structs. This allows you to reuse and extend functionality.

type Employee struct {
Person // Embedded struct
JobTitle string
}

func main() {
emp := Employee{
Person: Person{Name: "Alice", Age: 30},
JobTitle: "Developer",
}
fmt.Println(emp.Greet()) // Reusing Person's method
}

Interfaces for Polymorphism

Go uses interfaces to achieve polymorphism. An interface defines a set of method signatures, and any type that implements those methods implicitly satisfies the interface.

type Speaker interface {
Speak() string
}

func (p Person) Speak() string {
return "Hi, I'm " + p.Name
}

func Greet(s Speaker) {
fmt.Println(s.Speak())
}

func main() {
p := Person{Name: "Bob"}
Greet(p) // Output: Hi, I'm Bob
}
  • Interfaces in Go are implicitly implemented. There is no need to explicitly declare that a type implements an interface.
  • This makes Go’s interfaces flexible and decoupled from specific types.

Encapsulation

Go uses naming conventions to control visibility:

  • Exported (public): Identifiers starting with an uppercase letter (e.g., Name, Greet).
  • Unexported (private): Identifiers starting with a lowercase letter (e.g., age, greet).

No Constructors

Go does not have constructors, but it is common to use factory functions to initialize structs.

func NewPerson(name string, age int) Person {
return Person{
Name: name,
Age: age,
}
}

func main() {
p := NewPerson("Charlie", 25)
fmt.Println(p)
}

No Generics (Before Go 1.18)

Before Go 1.18, Go did not support generics, which made it challenging to write reusable, type-safe code. However, generics were introduced in Go 1.18, allowing for more flexible and reusable code.

func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}

func main() {
PrintSlice([]int{1, 2, 3})
PrintSlice([]string{"a", "b", "c"})
}

No Method Overloading

Go does not support method overloading (defining multiple methods with the same name but different parameters). Instead, you must use unique method names.

Empty Interface for Dynamic Typing

The empty interface (interface{}) can hold values of any type, similar to Object in Java or void* in C++. This is often used for functions that need to handle multiple types.

func PrintValue(v interface{}) {
fmt.Println(v)
}

func main() {
PrintValue(42)
PrintValue("Hello")
}

Embedding Interfaces

Go allows embedding interfaces within other interfaces, enabling you to compose interfaces.

type Reader interface {
Read() string
}

type Writer interface {
Write(string)
}

type ReadWriter interface {
Reader
Writer
}

Error Handling

The Built-in error Interface

The error interface is the standard way to represent errors in Go. It's defined as:

type error interface {
Error() string
}

Any type that implements the Error() method, which returns an error message as a string, satisfies the error interface.

Creating Errors

errors.New(): The simplest way to create a basic error value.

import "errors"

err := errors.New("something went wrong")

fmt.Errorf(): Creates formatted error messages, similar to fmt.Printf(). You can use the %w verb to wrap an existing error, adding context (more on this later).

import "fmt"

err := fmt.Errorf("file not found: %s", filename)

Returning Errors from Functions

Functions that can encounter errors return an error value as their last (or only) return value. A nil error indicates success.

func divide(x, y int) (int, error) {
if y == 0 {
return 0, fmt.Errorf("division by zero")
}
return x / y, nil // nil indicates no error
}

Handling Errors

The if err != nil Pattern: The most common way to handle errors is to check if the returned error is not nil.

result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return // Or handle the error in some other way (e.g., log, retry, exit)
}
fmt.Println("Result:", result) // Proceed if no error

Ignoring Errors (Use with Caution!): You can use the blank identifier _ to ignore an error value. Only do this if you are absolutely sure that the error can be safely ignored.

result, _ := divide(10, 2) // I don't care about potential errors here
fmt.Println("Result:", result)

Custom Error Types

Create a struct and implement the Error() method to satisfy the error interface.

`package main

import (
"fmt"
"time"
)

// Custom error type with extra information
type MyError struct {
When time.Time
What string
}

// Implement the Error() method for MyError
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s", e.When, e.What)
}

func doSomething() error {
// ... some operation that might fail ...

return &MyError{ // Return a pointer to MyError
When: time.Now(),
What: "it didn't work",
}
}

func main() {
if err := doSomething(); err != nil {
fmt.Println(err) // The Error() method is called automatically
// Output (with current time): at 2023-10-27 10:30:00.123456 +0000 UTC, it didn't work

// Type assertion to access MyError fields
if myErr, ok := err.(*MyError); ok {
fmt.Println("Error occurred on:", myErr.When)
fmt.Println("Error message:", myErr.What)
}
}
}

Error Wrapping (Go 1.13+)

Adds context to an error while preserving the original error. This creates an error chain that you can traverse.

fmt.Errorf() with %w: Use the %w verb to wrap an error:

err := readConfig() // Assume readConfig() returns an error
if err != nil {
return fmt.Errorf("failed to load configuration: %w", err)
}

errors.Is(): Checks if an error (or any error in its chain) matches a specific target error.

if errors.Is(err, os.ErrNotExist) {
// Handle file not found error specifically
}

errors.As(): Checks if an error (or any error in its chain) can be assigned to a specific type. This is useful for extracting custom error types.

var myErr *MyError
if errors.As(err, &myErr) {
fmt.Println("Custom error occurred:", myErr.What)
}

Unwrapping with errors.Unwrap(): You can get the next error in the chain using errors.Unwrap().

Sentinel Errors

Predefined, exported error variables that represent specific error conditions.

Example: io.EOF (end of file), sql.ErrNoRows (no rows found in a database query).

package mypackage

import "errors"

var ErrResourceNotFound = errors.New("resource not found")

func GetResource(id int) (*Resource, error) {
// ...
if notFound {
return nil, ErrResourceNotFound
}
// ...
}

// In another package:
resource, err := mypackage.GetResource(123)
if err != nil {
if errors.Is(err, mypackage.ErrResourceNotFound) {
// Handle resource not found specifically
} else {
// Handle other errors
}
}

Defer, Panic, and Recover

  • defer: Used to schedule a function call to be executed when the surrounding function returns (used for cleanup).
  • panic: Indicates a serious, often unrecoverable error. It stops the normal flow of execution and starts unwinding the stack, calling deferred functions along the way.
  • recover: Used inside a deferred function to stop a panic and regain control. It returns the value that was passed to panic.
func safeDivide(x, y int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
result = 0 // Set a default result
}
}()

if y == 0 {
panic("division by zero") // Cause a panic
}
result = x / y
return result, nil
}

func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("Error:", err) // Output: Error: recovered from panic: division by zero
}
fmt.Println("Result:", result) // Output: Result: 0
}

When to use panic:

  • Unrecoverable errors: Situations where the program cannot continue, like programming errors (e.g., invalid array index, nil pointer dereference).
  • During initialization: If essential setup fails (e.g., cannot connect to a database, cannot load a required configuration file).

When to use recover:

  • Preventing crashes: In long-running servers or critical applications where you want to handle panics gracefully and continue operation.
  • Isolating failures: In concurrent programs, you might want to recover from a panic in one goroutine without crashing the entire application.

Error Handling in Goroutines:

  • Each goroutine is responsible for handling its own errors.
  • Use channels to communicate errors from goroutines back to the main goroutine or other parts of the application.
func worker(jobs <-chan int, results chan<- int, errs chan<- error) {
for j := range jobs {
result, err := processJob(j)
if err != nil {
errs <- err // Send the error on the error channel
return // Or continue processing other jobs
}
results <- result
}
}

Best Practices for Error Handling

  • Handle Errors Where They Occur: Don’t just propagate errors up the call stack without doing anything. Log, wrap, or handle errors as close as possible to where they originate.
  • Provide Context: Wrap errors with fmt.Errorf("%w") to add information about what was happening when the error occurred.
  • Use Custom Error Types: Define your own error types when you need to represent specific error conditions or attach extra data to errors.
  • Use Sentinel Errors Sparingly: Only create sentinel errors for a few specific cases that callers are likely to check for explicitly.
  • Don’t Panic for Normal Errors: Use panic only for truly exceptional situations that indicate a bug in the program.
  • Document Your Errors: Explain in comments or documentation what errors a function might return and what they mean.
  • Log Errors Effectively: Log errors with enough detail to diagnose problems later. Consider including timestamps, error messages, stack traces (using a library like pkg/errors in the past, but now you can use %+v with fmt.Errorf()), and any relevant contextual information.

fmt.Errorf() vs fmt.Sprintf

Both fmt.Errorf() and fmt.Sprintf() are used for formatting strings, but they serve different purposes.

fmt.Sprintf(): Formats a string and returns it as a string type. When you need to create a formatted string for logging, messages, or any other purpose.

package main

import (
"fmt"
)

func main() {
name := "Alice"
age := 30

// Format a string
message := fmt.Sprintf("%s is %d years old.", name, age)
fmt.Println(message) // Output: Alice is 30 years old.
}

fmt.Errorf() : Formats a string and returns it as an error type. When you need to create a custom error message.

package main

import (
"fmt"
)

func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero: cannot divide %f by %f", a, b)
}
return a / b, nil
}

func main() {
_, err := divide(10, 0)
if err != nil {
fmt.Println(err) // Output: division by zero: cannot divide 10.000000 by 0.000000
}
}

Combining fmt.Sprintf() and fmt.Errorf():You can use fmt.Sprintf() to create a formatted string and then wrap it in an error using fmt.Errorf().

package main

import (
"fmt"
)

func main() {
name := "Alice"
age := 30

// Create a formatted string
message := fmt.Sprintf("%s is %d years old.", name, age)

// Wrap the formatted string in an error
err := fmt.Errorf("validation error: %s", message)
fmt.Println(err) // Output: validation error: Alice is 30 years old.
}

Pointers

What are Pointers?

Memory Addresses: A pointer is a variable that stores the memory address of another variable. Think of it as a signpost that points to a specific location in your computer’s memory where a value is stored.

Analogy: Imagine your computer’s memory as a long street with houses (memory locations), each having a unique address. A pointer is like a note that contains the address of a particular house. If you have the address (the pointer), you can find the house (the value stored at that address).

Declaration:

var ptr *int  // Declares a pointer to an integer
  • *T: The type *T is a pointer to a value of type T. So *int is a pointer to an integer, *string is a pointer to a string, and so on.
  • Zero Value: The zero value of a pointer is nil, meaning it doesn't point to anything yet.

The Address-of Operator (&)

The & operator is used to get the memory address of a variable.

package main

import "fmt"

func main() {
x := 10
ptr := &x // ptr now holds the memory address of x

fmt.Println("Value of x:", x) // Output: Value of x: 10
fmt.Println("Address of x:", &x) // Output: Address of x: 0xc00001a098 (or some other memory address)
fmt.Println("Value of ptr:", ptr) // Output: Value of ptr: 0xc00001a098 (same address as above)
}

The Dereference Operator (*)

The * operator is used to access the value stored at the memory address held by a pointer. This is called dereferencing the pointer.

package main

import "fmt"

func main() {
x := 10
ptr := &x

fmt.Println("Value of ptr:", ptr) // Output: Value of ptr: 0xc00001a098 (memory address)
fmt.Println("Value at ptr:", *ptr) // Output: Value at ptr: 10 (value stored at the address)

*ptr = 20 // Modify the value at the address pointed to by ptr
fmt.Println("New value of x:", x) // Output: New value of x: 20 (x has been changed through the pointer)
}

Pointers and Functions

Pass by Value (Copies): In Go, function arguments are passed by value. This means that when you pass a variable to a function, a copy of the variable’s value is created inside the function. Any changes made to the parameter inside the function do not affect the original variable.

package main

import "fmt"

func changeValue(val int) {
val = 100
}

func main() {
x := 5
changeValue(x)
fmt.Println(x) // Output: 5 (x is unchanged)
}

Pass by Reference (Using Pointers): To modify a variable inside a function, you need to pass a pointer to that variable. This way, the function can directly access and change the value at the original memory location.

package main

import "fmt"

func changeValue(ptr *int) {
*ptr = 100 // Dereference the pointer to modify the value
}

func main() {
x := 5
changeValue(&x) // Pass the address of x
fmt.Println(x) // Output: 100 (x has been changed)
}

The new Function

The new function allocates memory for a new variable of a specified type and returns a pointer to that memory. The allocated memory is initialized to the zero value of the type.

Syntax:

ptr := new(int) // Allocates memory for an int and returns a pointer to it

Example:

package main

import "fmt"

func main() {
ptr := new(int) // ptr is a pointer to an int, initialized to 0

fmt.Println("Value of ptr:", ptr) // Output: Memory address (e.g., 0xc00001a0b0)
fmt.Println("Value at ptr:", *ptr) // Output: 0 (zero value of int)

*ptr = 50
fmt.Println("New value at ptr:", *ptr) // Output: 50
}

Pointers and Data Structures

Structs: Pointers are often used with structs to avoid copying large structs and to allow methods to modify the struct’s fields.

package main

import "fmt"

type Person struct {
Name string
Age int
}

func (p *Person) celebrateBirthday() { // Pointer receiver
p.Age++ // Modifies the original struct
}

func main() {
p := &Person{Name: "Alice", Age: 30} // Pointer to a Person struct

fmt.Println(p.Age) // Output: 30
p.celebrateBirthday()
fmt.Println(p.Age) // Output: 31
}

Arrays/Slices: You can use pointers to elements of an array or slice to modify the original elements. When you pass Arrays as parameter to method then it is passed as value. But Slices are passed as reference.

Map: Map is also passed as reference when used as parameter.

Linked Lists, Trees, etc.: Pointers are essential for building dynamic data structures where elements are linked together using memory addresses.

Pointer Arithmetic (Not Directly Supported): Unlike C/C++, Go does not allow direct pointer arithmetic (e.g., ptr + 2 to move to the next element). This is a deliberate design choice to improve memory safety and prevent common errors. To work with sequences of elements, use slices, which provide bounds checking and prevent out-of-bounds memory access.

When to Use Pointers

  • Modifying Values in Functions: When you want a function to be able to modify its arguments.
  • Large Data Structures: To avoid copying large amounts of data when passing them around or storing them in data structures.
  • Optional Values: A pointer can be nil, which can be used to represent an optional value (similar to nullable types in other languages).
  • Dynamic Data Structures: When building structures like linked lists, trees, graphs, etc.
  • Representing Resource Handles: Sometimes pointers are used to represent handles to external resources (e.g., files, network connections), but often interfaces are used for this purpose as well.

Potential Pitfalls

Nil Pointer Dereferences: If you try to dereference a nil pointer (a pointer that doesn't point to anything), you'll get a runtime panic (similar to a segmentation fault in C/C++). Always check if a pointer is nil before dereferencing it if there's a possibility that it might not be initialized.

var ptr *int // ptr is nil

if ptr != nil {
fmt.Println(*ptr) // Safe to dereference here
}

Dangling Pointers: A dangling pointer is a pointer that points to memory that has been freed or deallocated. This can happen if you’re not careful about how you manage memory. Go’s garbage collector helps prevent this in most cases, but be aware of the possibility when working with unsafe code.

Memory Leaks: In Go, memory leaks are less common than in languages without garbage collection. However, you can still create situations where memory is unintentionally held onto and not released. This can happen, for example, if you have long-lived global data structures that keep references to objects that are no longer needed. Be mindful of circular references and long-lived objects.

Concurrency

Concurrency vs. Parallelism

  • Concurrency: Dealing with multiple tasks at the same time. It’s about the composition of independently executing processes. Think of a chef juggling various tasks in a kitchen (prepping ingredients, stirring a pot, checking the oven) — they might not all be happening simultaneously, but the chef is managing multiple tasks concurrently.
  • Parallelism: Simultaneously executing multiple tasks, typically using multiple CPU cores. It’s about the simultaneous execution of processes. Think of multiple chefs working in a large kitchen, each working on a different dish at the same time.
  • Go’s Approach: Go makes it easy to write concurrent programs using goroutines and channels. The Go runtime scheduler then maps these concurrent operations onto the underlying operating system threads, potentially executing them in parallel if multiple cores are available.

Goroutines

Lightweight, independently executing functions. They are managed by the Go runtime, not the operating system. Creating thousands or even millions of goroutines is feasible.

go keyword: You start a goroutine by simply putting the go keyword before a function call:

go myFunction() // myFunction will execute concurrently

Following is example for reference.

package main

import (
"fmt"
"time"
)

func say(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
time.Sleep(100 * time.Millisecond)
}
}

func main() {
go say("world") // Start a new goroutine
say("hello") // This continues executing concurrently
}

Output: You’ll see “hello” and “world” interleaved in the output because the two say functions are running concurrently. The exact order might vary on each run.

Key Idea: When you start a goroutine, the Go runtime adds it to a queue of tasks to be executed. The runtime scheduler then picks up these tasks and runs them, potentially switching between them very rapidly. This gives the illusion of parallelism even on a single-core system.

Channels

Definition: Typed conduits used for communication and synchronization between goroutines. They allow one goroutine to send data to another.

Analogy: Think of channels like pipes connecting two goroutines. One goroutine can put data into the pipe (send), and another goroutine can take data out of the pipe (receive).

Declaration:

ch := make(chan int) // Creates an unbuffered channel of type int
ch := make(chan string, 10) // Creates a buffered channel of strings with capacity 10

Send and Receive Operators:

<- (Left Arrow): Used for both sending and receiving, depending on the context.

ch <- 5    // Send the value 5 on the channel ch
value := <-ch // Receive a value from the channel ch and assign it to value

Blocking Nature:

  • Unbuffered Channels: Sends and receives on unbuffered channels block until both a sender and a receiver are ready. This provides inherent synchronization.
  • Buffered Channels: Sends on a buffered channel block only when the buffer is full. Receives block only when the buffer is empty.
package main

import "fmt"

func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // Send sum to c
}

func main() {
s := []int{7, 2, 8, -9, 4, 0}

c := make(chan int)
go sum(s[:len(s)/2], c) // Start goroutines to sum halves of the slice
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // Receive from c (order might vary)

fmt.Println(x, y, x+y) // Output: -5 17 12 (or 17 -5 12)
}

main Function and Goroutines:

  • The main function itself runs in a goroutine (the main goroutine).
  • If the main function exits, the entire program terminates, even if other goroutines are still running.
  • You need to use synchronization mechanisms (like channels or sync.WaitGroup) to ensure that main waits for other goroutines to complete if necessary.

Synchronization and Coordination

Closing a Channel:

  • close(ch): Signals that no more values will be sent on the channel.
  • Receiving from a closed channel does not block and returns the zero value of the channel’s type.
  • It’s the sender’s responsibility to close a channel, not the receiver’s.

range over Channels:

for v := range ch {
fmt.Println(v) // Receive values from ch until it's closed
}

Checking if a Channel is Closed:

v, ok := <-ch
if !ok {
fmt.Println("Channel is closed")
}

Select Statement:

  • Purpose: Allows a goroutine to wait on multiple communication operations (sends or receives on multiple channels).
  • Similar to switch: It has multiple case statements, each associated with a communication operation.
  • Blocking: It blocks until at least one of the communication operations can proceed.
  • default: An optional default case is executed if none of the other cases are ready.
select {
case v := <-ch1:
fmt.Println("Received from ch1:", v)
case v := <-ch2:
fmt.Println("Received from ch2:", v)
case ch3 <- 10:
fmt.Println("Sent 10 to ch3")
default:
fmt.Println("No communication ready")
}

Example: Using select for Timeout:

select {
case result := <-ch:
fmt.Println("Received result:", result)
case <-time.After(3 * time.Second): // Timeout after 3 seconds
fmt.Println("Timeout!")
}

sync Package

sync.WaitGroup: To wait for a collection of goroutines to finish.

  • Add(delta int): Adds delta to the counter.
  • Done(): Decrements the counter by 1 (typically deferred in a goroutine).
  • Wait(): Blocks until the counter is zero.
package main

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

func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Decrement the counter when the goroutine finishes

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

func main() {
var wg sync.WaitGroup

for i := 1; i <= 5; i++ {
wg.Add(1) // Increment the counter for each goroutine
go worker(i, &wg)
}

wg.Wait() // Wait for all workers to finish
fmt.Println("All workers finished.")
}

sync.Mutex (Mutual Exclusion Lock):

To protect shared resources from concurrent access, ensuring that only one goroutine can access the resource at a time.

  • Lock(): Acquires the lock. If the lock is already held, the goroutine blocks until it becomes available.
  • Unlock(): Releases the lock.
package main

import (
"fmt"
"sync"
)

var (
counter int
mu sync.Mutex
)

func increment() {
mu.Lock() // Acquire the lock
defer mu.Unlock() // Release the lock when the function exits

counter++
fmt.Println("Counter:", counter)
}

func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
}

sync.RWMutex (Read/Write Mutex):

Allows multiple readers or a single writer to access a shared resource concurrently.

  • RLock(): Acquires a read lock. Multiple readers can hold the read lock simultaneously.
  • RUnlock(): Releases a read lock.
  • Lock(): Acquires a write lock. Only one writer can hold the lock at a time, and no readers are allowed while the write lock is held.
  • Unlock(): Releases a write lock.
var (
data int
rwMu sync.RWMutex
)

func readData() int {
rwMu.RLock()
defer rwMu.RUnlock()
// Read data
return data
}

func writeData(val int) {
rwMu.Lock()
defer rwMu.Unlock()
// Write data
data = val
}

sync.Once:

To ensure that a function is executed only once, even if called concurrently from multiple goroutines. Do(f func()): Takes a function f as an argument and executes it only once.

var once sync.Once
var config *Config

func GetConfig() *Config {
once.Do(func() { // This function will be executed only once
config = loadConfig() // Assume loadConfig() loads configuration data
})
return config
}

sync.Cond (Condition Variable):

To provide a way for goroutines to wait for a certain condition to become true, and to signal other goroutines when the condition has changed.

  • Wait(): Atomically unlocks the associated mutex and suspends the calling goroutine. When the goroutine is awakened, it reacquires the lock before returning.
  • Signal(): Wakes up one waiting goroutine (if any).
  • Broadcast(): Wakes up all waiting goroutines.

Typical Use: Used in conjunction with a sync.Mutex to protect the condition being checked.

var (
mu sync.Mutex
cond = sync.NewCond(&mu)
ready bool
)

func worker() {
mu.Lock()
for !ready { // Check the condition in a loop
cond.Wait() // Wait until ready is true
}
mu.Unlock()
// ... do work ...
}

func main() {
// ... start multiple worker goroutines ...

// ... some time later ...
mu.Lock()
ready = true // Change the condition
mu.Unlock()
cond.Broadcast() // Notify all waiting goroutines
}

Worker Pools

A fixed number of worker goroutines are created to process tasks from a queue (typically a channel).

Benefits:

  • Limits resource usage by controlling the number of concurrent workers.
  • Improves performance by reusing goroutines instead of creating new ones for each task.
package main

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

func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Println("worker", id, "started job", j)
time.Sleep(time.Second) // Simulate work
fmt.Println("worker", id, "finished job", j)
results <- j * 2 // Send result
}
}

func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)

// Start worker goroutines
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}

// Send jobs to the workers
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // Close the jobs channel to signal that no more jobs will be sent

// Collect results
for a := 1; a <= numJobs; a++ {
<-results
}
}

Fan-Out, Fan-In

  • Fan-Out: A single goroutine distributes tasks to multiple worker goroutines.
  • Fan-In: Multiple goroutines send results to a single goroutine that collects and processes them.
package main

import (
"fmt"
"sync"
)

func producer(nums []int) <-chan int { // Fan-out: Produces data
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}

func square(in <-chan int) <-chan int { // Worker: Processes data
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}

func merge(cs ...<-chan int) <-chan int { // Fan-in: Merges results
var wg sync.WaitGroup
out := make(chan int)

output := func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}

wg.Add(len(cs))
for _, c := range cs {
go output(c)
}

// Start a goroutine to close out once all output goroutines are done
go func() {
wg.Wait()
close(out)
}()
return out
}

func main() {
in := producer([]int{1, 2, 3, 4, 5})

// Fan-out: Start two square worker goroutines
c1 := square(in)
c2 := square(in)

// Fan-in: Merge results from c1 and c2
for n := range merge(c1, c2) {
fmt.Println(n)
}
}

Pipelines

A series of stages connected by channels, where each stage performs a specific operation on the data flowing through the pipeline.

package main

import "fmt"

func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}

func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}

func main() {
// Set up the pipeline: generator -> square -> output
c := generator(2, 3)
out := square(c)

// Consume the output
for n := range out {
fmt.Println(n) // Output: 4 9
}
}

Testing and Benchmarking

The testing Package: The core of Go's testing capabilities is the testing package in the standard library. It provides the tools you need to write and run tests.

Test Files:

  • Test files have names that end with _test.go. For example, if you have a file named mycode.go, your test file would be mycode_test.go.
  • Test files reside in the same package as the code they are testing.

Test Functions:

  • Test functions have names that start with Test followed by a capitalized word or phrase (e.g., TestCalculateSum, TestValidateInput).
  • They take a single argument of type *testing.T.
  • They are used to make assertions about the behavior of your code.

Running Tests:

  • go test: Runs tests in the current package.
  • go test ./...: Runs tests in the current directory and all subdirectories.
  • go test -v: Runs tests in verbose mode, showing the results of each individual test.
  • go test -run <regex>: Runs only tests whose names match the regular expression.
  • go test -cover: Shows test coverage information (what percentage of your code is executed by tests).
  • go test -coverprofile=cover.out: Generates a coverage profile file.
  • go tool cover -html=cover.out: Displays the coverage profile in a web browser, highlighting covered and uncovered code.

Unit Testing with testing.T

Assertions: Test functions use methods on the *testing.T object to report failures:

  • t.Error(args ...): Reports a non-fatal error. The test continues execution.
  • t.Errorf(format string, args ...): Reports a non-fatal error with a formatted message.
  • t.Fatal(args ...): Reports a fatal error. The test stops execution immediately.
  • t.Fatalf(format string, args ...): Reports a fatal error with a formatted message.
  • t.Log(args ...): Logs information (useful for debugging).
  • t.Logf(format string, args ...): Logs formatted information.
  • t.Helper(): Marks the calling function as a test helper function. When a test fails, the error will be reported on the line of code where the helper function was called from, making it easier to identify the source of the failure.
// mycode.go
package mypackage

func Add(x, y int) int {
return x + y
}
// mycode_test.go
package mypackage

import "testing"

func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}

Table-Driven Tests: A powerful pattern for testing multiple inputs and expected outputs concisely.

// mycode_test.go
package mypackage

import "testing"

func TestAdd(t *testing.T) {
tests := []struct {
name string
x int
y int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"zero", 0, 5, 5},
{"zero values", 0, 0, 0}, // Example of a descriptive name
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { // Run each test case as a subtest
result := Add(tt.x, tt.y)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.x, tt.y, result, tt.expected)
}
})
}
}

Setup and Teardown:

TestMain: A special function that can be used to perform setup and teardown for an entire test suite (all tests in a package). It's called only once per package.

func TestMain(m *testing.M) {
// Perform setup (e.g., create a database connection, initialize resources)
fmt.Println("Setup for the test suite")

exitCode := m.Run() // Run the tests

// Perform teardown (e.g., close the database connection, clean up resources)
fmt.Println("Teardown for the test suite")

os.Exit(exitCode)
}

t.Cleanup(func()): Registers a function to be called when the test (or subtest) completes. Useful for cleaning up resources specific to a test.

func TestSomething(t *testing.T) {
// ... setup for the test ...

t.Cleanup(func() {
// Clean up resources for this test (e.g., close a file, delete temporary data)
fmt.Println("Cleaning up after TestSomething")
})

// ... test logic ...
}

Helper Functions: Functions that encapsulate common setup or assertion logic. Use t.Helper() to mark them as helper functions.

func assertEqual(t *testing.T, got, want int) {
t.Helper() // Mark this function as a helper
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}

func TestMyFunction(t *testing.T) {
result := MyFunction(5)
assertEqual(t, result, 10) // Errors will be reported on this line
}

Mocking (Beyond the Standard Library)

What is Mocking? Replacing dependencies of your code with controlled substitutes (mocks) for testing purposes. This allows you to isolate the unit under test and simulate different behaviors of its dependencies.

Why Mock?

  • Isolation: Test units in isolation, without relying on external systems or the real implementations of dependencies.
  • Control: Simulate various responses and error conditions from dependencies.
  • Speed: Mocking can make tests faster, especially when dealing with slow dependencies like databases or network call

Popular Mocking Libraries:

Example (Illustrative — using testify/mock):

// myapp.go
package myapp

type Database interface {
GetUserName(userID int) (string, error)
}

type MyApp struct {
db Database
}

func (a *MyApp) GetUserGreeting(userID int) (string, error) {
userName, err := a.db.GetUserName(userID)
if err != nil {
return "", err
}
return "Hello, " + userName + "!", nil
}
// myapp_test.go
import (
"errors"
"testing"

"github.com/stretchr/testify/mock"
)

// MockDatabase is a mock implementation of the Database interface
type MockDatabase struct {
mock.Mock
}

func (m *MockDatabase) GetUserName(userID int) (string, error) {
args := m.Called(userID) // Record that the method was called with userID
return args.String(0), args.Error(1) // Return values set by expectations
}

func TestGetUserGreeting(t *testing.T) {
// Create a mock database
mockDB := new(MockDatabase)

// Set up expectations for the mock
mockDB.On("GetUserName", 123).Return("Alice", nil) // Expect GetUserName(123) to be called, return "Alice", nil
mockDB.On("GetUserName", 456).Return("", errors.New("user not found")) // Expect GetUserName(456) to be called, return "", error

// Create an instance of MyApp with the mock database
app := &MyApp{db: mockDB}

// Test case 1: Successful retrieval
greeting, err := app.GetUserGreeting(123)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if greeting != "Hello, Alice!" {
t.Errorf("got %s; want %s", greeting, "Hello, Alice!")
}

// Test case 2: Error retrieval
_, err = app.GetUserGreeting(456)
if err == nil {
t.Fatal("expected error, got nil")
}

// Assert that all expectations were met
mockDB.AssertExpectations(t)
}

Reference

Happy learning golang :-)

--

--

Dilip Kumar
Dilip Kumar

Written by Dilip Kumar

With 18+ years of experience as a software engineer. Enjoy teaching, writing, leading team. Last 4+ years, working at Google as a backend Software Engineer.

No responses yet