Lecture 1 / 12
Lecture 01 Β· Fundamentals

Introduction to Go & Setup

Beginner ~50 min

What is Go?

Go (often called Golang) is an open-source, statically typed, compiled programming language developed at Google. It was designed to solve problems encountered in large-scale software development: slow build times, complex dependency management, and the difficulty of writing efficient concurrent code.

Think of Go as a language that combines the performance of C++ with the simplicity and readability of Python.

Why Go?

  • Excellent Performance: Since it is compiled directly to machine code, Go is incredibly fast and efficient.
  • Built-in Concurrency: Using "Goroutines" (lightweight threads), Go can handle thousands of tasks simultaneously, making it perfect for high-traffic servers.
  • Strong Typing: Being statically typed means errors are caught during compilation rather than at runtime, leading to more stable code.
  • Fast Compilation: Go was designed for fast build times, even for massive projects.
  • Ideal Use-Cases: It is the industry standard for cloud-native development, microservices, Kubernetes, Docker, and high-performance CLI tools.

Installation

To get started, download the installer from go.dev/dl. Follow the installation wizard for your OS (Windows, macOS, or Linux).

Once installed, verify the installation by opening your terminal and running:

terminal
go version
go version go1.22.3 linux/amd64

Your First Go Program

Create a file named main.go and enter the following code:

main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, Go Mastery!")
    fmt.Println("Fast, simple, and concurrent!")
}
Output
Hello, Go Mastery!
Fast, simple, and concurrent!

Breaking Down the Code

  • package main: Every Go file must start with a package declaration. The main package tells Go that this file should compile as an executable program rather than a library.
  • import "fmt": The fmt (format) package contains functions for formatting text and printing to the console.
  • func main(): This is the entry point of the program. When you run the program, the code inside this function is executed first.
  • fmt.Println(): A function from the fmt package that prints text to the console followed by a new line.

πŸ’» Try It Yourself - Multi-Language Compiler

Practice Go and many other programming languages right here in your browser! Switch between languages, modify the code, and click "Run" to see results instantly.

πŸ’‘ Practice Tips:

  • Switch to Go in the language selector and try the concurrency examples
  • Experiment with Go's simple syntax and strong typing
  • Try other languages like Rust, C++, or Java to compare performance features
  • Use the "Load Example" button to see Go-specific code samples
  • Use Ctrl+Enter to quickly run your code

Running Your Code

Go provides two main ways to run your code:

1. Direct Run (Development): Use this to test your code quickly without creating a permanent binary file.

terminal
go run main.go

2. Build (Production): This compiles the code into a standalone executable binary that can be run on any machine without Go installed.

terminal
go build main.go
./main

Understanding Go Modules

In modern Go, we use Modules to manage dependencies. A module is a collection of related Go packages that are versioned together. To start a new project, you must initialize a module:

terminal
go mod init hello-world

This creates a go.mod file, which tracks your project's dependencies and Go version.

🎯 Exercise 1.1

Complete these steps to verify your environment:

  1. Install Go and verify with go version.
  2. Create a project folder and initialize a module using go mod init hello.
  3. Create main.go, write a program that prints your name and your favorite goal for learning Go.
  4. Run it using go run main.go.

Lecture 02 Β· Fundamentals

Variables & Data Types

Beginner ~45 min

Introduction to Variables

Variables are used to store data in memory. In Go, every variable has a specific data type which determines what kind of value it can hold.

For example:

  • string β†’ stores text
  • int β†’ stores whole numbers
  • float64 β†’ stores decimal numbers
  • bool β†’ stores true or false values

Variables make programs dynamic because values can change while the program runs.

Declaring Variables

Go offers multiple ways to declare variables. The short declaration := is the most common inside functions.

var name string = "Gopher"
age := 10 // Type inferred as int

In the example above:

  • name is explicitly declared as a string
  • age uses type inference, so Go automatically detects the type

Different Ways to Declare Variables

Go supports several styles of variable declaration depending on the situation.

// Explicit type declaration
var city string = "Tokyo"

// Type inference
var score = 95

// Short declaration
language := "Go"

The short declaration syntax := can only be used inside functions.

Multiple Variable Declarations

You can declare multiple variables in a single line.

name, age := "Alice", 21

This is useful when working with multiple related values.

Understanding Data Types

Data types define the kind of data stored inside a variable.

Type Description Example
int Whole numbers 10, -5, 200
float64 Decimal numbers 3.14, 9.99
string Text values "Hello"
bool True or false true, false

Strings in Go

Strings are sequences of characters surrounded by double quotes.

message := "Welcome to Go"
fmt.Println(message)

Strings are commonly used for displaying messages, usernames, and text data.

Integer and Float Values

Integers store whole numbers while floats store decimal values.

age := 25
price := 19.99

Use integers when decimals are not needed and floats when precision with decimals is required.

Boolean Values

Boolean variables can only contain two values: true or false.

isLoggedIn := true
isAdmin := false

Booleans are commonly used in conditions and decision-making statements.

Zero Values

Variables declared without an initial value are given their zero value.

Data Type Zero Value
int 0
float64 0
string ""
bool false
var number int
var username string
var active bool

fmt.Println(number)
fmt.Println(username)
fmt.Println(active)

The output will be:

0

false

Constants

Constants are values that cannot be changed after they are declared.

const pi = 3.14159
const appName = "Go Learning"

Use constants for fixed values such as mathematical values, configuration names, or application titles.

Naming Conventions

Good variable names make code easier to read and maintain.

  • Use meaningful names
  • Avoid unnecessary abbreviations
  • Use camelCase for multi-word names
userName := "John"
totalPrice := 250

Common Beginner Mistakes

  • Using := outside functions
  • Forgetting to initialize variables properly
  • Using the wrong data type
  • Trying to change constant values

Practice Exercise

Create variables for the following:

  • Your name
  • Your age
  • Your favorite programming language
  • Whether you are learning Go

Print all values using fmt.Println().

package main

import "fmt"

func main() {
    name := "Alex"
    age := 22
    language := "Go"
    learning := true

    fmt.Println(name)
    fmt.Println(age)
    fmt.Println(language)
    fmt.Println(learning)
}

Summary

In this lecture, you learned:

  • How to declare variables in Go
  • Different Go data types
  • How zero values work
  • How constants are used
  • Best practices for naming variables
Lecture 03 Β· Fundamentals

Control Flow

Beginner ~40 min

Control flow allows your program to make decisions, repeat blocks of code, and branch into different execution paths based on conditions.

Conditional Branching: If / Else

The if` statement is the most basic way to control flow. In Go, the condition does not require parentheses.

main.go
if age >= 18 {
    fmt.Println("You are an adult.")
} else if age >= 13 {
    fmt.Println("You are a teenager.")
} else {
    fmt.Println("You are a child.")
}

The "If" with a Short Statement

Go allows a unique feature where you can declare a local variable right inside the if` statement. This variable is only available within the scope of that if/else block.

main.go
if val := getNumber(); val > 10 {
    fmt.Println("Value is high: ", val)
} else {
    fmt.Println("Value is low: ", val)
}

The Switch Statement

A switch` statement is a cleaner way to write multiple if/else` conditions. In Go, the switch breaks automaticallyβ€”you don't need to write break` after every case.

main.go
switch day := "Monday"; day {
case "Monday":
    fmt.Println("Start of the work week!")
case "Friday":
    fmt.Println("Weekend is almost here!")
case "Saturday", "Sunday":
    fmt.Println("It's the weekend!")
default:
    fmt.Println("Just another mid-week day.")
}

The Only Loop: For

Go keeps it simple: it only has one looping construct, the for` loop. However, it is incredibly versatile and can be used in three different ways.

1. Standard C-Style Loop

Used when you know exactly how many times you want to iterate.

main.go
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

2. While-Style Loop

Go doesn't have a while` keyword. Instead, you just omit the initialization and post-statement parts of the for` loop.

main.go
// while-style loop
n := 1
for n < 100 {
    n *= 2
    fmt.Println(n)
}

3. Infinite Loop

If you omit all conditions, the loop runs forever until you manually break` or return` from the function.

main.go
for {
    fmt.Println("This runs forever...")
    break // Stop the loop immediately
}

Loop Control: Break & Continue

  • break: Immediately exits the loop.
  • continue: Skips the rest of the current iteration and jumps to the next loop cycle.
🎯 Exercise 3.1

Write a program that prints numbers from 1 to 20. However:

  • If the number is divisible by 3, print "Fizz" instead of the number.
  • If the number is divisible by 5, print "Buzz".
  • If divisible by both, print "FizzBuzz".
Use a for` loop and if/else` logic.

Lecture 04 Β· Fundamentals

Functions & Multiple Returns

Beginner ~50 min

Introduction to Functions

Functions are reusable blocks of code designed to perform a specific task. They help organize programs into smaller, manageable sections and reduce code repetition.

Instead of writing the same code multiple times, you can place it inside a function and call it whenever needed.

Basic Function Syntax

In Go, functions are declared using the func keyword.

func greet() {
    fmt.Println("Welcome to Go!")
}

To execute the function, call it by its name.

greet()

Functions with Parameters

Parameters allow functions to accept input values.

func greetUser(name string) {
    fmt.Println("Hello", name)
}

Calling the function:

greetUser("Alice")

Output:

Hello Alice

Functions with Return Values

Functions can return data back to the caller using the return keyword.

func add(a, b int) int {
    return a + b
}

Using the returned value:

result := add(5, 3)
fmt.Println(result)

Why Functions Matter

Functions improve:

  • Code reusability
  • Program organization
  • Readability
  • Debugging and maintenance

Large applications rely heavily on functions to separate logic into smaller units.

Multiple Return Values

One of Go's most powerful features is the ability for functions to return multiple values.

This is commonly used for returning a result together with an error value.

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

    return a / b, nil
}

Understanding the Function

In the example above:

  • The function accepts two float64 numbers
  • It returns two values:
    • A division result
    • An error value
  • If division by zero occurs, an error is returned
  • If successful, the error value becomes nil

Using Multiple Return Values

When calling a function with multiple returns, store each value in separate variables.

result, err := divide(10, 2)

if err != nil {
    fmt.Println(err)
} else {
    fmt.Println(result)
}

Output:

5

Handling Errors Properly

Error handling is extremely important in Go programming.

Instead of using exceptions like some other languages, Go encourages developers to explicitly check errors.

result, err := divide(5, 0)

if err != nil {
    fmt.Println("Error:", err)
    return
}

fmt.Println(result)

Output:

Error: cannot divide by zero

Ignoring Return Values

Sometimes you may only need one of the returned values. Go allows unused values to be ignored using the blank identifier _.

result, _ := divide(20, 4)
fmt.Println(result)

The underscore tells Go to ignore the second return value.

Named Return Values

Go also supports named return variables.

func rectangle(width, height int) (area int) {
    area = width * height
    return
}

This style can make some functions easier to read, but overusing it may reduce clarity.

Variadic Functions

Variadic functions can accept a variable number of arguments.

func sum(numbers ...int) int {
    total := 0

    for _, num := range numbers {
        total += num
    }

    return total
}

Calling the function:

fmt.Println(sum(1, 2, 3, 4))

Output:

10

Anonymous Functions

Go supports functions without names, called anonymous functions.

message := func() {
    fmt.Println("Anonymous function executed")
}

message()

Anonymous functions are useful for short tasks and callbacks.

Scope of Variables

Variables declared inside a function are local to that function.

func test() {
    number := 10
    fmt.Println(number)
}

The variable number cannot be accessed outside the function.

Common Beginner Mistakes

  • Forgetting to return values
  • Ignoring important errors
  • Using wrong parameter types
  • Declaring variables that are never used
  • Confusing local and global variables

Practice Exercise

Create a function called multiply that accepts two integers and returns their product.

Then create another function called subtract that returns both the result and an error if the result becomes negative.

func multiply(a, b int) int {
    return a * b
}

Summary

In this lecture, you learned:

  • How functions work in Go
  • How to use parameters and return values
  • How multiple return values work
  • How Go handles errors
  • How variadic and anonymous functions operate
Lecture 05 Β· Fundamentals

Pointers & Structs

Intermediate ~55 min

Introduction to Structs

Structs are the primary way to create custom data types in Go. They allow you to group related data together under a single name, similar to objects or classes in other programming languages.

Structs are widely used for:

  • Representing users, products, and database records
  • Building APIs and web applications
  • Organizing related data cleanly
  • Creating reusable program models

Structs

A struct is defined using the type and struct keywords.

type User struct {
    ID    int
    Name  string
    Email string
    Age   int
}

Each field inside the struct has:

  • A field name
  • A data type

Creating Struct Values

You can create struct values using struct literal syntax.

var u1 User                    // zero value

u2 := User{
    ID: 1,
    Name: "Alice",
}

u3 := User{                       // order-based (not recommended)
    2,
    "Bob",
    "bob@example.com",
    28,
}

Zero Values in Structs

When a struct is created without assigning values, Go automatically assigns zero values.

Type Zero Value
int 0
string ""
bool false
pointer nil

Accessing and Modifying Fields

Struct fields are accessed using dot notation.

u := User{
    ID: 1,
    Name: "Alice",
}

fmt.Println(u.Name)     // Alice

u.Age = 25              // modify field

fmt.Println(u)          // {1 Alice  25}

Comparing Structs

Structs with comparable fields can be compared directly using ==.

u1 := User{ID: 1, Name: "Alice"}

u2 := User{ID: 1, Name: "Alice"}

fmt.Println(u1 == u2)   // true

Nested Structs

Structs can contain other structs as fields.

type Address struct {
    City    string
    Country string
}

type Employee struct {
    Name    string
    Address Address
}

Introduction to Pointers

In Go, everything is passed by value. This means copies are created when values are passed to functions.

Pointers allow us to work directly with memory addresses instead of copies.

Pointers

type User struct {
    ID   int
    Name string
}

u := User{ID: 1, Name: "Alice"}

p := &u                    // pointer to u

fmt.Println((*p).Name)    // explicit dereference

fmt.Println(p.Name)       // convenience syntax

Understanding Memory Addresses

The & operator gets the address of a variable, while the * operator accesses the value stored at that address.

x := 10

p := &x

fmt.Println(p)     // memory address

fmt.Println(*p)    // value stored at address

Creating Pointers Directly

You can create pointers directly using struct literals.

u := &User{
    ID: 2,
    Name: "Bob",
}

u.Name = "Robert"

fmt.Println(u.Name)   // Robert

Why Use Pointers with Structs?

  • Avoid copying large structs
  • Improve application performance
  • Allow functions to modify original data
  • Useful when sharing data across functions

Passing Structs to Functions

Passing a struct without pointers creates a copy.

func printUser(user User) {
    fmt.Println(user.Name)
}

Any modifications inside the function will not affect the original struct.

Modifying Structs with Pointers

Pointers allow functions to update the original struct value.

func updateName(user *User, newName string) {

    user.Name = newName
}

u := User{Name: "Alice"}

updateName(&u, "Alicia")

fmt.Println(u.Name)    // Alicia

Common Mistake: Forgetting Pointers

Forgetting pointers causes functions to modify copies instead of originals.

func badUpdate(user User) {

    user.Name = "Changed"
}

u := User{Name: "Alice"}

badUpdate(u)

fmt.Println(u.Name)     // Still "Alice"

Nil Pointers

A pointer that does not reference any value is called a nil pointer.

var p *User

fmt.Println(p == nil)   // true

Dereferencing a nil pointer will cause a runtime panic.

Struct Tags

Struct tags provide metadata for frameworks and libraries.

They are commonly used with:

  • JSON encoding
  • Database ORM tools
  • Validation libraries
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name,omitempty"`
    Email string `json:"-"` 
}

Embedded Structs (Composition)

Go uses composition instead of traditional inheritance. Embedded structs allow one struct to include another struct.

type Address struct {
    City    string
    Country string
}

type User struct {
    ID      int
    Name    string
    Address
}

u := User{
    Name: "Alice",
    Address: Address{
        City: "Ludhiana",
        Country: "India",
    },
}

fmt.Println(u.City)

Benefits of Composition

  • Encourages reusable code
  • Reduces tight coupling
  • Provides flexible designs
  • Preferred over inheritance in Go

Best Practices

  • Use pointers for large structs
  • Prefer named field initialization
  • Keep structs focused on one purpose
  • Use composition instead of complex nesting
  • Handle nil pointers carefully

Common Mistakes

  • Forgetting to dereference pointers
  • Passing large structs by value unnecessarily
  • Using order-based struct initialization
  • Dereferencing nil pointers
  • Overcomplicating struct designs

Mini Practice

Try creating the following:

  • A Book struct with title and author
  • A function that updates a struct using pointers
  • A nested struct example
  • A struct with JSON tags
  • An embedded struct using composition
Lecture 06 Β· Core Concepts

Arrays, Slices & Maps

Intermediate ~60 min

Introduction to Collections in Go

Go provides several built-in data structures for storing and organizing data. The most commonly used are arrays, slices, and maps.

These structures help developers:

  • Store multiple values efficiently
  • Organize related data
  • Process large collections easily
  • Build scalable applications

Arrays

Arrays are fixed-size collections of elements of the same type.

var numbers [5]int

This creates an array that can store exactly 5 integers.

Initializing Arrays

Arrays can be initialized during declaration.

numbers := [5]int{1, 2, 3, 4, 5}

Each value is stored in a numbered position called an index.

Accessing Array Elements

Array elements are accessed using indexes starting from 0.

numbers := [3]int{10, 20, 30}

fmt.Println(numbers[0])   // 10

fmt.Println(numbers[2])   // 30

Modifying Array Values

numbers := [3]int{1, 2, 3}

numbers[1] = 50

fmt.Println(numbers)

Array Length

The len() function returns the number of elements in an array.

arr := [4]int{1, 2, 3, 4}

fmt.Println(len(arr))   // 4

Looping Through Arrays

Arrays are commonly traversed using loops.

numbers := [3]int{10, 20, 30}

for i, value := range numbers {
    fmt.Println(i, value)
}

Limitations of Arrays

  • Fixed size
  • Cannot grow dynamically
  • Less flexible for real-world applications

Because of these limitations, Go developers usually prefer slices.

Slices: Dynamic Arrays

Slices are flexible and dynamic views over arrays. They are one of the most important data structures in Go.

s := []int{1, 2, 3}

s = append(s, 4)

Why Slices Are Important

  • Dynamic size
  • Easy to modify
  • Efficient memory handling
  • Widely used in Go libraries

Creating Slices

Slices can be created from arrays or directly using literals.

nums := []int{1, 2, 3, 4}

Slice Operations

Slices support many useful operations.

nums := []int{1, 2, 3, 4, 5}

fmt.Println(nums[1:4])   // [2 3 4]

Slice syntax:

  • [start:end]
  • Start index is included
  • End index is excluded

Appending Elements

The append() function adds new elements to a slice.

s := []int{1, 2}

s = append(s, 3)

s = append(s, 4, 5)

fmt.Println(s)

Slice Length and Capacity

Slices have both length and capacity.

s := []int{1, 2, 3}

fmt.Println(len(s))   // length

fmt.Println(cap(s))   // capacity

Length represents the number of elements, while capacity represents the available underlying storage.

Using make() with Slices

The make() function creates slices with predefined length and capacity.

s := make([]int, 5, 10)

fmt.Println(len(s))

fmt.Println(cap(s))

Copying Slices

The copy() function copies elements between slices.

src := []int{1, 2, 3}

dest := make([]int, len(src))

copy(dest, src)

fmt.Println(dest)

Maps: Key-Value Pairs

Maps store data as key-value pairs. They are similar to dictionaries or hash tables in other languages.

m := make(map[string]int)

m["apples"] = 5

Creating Maps

scores := map[string]int{
    "Alice": 90,
    "Bob": 85,
}

Accessing Map Values

fmt.Println(scores["Alice"])

Checking If a Key Exists

Go provides a special syntax for checking map keys.

value, exists := scores["John"]

fmt.Println(value)

fmt.Println(exists)

Deleting Map Entries

The delete() function removes keys from maps.

delete(scores, "Bob")

Looping Through Maps

for key, value := range scores {
    fmt.Println(key, value)
}

Nested Maps

Maps can also store other maps.

students := map[string]map[string]int{
    "Alice": {
        "Math": 95,
    },
}

Choosing Between Arrays, Slices, and Maps

Structure Use Case
Array Fixed-size collections
Slice Dynamic ordered data
Map Fast key-value lookups

Best Practices

  • Prefer slices over arrays in most applications
  • Use maps for fast lookups
  • Initialize maps before use
  • Use meaningful keys in maps
  • Avoid unnecessary slice copying

Common Mistakes

  • Accessing invalid indexes
  • Using uninitialized maps
  • Confusing length and capacity
  • Forgetting that arrays have fixed size
  • Modifying slices unexpectedly through shared backing arrays

Mini Practice

Try creating the following:

  • An array storing 5 numbers
  • A slice that grows using append()
  • A map storing student scores
  • A loop that prints slice values
  • A nested map representing user data
Lecture 07 Β· Core Concepts

Methods & Interfaces

Intermediate ~60 min

Introduction to Methods

Methods in Go are functions that are attached to a specific type. They allow you to define behavior for structs and custom types. Methods make code more organized, reusable, and easier to understand.

In many programming languages, methods belong to classes. Go does not have classes, but it allows methods to be attached directly to types.

package main

import "fmt"

type Person struct {
    Name string
}

func (p Person) Greet() {
    fmt.Println("Hello,", p.Name)
}

func main() {
    user := Person{Name: "John"}
    user.Greet()
}

In the example above:

  • Person is a struct.
  • Greet() is a method attached to the Person type.
  • p Person is called the receiver.

Understanding Receivers

A receiver connects a method to a type. Go supports two kinds of receivers:

  • Value Receivers
  • Pointer Receivers

Value Receiver

A value receiver works with a copy of the original value. Changes made inside the method do not affect the original data.

type Counter struct {
    Value int
}

func (c Counter) Increment() {
    c.Value++
}

Even after calling Increment(), the original value remains unchanged because Go passes a copy.

Pointer Receiver

A pointer receiver allows the method to modify the original struct value.

func (c *Counter) Increment() {
    c.Value++
}

Pointer receivers are commonly used when:

  • You want to modify the original object.
  • The struct is large and copying it would waste memory.

Methods on Custom Types

Methods are not limited to structs. You can attach methods to custom types as well.

type Celsius float64

func (c Celsius) ToFahrenheit() float64 {
    return (float64(c) * 9/5) + 32
}

This allows custom types to have their own behavior and utility methods.

Interfaces

Go uses implicit interfaces. If a type implements the methods, it implements the interface automatically. There is no need to explicitly declare implementation like in Java or C#.

type Shape interface {
    Area() float64
}

An interface defines a set of method signatures. Any type that contains those methods satisfies the interface.

Implementing an Interface

Let us create a rectangle type that implements the Shape interface.

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

Because Rectangle has an Area() method, it automatically satisfies the Shape interface.

Multiple Types Using One Interface

Interfaces become powerful when different types share the same behavior.

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

Now both Rectangle and Circle satisfy the same interface.

func PrintArea(s Shape) {
    fmt.Println("Area:", s.Area())
}

The function above accepts any type that implements the Shape interface.

Why Interfaces Are Important

Interfaces are heavily used in Go because they:

  • Promote reusable code.
  • Allow flexible program design.
  • Reduce tight coupling between components.
  • Make testing easier using mock implementations.
  • Enable polymorphism.

The Empty Interface

The empty interface can hold values of any type because it defines zero methods.

var data interface{}

data = 42
data = "Hello"
data = true

Before Go introduced generics, the empty interface was widely used for handling unknown data types.

Type Assertions

Type assertions allow you to retrieve the concrete value stored inside an interface.

var value interface{} = "Go"

str := value.(string)

fmt.Println(str)

You can also safely check types using the two-value form.

str, ok := value.(string)

if ok {
    fmt.Println("Value:", str)
}

Type Switches

Type switches make it easier to work with multiple possible types stored inside interfaces.

switch v := value.(type) {
case string:
    fmt.Println("String:", v)

case int:
    fmt.Println("Integer:", v)

default:
    fmt.Println("Unknown type")
}

Real-World Usage of Interfaces

Interfaces are everywhere in Go’s standard library. Examples include:

  • io.Reader
  • io.Writer
  • fmt.Stringer
  • http.Handler

These interfaces allow completely different types to work together through shared behavior.

Best Practices

  • Keep interfaces small and focused.
  • Use pointer receivers when modifying data.
  • Name interfaces based on behavior.
  • Do not create interfaces too early.
  • Prefer composition over complex inheritance patterns.

Summary

Methods define behavior for types, while interfaces define shared behavior between multiple types. Together, they form the foundation of abstraction and reusable design in Go applications.

Understanding methods and interfaces is essential for building scalable applications, APIs, concurrent systems, and production-level Go software.

Lecture 08 Β· Core Concepts

Error Handling

Intermediate ~40 min

Introduction to Error Handling

Error handling is one of the most important concepts in Go. Unlike many programming languages that use exceptions, Go uses explicit error checking. This approach makes programs easier to understand and more predictable.

In Go, functions commonly return two values:

  • The actual result.
  • An error value.

If the error is nil, the operation succeeded. Otherwise, something went wrong.

The Idiomatic Way

Go developers follow a simple and readable pattern for handling errors.

val, err := doSomething()

if err != nil {
    // Handle error
    return err
}

This pattern is called the idiomatic way because it is the standard style used throughout Go applications and the Go standard library.

Why Go Uses Explicit Errors

Go avoids hidden control flow caused by exceptions. Instead of automatic exception handling, errors are returned as normal values.

This provides several advantages:

  • Programs become easier to debug.
  • Error handling is visible in the code.
  • Developers are forced to think about failure cases.
  • The control flow remains simple and predictable.

Creating Errors

The errors package is commonly used to create custom error messages.

package main

import (
    "errors"
    "fmt"
)

func divide(a, b float64) (float64, error) {

    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }

    return a / b, nil
}

func main() {

    result, err := divide(10, 0)

    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Println("Result:", result)
}

The errors.New() function creates a simple error value containing a message.

Using fmt.Errorf()

Sometimes you need formatted error messages that include variables or additional information.

err := fmt.Errorf("user %s not found", username)

This works similarly to fmt.Printf() but returns an error value instead of printing output.

Returning Multiple Values

Go functions frequently return multiple values. Error handling is one of the biggest reasons for this design.

func getUser(id int) (string, error) {

    if id == 0 {
        return "", errors.New("invalid user id")
    }

    return "John", nil
}

The first value is the actual result, while the second value reports whether the operation failed.

Ignoring Errors

Sometimes developers intentionally ignore returned values using the blank identifier _.

value, _ := doSomething()

However, ignoring errors is generally discouraged unless you are absolutely certain the error does not matter.

Panic

Go provides the panic() function for unrecoverable situations. A panic immediately stops normal execution of the program.

panic("something went terribly wrong")

Panic should be used carefully. Most errors should be handled normally instead of crashing the program.

Recover

The recover() function allows a program to regain control after a panic. It is commonly used with deferred functions.

func safe() {

    defer func() {

        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }

    }()

    panic("unexpected error")
}

This prevents the application from crashing completely.

Deferred Cleanup

The defer keyword is frequently used during error handling to ensure cleanup code always runs.

file, err := os.Open("data.txt")

if err != nil {
    return err
}

defer file.Close()

Even if an error occurs later, the file will still be closed properly.

Custom Error Types

Go allows developers to create custom error types by implementing the Error() method.

type ValidationError struct {
    Message string
}

func (v ValidationError) Error() string {
    return v.Message
}

Any type that implements the Error() method satisfies the built-in error interface.

Error Wrapping

Modern Go applications often wrap errors to preserve context and debugging information.

if err != nil {
    return fmt.Errorf("failed to load config: %w", err)
}

The %w verb wraps the original error so it can still be inspected later.

Checking Wrapped Errors

The errors.Is() function checks whether an error matches another error.

if errors.Is(err, os.ErrNotExist) {
    fmt.Println("File does not exist")
}

This is useful when working with wrapped or nested errors.

Best Practices

  • Always check returned errors.
  • Keep error messages clear and meaningful.
  • Avoid using panic for normal application logic.
  • Use defer for cleanup operations.
  • Wrap errors when adding additional context.
  • Return errors instead of hiding them.

Common Beginner Mistakes

  • Ignoring errors completely.
  • Using panic too often.
  • Returning vague error messages.
  • Forgetting to close files or connections.
  • Not checking for nil values.

Summary

Error handling in Go is simple, explicit, and reliable. Instead of exceptions, Go treats errors as regular values that developers handle directly.

Mastering error handling is essential for building stable APIs, command-line tools, servers, and production-level Go applications.

Lecture 09 Β· Core Concepts

Generics

Intermediate ~45 min

Introduction to Generics

Generics allow you to write flexible and reusable code that works with multiple data types without duplicating logic. Before generics were introduced in Go, developers often had to write separate functions for different types such as int, float64, and string.

Generics solve this problem by allowing functions, structs, and data structures to work with type parameters.

This feature was officially introduced in Go 1.18 and became one of the biggest additions to the language.

Why Generics Are Important

Without generics, developers usually:

  • Repeated the same code for multiple types.
  • Used empty interfaces which reduced type safety.
  • Wrote large amounts of boilerplate code.

Generics make code:

  • Reusable
  • Cleaner
  • Safer
  • More maintainable
  • Easier to scale

Understanding Type Parameters

Type parameters are placeholders for actual types. They are written inside square brackets [].

func Print[T any](value T) {
    fmt.Println(value)
}

In this example:

  • T is a type parameter.
  • any means the function accepts any type.

The function can now work with integers, strings, floats, booleans, and custom structs.

Print(10)
Print("Hello")
Print(true)

Generic Functions

Generic functions are functions that work with multiple data types using type parameters.

func Sum[T int | float64](nums []T) T {

    var total T

    for _, n := range nums {
        total += n
    }

    return total
}

fmt.Println(Sum([]int{1, 2, 3}))      // 6
fmt.Println(Sum([]float64{1.5, 2.5})) // 4

The function above works for both integer slices and floating-point slices.

Instead of writing two separate functions, generics allow one reusable solution.

How Type Inference Works

Go can automatically detect the type parameter based on the values passed into the function.

result := Sum([]int{1, 2, 3})

Go automatically understands that T is int.

You can also specify the type manually.

result := Sum[int]([]int{1, 2, 3})

Type Constraints

Type constraints define which types are allowed for a generic parameter.

type Number interface {
    int | int64 | float32 | float64
}

func Max[T Number](a, b T) T {

    if a > b {
        return a
    }

    return b
}

Here:

  • Number is a custom constraint.
  • The function only accepts numeric types.

This prevents invalid types from being used.

The any Keyword

The any keyword is simply an alias for interface{}.

func Display[T any](data T) {
    fmt.Println(data)
}

It means the generic parameter can accept any possible type.

Generic Types

Generics are not limited to functions. Structs and custom types can also use type parameters.

type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(x T) {
    s.items = append(s.items, x)
}

func (s *Stack[T]) Pop() T {

    n := len(s.items) - 1

    x := s.items[n]

    s.items = s.items[:n]

    return x
}

This stack can now store any type:

  • Integers
  • Strings
  • Booleans
  • Structs
var nums Stack[int]
nums.Push(10)

var names Stack[string]
names.Push("Alice")

Generic Data Structures

Generics are especially useful for reusable data structures such as:

  • Stacks
  • Queues
  • Linked Lists
  • Trees
  • Hash Maps

Without generics, developers often needed to duplicate these structures for each data type.

Using Comparable Types

Go provides the built-in comparable constraint for types that support equality operations.

func Equal[T comparable](a, b T) bool {
    return a == b
}

This function works with types that can be compared using ==.

Combining Interfaces and Generics

Generics and interfaces can work together to create powerful abstractions.

type Printer interface {
    Print()
}

func Show[T Printer](item T) {
    item.Print()
}

Any type that implements the Print() method can be used with the generic function.

Benefits of Generics

  • Reduces duplicated code.
  • Improves type safety.
  • Makes reusable libraries easier to build.
  • Provides cleaner APIs.
  • Eliminates unnecessary type casting.

Limitations of Generics

Although generics are powerful, they should not be overused.

  • Some code becomes harder to read.
  • Complex constraints can confuse beginners.
  • Not every function needs generics.

Use generics only when they genuinely improve code reuse and maintainability.

Common Beginner Mistakes

  • Using generics for very simple functions.
  • Creating overly complex constraints.
  • Confusing interfaces with generics.
  • Forgetting that type inference exists.
  • Using any when stricter constraints are better.

Real-World Use Cases

Generics are commonly used in:

  • Utility libraries
  • Database query builders
  • Reusable collections
  • Frameworks
  • Data processing systems
  • Algorithms and sorting utilities

Practice

Practice

Write a generic Filter function that works for any slice type.

Your function should:

  • Accept a slice of any type.
  • Accept a condition function.
  • Return only matching elements.
func Filter[T any](items []T, fn func(T) bool) []T {

    var result []T

    for _, item := range items {

        if fn(item) {
            result = append(result, item)
        }

    }

    return result
}

Summary

Generics allow Go developers to write reusable, type-safe, and maintainable code without duplication. By using type parameters and constraints, you can create flexible functions and data structures that work across many data types.

Understanding generics is important for modern Go development, especially when building reusable libraries, scalable systems, and production-grade applications.

Lecture 10 Β· Concurrency

Concurrency & Goroutines

Advanced ~60 min

Goroutines

Use the go keyword to run a function concurrently.

go sayHello()
Lecture 11 Β· Concurrency

Channels & Synchronization

Advanced ~65 min

Channels

Channels are the pipes that connect concurrent goroutines.

ch := make(chan int)
ch <- 42 // Send
val := <-ch // Receive
Lecture 12 Β· Concurrency

Context & Cancellation

Advanced~45 min

Context Basics

import "context"

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

select {
case <-time.After(3 * time.Second):
    fmt.Println("done")
case <-ctx.Done():
    fmt.Println("timeout:", ctx.Err())
}

WithCancel

ctx, cancel := context.WithCancel(context.Background())
go func() {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            doWork()
        }
    }
}()
time.Sleep(1 * time.Second)
cancel() // signal goroutine to stop

Passing Values

ctx := context.WithValue(context.Background(), "userID", 42)
id := ctx.Value("userID").(int)
Practice

Build an HTTP client that cancels requests after 5 seconds using context.

Lecture 13 Β· Production

Modules & Testing

Intermediate ~50 min

Writing Tests

func TestAdd(t *testing.T) {
    if Add(1, 2) != 3 {
        t.Errorf("Failed!")
    }
}
Lecture 14 Β· Production

HTTP Servers

Advanced~45 min

Basic HTTP Server

package main
import (
    "fmt"
    "net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
}
func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

JSON Responses

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}
func userHandler(w http.ResponseWriter, r *http.Request) {
    u := User{Name: "Ahmed", Age: 25}
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(u)
}

Middleware

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println(r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}
Practice

Build a simple URL shortener with in-memory storage.

Lecture 15 Β· Production

REST API with Gin

Advanced~45 min

Setup

go get -u github.com/gin-gonic/gin

Basic Server

package main
import "github.com/gin-gonic/gin"
func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })
    r.Run(":8080")
}

CRUD Routes

type Book struct {
    ID    string `json:"id"`
    Title string `json:"title"`
}
var books []Book

r.GET("/books", func(c *gin.Context) {
    c.JSON(200, books)
})
r.POST("/books", func(c *gin.Context) {
    var b Book
    if err := c.BindJSON(&b); err != nil { return }
    books = append(books, b)
    c.JSON(201, b)
})
r.GET("/books/:id", func(c *gin.Context) {
    id := c.Param("id")
    // find and return
})
Practice

Build a full REST API for a todo list using Gin.

Lecture 16 Β· Production

Database with GORM

Advanced~45 min

Setup

go get gorm.io/gorm
go get gorm.io/driver/postgres

Models

type Product struct {
    gorm.Model
    Code  string
    Price uint
}

dsn := "host=localhost user=postgres dbname=shop sslmode=disable"
db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
db.AutoMigrate(&Product{})

CRUD Operations

// Create
db.Create(&Product{Code: "P1", Price: 100})

// Read
var p Product
db.First(&p, 1)
db.Where("code = ?", "P1").First(&p)

// Update
db.Model(&p).Update("Price", 200)

// Delete
db.Delete(&p, 1)

Relationships

type User struct {
    gorm.Model
    Name   string
    Orders []Order // has many
}
type Order struct {
    gorm.Model
    UserID uint
    Total  float64
}
db.Preload("Orders").Find(&users)
Practice

Build a blog API with User-Post-Comment relationships using GORM.

Lecture 17 Β· Production

Microservices in Go

Advanced~45 min

Why Microservices?

Microservices split a large app into small, independently deployable services that communicate via HTTP/gRPC. Go's small binaries and concurrency model make it ideal.

gRPC Service

// users.proto
service UserService {
    rpc GetUser(UserRequest) returns (UserResponse);
}
message UserRequest { int32 id = 1; }
message UserResponse { string name = 1; }
// server.go
func (s *server) GetUser(ctx context.Context, req *pb.UserRequest) (*pb.UserResponse, error) {
    return &pb.UserResponse{Name: "Ahmed"}, nil
}
func main() {
    lis, _ := net.Listen("tcp", ":50051")
    s := grpc.NewServer()
    pb.RegisterUserServiceServer(s, &server{})
    s.Serve(lis)
}

Docker Deployment

# Dockerfile
FROM golang:1.22-alpine AS build
WORKDIR /app
COPY . .
RUN go build -o myapp

FROM alpine
COPY --from=build /app/myapp /myapp
ENTRYPOINT ["/myapp"]

Service Discovery

Use Consul, etcd, or Kubernetes DNS for services to find each other dynamically.

Practice

Build 2 microservices (auth + products) that communicate via gRPC. Containerize with Docker.

Lecture 18 Β· Capstone Project

Capstone: Concurrent CLI Tool

Advanced ~120 min

Build a tool that fetches data from multiple APIs concurrently using goroutines and channels.

// Requirements:
// 1. Use net/http to fetch data
// 2. Use goroutines for parallel requests
// 3. Collect results via channels
// 4. Use context for timeouts