The Complete Go (Golang) Guide

From zero to production — every concept, with code. After this, you jump straight into Gin.

01 Why Go?

Go was created at Google in 2007 by Robert Griesemer, Rob Pike, and Ken Thompson. It was designed to solve real problems:

Go's 25 Keywords
break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var

Go is used by: Docker, Kubernetes, Terraform, Prometheus, CockroachDB, Hugo, Caddy, Uber, Twitch, Dropbox, Cloudflare.

02 Setup & Hello World

Installation

# macOS
brew install go  # installs Go via Homebrew

# Linux
wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz  # download Go tarball
sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz  # extract to /usr/local
export PATH=$PATH:/usr/local/go/bin  # add go binary to PATH

# Verify
go version   # go version go1.22.0 darwin/arm64

Your First Program

package main  // every file must declare its package

import "fmt"  // import the fmt (format) package

func main() {  // program entry point
    fmt.Println("Hello, World!")  // print with newline
}
# Run it
go run main.go  # compile + run in one step

# Build a binary
go build -o myapp main.go  # -o names the output binary
./myapp  # execute the compiled binary
Key Rules
  • Every Go file starts with package declaration
  • The main package with func main() is the entry point
  • Unused imports = compile error (Go is strict!)
  • Unused variables = compile error
  • Opening brace { MUST be on the same line (not next line)
  • No semicolons needed (compiler inserts them)

03 Modules & Packages

Creating a Module

# Initialize a new module
mkdir myproject && cd myproject  # create and enter project dir
go mod init github.com/yourname/myproject  # creates go.mod with module path

# This creates go.mod file

go.mod file

module github.com/yourname/myproject  // unique module path

go 1.22  // minimum Go version required

require (  // external dependencies
    github.com/gin-gonic/gin v1.9.1  // web framework
    github.com/go-sql-driver/mysql v1.7.1  // MySQL driver
)

Package System — File Structure

myproject/ ├── go.mod ← module definition ├── go.sum ← dependency checksums (auto-generated) ├── main.go ← package main, entry point ├── internal/ ← private to this module (CANNOT be imported by others) │ └── database/ │ └── db.go ← package database ├── pkg/ ← public packages (CAN be imported by others) │ ├── models/ │ │ ├── user.go ← package models │ │ └── product.go ← package models (same package, same folder) │ └── utils/ │ └── helpers.go ← package utils ├── cmd/ ← multiple entry points │ ├── server/ │ │ └── main.go ← package main │ └── worker/ │ └── main.go ← package main └── configs/ └── config.yaml
Package Rules
  • One folder = one package (all .go files in a folder must have the same package name)
  • Exported = starts with Uppercase (e.g., User, GetName())
  • Unexported = starts with lowercase (e.g., user, getName()) — private to package
  • internal/ packages can only be imported by code within the parent directory

Using Packages

// pkg/models/user.go
package models  // folder name = package name

type User struct {  // Uppercase = exported type
    ID   int  // Uppercase = exported field
    Name string  // accessible outside package
    age  int    // lowercase = unexported, private to this package
}

// Exported function — accessible from outside
func NewUser(id int, name string, age int) User {
    return User{ID: id, Name: name, age: age}  // set unexported field here
}

// unexported function — only accessible within 'models' package
func validate(u User) bool {
    return u.age > 0  // age accessible inside package
}
// main.go
package main  // entry point package

import (  // grouped imports
    "fmt"  // standard library
    "github.com/yourname/myproject/pkg/models"  // our package
)

func main() {
    u := models.NewUser(1, "Yatin", 25)  // create user via constructor
    fmt.Println(u.Name)  // OK: Name is exported
    // fmt.Println(u.age) // ERROR: age is unexported
}

04 Variables & Types

Declaring Variables

package main  // declares this file belongs to main

import "fmt"  // needed for Println

func main() {
    // Method 1: var keyword (can be used anywhere)
    var name string = "Yatin"  // explicit type + value
    var age int = 25  // explicit int type

    // Method 2: type inference
    var city = "Delhi"  // Go infers string

    // Method 3: short declaration := (ONLY inside functions)
    country := "India"  // most common style
    pi := 3.14         // inferred as float64
    isActive := true    // inferred as bool

    // Method 4: multiple declarations
    var (
        x int     = 10  // grouped vars are easier to read
        y float64 = 20.5
        z string  = "hello"
    )

    // Multiple short declarations
    a, b, c := 1, "two", 3.0  // each gets its own type

    fmt.Println(name, age, city, country, pi, isActive, x, y, z, a, b, c)  // use all vars
}

Zero Values (defaults when not initialized)

TypeZero ValueExample
int, int8, int16, int32, int640var x int → 0
uint, uint8...uint640var x uint → 0
float32, float640.0var x float64 → 0
boolfalsevar x bool → false
string""var x string → ""
pointer, slice, map, channel, func, interfacenilvar x []int → nil

Type Conversions (Go has NO implicit conversions)

func main() {
    var i int = 42  // start with an int
    var f float64 = float64(i)     // explicit conversion required
    var u uint = uint(f)           // explicit

    // String conversions
    s := strconv.Itoa(42)          // int → string: "42"
    n, _ := strconv.Atoi("42")     // string → int: 42
    fs := fmt.Sprintf("%.2f", 3.14) // float → string: "3.14"

    // byte slice ↔ string
    bytes := []byte("hello")       // string → []byte
    str := string(bytes)              // []byte → string
    _ = i; _ = f; _ = u; _ = s; _ = n; _ = fs; _ = str  // suppress unused-var errors
}

Type Aliases & Custom Types

// Custom type — creates a NEW type
type UserID int64  // prevents mixing IDs with plain ints
type Celsius float64  // named type for type safety
type Fahrenheit float64  // separate type from Celsius

func main() {
    var id UserID = 12345  // can't accidentally pass as plain int64
    var temp Celsius = 36.6  // body temperature in Celsius

    // Cannot mix types even if underlying type is same
    // var f Fahrenheit = temp  // ERROR!
    var f Fahrenheit = Fahrenheit(temp*9/5 + 32)  // convert formula
    _ = id; _ = f  // discard to avoid unused-var error
}

// Type alias — just another name (same type)
type byte = uint8   // this is how byte is defined in Go
type rune = int32   // this is how rune is defined in Go

05 Constants & Iota

const Pi = 3.14159  // untyped constant, very precise
const AppName = "MyApp"  // string constant, immutable

// Group of constants
const (
    StatusOK       = 200  // HTTP 200 OK
    StatusNotFound = 404  // HTTP 404 Not Found
    StatusError    = 500  // HTTP 500 Server Error
)

// iota — auto-incrementing constant generator (starts at 0)
type Weekday int  // custom int type for weekdays

const (
    Sunday    Weekday = iota  // 0
    Monday                      // 1
    Tuesday                     // 2
    Wednesday                   // 3
    Thursday                    // 4
    Friday                      // 5
    Saturday                    // 6
)

// iota with bit shifting — permission flags
type Permission uint  // bitmask type for permissions

const (
    Read    Permission = 1 << iota  // 1  (001)
    Write                              // 2  (010)
    Execute                            // 4  (100)
)

func main() {
    myPerms := Read | Write  // 3 (011)
    if myPerms&Read != 0 {  // bitwise AND to check flag
        fmt.Println("Has read permission")
    }
}

06 Operators

// Arithmetic: +  -  *  /  %
// Comparison: ==  !=  <  >  <=  >=
// Logical:    &&  ||  !
// Bitwise:    &  |  ^  <<  >>  &^
// Assignment: =  :=  +=  -=  *=  /=  %=  &=  |=  ^=  <<=  >>=
// Address:    &  (address of)   *  (dereference)
// Channel:    <-  (send/receive)

func main() {
    // Go has NO ternary operator (no ? :) — must use if-else
    x := 10  // value to test
    var result string  // declared before if-else scope
    if x > 5 {
        result = "big"  // assign inside if-branch
    } else {
        result = "small"  // assign inside else-branch
    }

    // Bit clear (AND NOT) — unique to Go
    a := 0b1010  // binary literal: 10
    b := 0b1100  // binary literal: 12
    fmt.Printf("%04b\n", a&^b) // 0010
    _ = result  // discard to avoid unused-var error
}

07 Control Flow

If-Else (with init statement)

// Standard
if x > 10 {  // no parentheses needed around condition
    fmt.Println("big")
} else if x > 5 {  // chain more conditions
    fmt.Println("medium")
} else {  // catch-all
    fmt.Println("small")
}

// With init statement (v is scoped to the if-else block)
if v := compute(); v > 100 {  // init runs before condition
    fmt.Println("over 100:", v)
}

// Very common pattern in Go:
if err := doSomething(); err != nil {  // run & check error at once
    return err  // early return on error
}

For Loop (the ONLY loop in Go)

// Classic for
for i := 0; i < 10; i++ {  // init; condition; post
    fmt.Println(i)
}

// While-style
n := 1  // start value
for n < 100 { n *= 2 }  // only condition, like while

// Infinite loop
for {  // no condition = loops forever
    break  // use break/return to exit
}

// Range over slice
nums := []int{10, 20, 30}  // slice to iterate
for i, v := range nums { fmt.Println(i, v) }  // i=index, v=value

// Range — skip index
for _, v := range nums { fmt.Println(v) }  // _ discards index

// Range over map
m := map[string]int{"a": 1, "b": 2}  // map literal
for k, v := range m { fmt.Println(k, v) }  // k=key, v=value

// Range over string (iterates runes, not bytes)
for i, ch := range "Hello" { fmt.Printf("%d: %c\n", i, ch) }  // i=byte offset, ch=rune

// Labels with break/continue
outer:  // label for the outer loop
for i := 0; i < 5; i++ {
    for j := 0; j < 5; j++ {
        if i+j == 4 { break outer }  // breaks all the way out
    }
}

Switch

// No fall-through by default (no break needed!)
switch day {  // switch on variable value
case "Mon", "Tue", "Wed", "Thu", "Fri":  // multiple values per case
    fmt.Println("Weekday")
case "Sat", "Sun":
    fmt.Println("Weekend")
default:  // runs if no case matched
    fmt.Println("Unknown")
}

// Type switch (very useful with interfaces)
switch v := i.(type) {  // i.(type) extracts dynamic type
case int:    fmt.Println("int", v)  // v is int here
case string: fmt.Println("string", v)  // v is string here
default:    fmt.Printf("unknown: %T\n", v)  // %T prints type name
}

// Expressionless switch (cleaner than if-else chains)
switch {  // no value — each case is a bool expression
case score >= 90: fmt.Println("A")
case score >= 80: fmt.Println("B")
default:          fmt.Println("F")
}

// fallthrough — explicitly fall to next case
switch 1 {  // switch on literal value 1
case 1:
    fmt.Println("one")
    fallthrough  // forces execution into case 2
case 2:
    fmt.Println("two")  // also prints
}

08 Functions

// Basic function
func add(a, b int) int { return a + b }  // a,b share type int

// Multiple return values
func divide(a, b float64) (float64, error) {  // result + error pattern
    if b == 0 { return 0, fmt.Errorf("division by zero") }  // guard clause
    return a / b, nil  // nil means no error
}

// Named return values
func swap(a, b string) (first, second string) {  // pre-declared return vars
    first = b; second = a  // assign named returns directly
    return  // naked return
}

// Variadic functions
func sum(nums ...int) int {  // ... accepts 0 or more ints
    total := 0  // accumulator variable
    for _, n := range nums { total += n }  // add each number
    return total
}
sum(1, 2, 3)  // pass multiple args directly
numbers := []int{1, 2, 3}  // existing slice
sum(numbers...)  // spread slice

// Functions are first-class — assigned to variables
double := func(n int) int { return n * 2 }  // anonymous func as var
fmt.Println(double(5))  // 10

// Function as parameter (higher-order)
func apply(nums []int, fn func(int) int) []int {  // fn is a func param
    result := make([]int, len(nums))  // pre-allocate output slice
    for i, v := range nums { result[i] = fn(v) }  // call fn on each element
    return result
}

// init() — runs before main(), used for setup
func init() {  // auto-called by runtime at startup
    // package-level vars → init() → main()
    fmt.Println("initializing...")
}

09 Arrays & Slices

Arrays (fixed size — rarely used directly)

var a [5]int                  // [0 0 0 0 0]
b := [3]string{"a", "b", "c"}  // array literal with size
c := [...]int{1, 2, 3}         // compiler counts: [3]int

// [3]int and [5]int are DIFFERENT types
// Arrays are VALUE types — assignment copies entire array
x := [3]int{1, 2, 3}  // original array
y := x           // full copy
y[0] = 99  // modify the copy only
fmt.Println(x)  // [1 2 3]  — unchanged!
_ = a; _ = b; _ = c  // suppress unused-var errors

Slices (dynamic — 99% of the time)

// A slice has: pointer, length, capacity
s1 := []int{1, 2, 3}           // literal
s2 := make([]int, 5)            // len=5, cap=5, zeros
s3 := make([]int, 3, 10)        // len=3, cap=10
var s4 []int                      // nil slice

// Append
s1 = append(s1, 4, 5)          // [1 2 3 4 5]
s1 = append(s1, s2...)           // append another slice

// Slicing (shares same underlying array!)
nums := []int{0, 1, 2, 3, 4, 5}  // base slice
a := nums[1:4]    // [1 2 3]
b := nums[:3]      // [0 1 2]
c := nums[3:]      // [3 4 5]

// True copy:
copySlice := make([]int, len(nums))  // allocate new backing array
copy(copySlice, nums)  // copy src into dst

// Delete element at index i
i := 2  // index to remove
nums = append(nums[:i], nums[i+1:]...)  // stitch around index

// Sort
sort.Ints(nums)  // sort ints in-place ascending
sort.Slice(people, func(i, j int) bool {  // custom sort via less-fn
    return people[i].Age < people[j].Age  // sort by Age ascending
})
_ = s3; _ = s4; _ = a; _ = b; _ = c  // suppress unused-var errors
Slice Internals (important for interviews)

A slice header is: { ptr *Array, len int, cap int }

When append exceeds capacity, Go allocates a new array (2x for small, 1.25x for large) and copies elements. Always use: s = append(s, x)

10 Maps

m1 := map[string]int{"apple": 5, "banana": 3}  // map literal
m2 := make(map[string]int)  // empty, ready to use
var m3 map[string]int  // nil map — PANICS on write!

// CRUD
m1["cherry"] = 7               // create/update
val := m1["apple"]              // read
delete(m1, "banana")            // delete

// Check if key exists (comma ok idiom)
if v, ok := m1["grape"]; ok {  // ok is false if key missing
    fmt.Println("found:", v)
}

// Iterate (order NOT guaranteed)
for k, v := range m1 { fmt.Println(k, v) }  // random order each run

// Map of slices
graph := map[string][]string{  // adjacency list pattern
    "a": {"b", "c"},  // a connects to b and c
    "b": {"d"},  // b connects to d
}
// Maps are NOT safe for concurrent use! Use sync.Map or mutex
_ = m2; _ = m3; _ = val; _ = graph  // suppress unused-var errors

11 Strings & Runes

// Strings are immutable byte slices
emoji := "Go is 🔥"  // emoji is multi-byte (UTF-8)
fmt.Println(len(emoji))           // 10 (bytes!)
fmt.Println(len([]rune(emoji)))   // 7 (characters)

// String builder (efficient concatenation)
var sb strings.Builder  // avoids repeated allocs
for i := 0; i < 1000; i++ { sb.WriteString("hello ") }  // append to buffer
result := sb.String()  // convert buffer to string

// strings package essentials
strings.Contains(s, "World")       // true
strings.HasPrefix(s, "Hello")      // true
strings.HasSuffix(s, "!")          // true
strings.ToUpper(s)                 // "HELLO, WORLD!"
strings.TrimSpace("  hi  ")       // "hi"
strings.Split("a,b,c", ",")        // ["a","b","c"]
strings.Join([]string{"a","b"}, "-") // "a-b"
strings.ReplaceAll(s, "l", "L")   // "HeLLo, WorLd!"
strings.Repeat("Go", 3)            // "GoGoGo"

// Raw strings (backticks)
raw := `no \n escaping here`  // useful for regex, JSON, HTML

// Sprintf for formatting
msg := fmt.Sprintf("Name: %s, Age: %d", "Yatin", 25)  // build formatted string
_ = result; _ = raw; _ = msg  // suppress unused-var errors

12 Structs

type User struct {  // groups related data together
    ID        int  // unique identifier
    FirstName string
    LastName  string
    Email     string
    Age       int
    Active    bool  // zero value is false (safe default)
}

// Create
u1 := User{ID: 1, FirstName: "Yatin", Email: "y@test.com", Active: true}  // named fields
u2 := &User{FirstName: "Ptr"}   // pointer to struct
u3 := new(User)                  // *User, all zero values

// Access — auto-dereferences pointers
u2.FirstName  // same as (*u2).FirstName

// Struct tags (json, db, validate)
type Product struct {
    ID    int     `json:"id" db:"product_id"`  // maps to JSON key "id"
    Name  string  `json:"name" validate:"required"`  // validation tag
    Price float64 `json:"price,omitempty"`  // omit from JSON if zero
    SKU   string  `json:"-"`  // excluded from JSON
}

// Constructor pattern (Go has no constructors)
func NewUser(first, last, email string) *User {  // returns pointer
    return &User{FirstName: first, LastName: last, Email: email, Active: true}  // set defaults here
}

// Anonymous struct
point := struct{ X, Y int }{10, 20}  // one-off struct, no type name
_ = u1; _ = u3; _ = point  // suppress unused-var errors

13 Pointers

x := 42  // regular variable on stack
p := &x         // p is *int, points to x
fmt.Println(*p) // 42 (dereference)
*p = 100  // write through the pointer
fmt.Println(x)  // 100 — changed through pointer!

// nil pointer
var np *int       // nil
// *np  → PANIC: nil pointer dereference!

// Go has NO pointer arithmetic (unlike C)

// Pass by value vs pointer
func doubleValue(n int)   { n *= 2 }   // copy, original unchanged
func doublePtr(n *int)    { *n *= 2 }  // modifies original

// When to use pointers:
// 1. Mutate the argument
// 2. Large structs (avoid copying)
// 3. Represent "absence" (nil)
// When NOT: small structs, slices/maps/channels (already refs)
_ = np  // suppress unused-var error

14 Methods & Interfaces

Methods

type Rectangle struct { Width, Height float64 }  // simple data struct

// Value receiver — gets a COPY
func (r Rectangle) Area() float64 { return r.Width * r.Height }  // read-only, safe

// Pointer receiver — can MODIFY the struct
func (r *Rectangle) Scale(factor float64) {  // r points to original
    r.Width *= factor  // mutate in-place
    r.Height *= factor
}
// Rule: if ANY method needs pointer receiver, use pointer for ALL

Interfaces

// Interface = set of method signatures
// IMPLICITLY satisfied (no "implements" keyword)
type Shape interface {  // any type with these methods satisfies Shape
    Area() float64  // must implement Area
    Perimeter() float64  // must implement Perimeter
}

type Circle struct { Radius float64 }  // concrete type

func (c Circle) Area() float64      { return math.Pi * c.Radius * c.Radius }  // π*r²
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }  // 2πr

// Polymorphism
func PrintInfo(s Shape) {  // accepts any Shape implementation
    fmt.Printf("Area: %.2f\n", s.Area())  // calls correct method at runtime
}

// Empty interface — accepts ANY type
func printAnything(v any) { fmt.Println(v) }  // any = interface{}

// Type assertion
s, ok := i.(string)  // safe assertion

// Key stdlib interfaces:
// fmt.Stringer   → String() string
// error          → Error() string
// io.Reader      → Read(p []byte) (n int, err error)
// io.Writer      → Write(p []byte) (n int, err error)
// http.Handler   → ServeHTTP(ResponseWriter, *Request)

// Interface composition
type ReadWriter interface { io.Reader; io.Writer }  // embed two interfaces

// Best practice: accept interfaces, return structs
_ = s; _ = ok  // suppress unused-var errors
Interface Gotcha

An interface value is nil only if BOTH the type and value are nil:

var s *MyStruct = nil  // typed nil pointer
var i Shape = s  // interface holds (type=*MyStruct, value=nil)
fmt.Println(i == nil)  // false! type is *MyStruct, value is nil

15 Embedding & Composition

// Go has NO inheritance. Uses COMPOSITION via embedding.
type Animal struct { Name, Sound string }  // base struct
func (a Animal) Speak() string { return a.Name + " says " + a.Sound }  // base method

type Dog struct {
    Animal        // embedded — promotes all fields & methods
    Breed string  // Dog-specific field
}

d := Dog{Animal: Animal{"Rex", "Woof"}, Breed: "German Shepherd"}  // init embedded struct
d.Name       // "Rex" (promoted from Animal)
d.Speak()    // "Rex says Woof"

// Override methods
func (d Dog) Speak() string { return d.Name + " barks!" }  // shadows Animal.Speak

16 Error Handling

// error is an interface: Error() string
err1 := errors.New("something went wrong")  // simple static error
err2 := fmt.Errorf("user %d not found", 42)  // formatted error message

// The Go pattern: always check errors
data, err := os.ReadFile(path)  // returns ([]byte, error)
if err != nil {  // nil means success
    return nil, fmt.Errorf("reading %s: %w", path, err)  // %w wraps
}

// Custom error types
type ValidationError struct { Field, Message string }  // rich error info
func (e *ValidationError) Error() string {  // satisfies error interface
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

// Sentinel errors
var ErrNotFound = errors.New("not found")  // compare with errors.Is

// errors.Is — check through wrapping chain
if errors.Is(err, ErrNotFound) { /* handle */ }  // works through wrapped errors

// errors.As — extract specific error type
var ve *ValidationError  // target type to extract
if errors.As(err, &ve) { fmt.Println(ve.Field) }  // ve populated if matched
_ = err1; _ = err2; _ = data  // suppress unused-var errors

17 Goroutines & Channels

Goroutines

// Lightweight thread (~2KB stack vs ~1MB OS thread)
go doWork(1)  // launch goroutine

// WaitGroup — wait for goroutines to finish
var wg sync.WaitGroup  // counter: Add/Done/Wait
for i := 0; i < 5; i++ {
    wg.Add(1)  // increment counter before goroutine
    go func(id int) {  // id passed to avoid capture bug
        defer wg.Done()  // decrement when goroutine exits
        doWork(id)
    }(i)  // pass i as argument immediately
}
wg.Wait()  // block until counter hits zero
Common Goroutine Bug (pre Go 1.22)
// BUG: loop variable captured by reference
for i := 0; i < 5; i++ {
    go func() { fmt.Println(i) }()  // prints 5,5,5,5,5
}
// FIX: pass as argument
for i := 0; i < 5; i++ {
    go func(n int) { fmt.Println(n) }(i)  // copy i into n
}
// Go 1.22+: loop vars are per-iteration (fixed!)

Channels

// Unbuffered — sender blocks until receiver ready
ch := make(chan int)  // synchronous handoff
go func() { ch <- 42 }()  // goroutine sends 42
val := <-ch  // 42

// Buffered — blocks only when buffer full
bch := make(chan string, 3)  // can send 3 items without waiting

// Direction constraints
func producer(ch chan<- int) { ch <- 1 }    // send-only
func consumer(ch <-chan int) { <-ch }       // receive-only

// Range over channel (until closed)
for v := range ch { fmt.Println(v) }  // exits when ch is closed

// Worker pool pattern
jobs := make(chan int, 100)  // input queue
results := make(chan int, 100)  // output queue
for w := 0; w < 3; w++ {  // 3 worker goroutines
    go func() {
        for job := range jobs {  // read until jobs closed
            results <- job * 2  // process and send result
        }
    }()
}
_ = val; _ = bch  // suppress unused-var errors

18 Select Statement

// select — wait on multiple channel operations
select {  // blocks until one case is ready
case msg := <-ch1:  // receive from ch1
    fmt.Println(msg)
case msg := <-ch2:  // receive from ch2
    fmt.Println(msg)
case <-time.After(1 * time.Second):  // fires after 1s
    fmt.Println("timeout!")
default:  // non-blocking: runs if nothing else ready
    fmt.Println("no message ready")
}

// Ticker — periodic events
ticker := time.NewTicker(500 * time.Millisecond)  // fires every 500ms
defer ticker.Stop()  // clean up goroutine when done
for {
    select {
    case <-done: return  // exit on done signal
    case t := <-ticker.C: fmt.Println("tick", t)  // handle each tick
    }
}

19 Sync Package

// Mutex
type SafeCounter struct {
    mu sync.Mutex  // protects v from concurrent access
    v  map[string]int  // shared mutable state
}
func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()  // acquire exclusive lock
    defer c.mu.Unlock()  // always release lock
    c.v[key]++  // safe to modify now
}

// RWMutex — multiple readers OR one writer
type Cache struct { mu sync.RWMutex; data map[string]string }
func (c *Cache) Get(k string) string {
    c.mu.RLock(); defer c.mu.RUnlock()  // read lock allows concurrent reads
    return c.data[k]
}

// sync.Once — run exactly once (singleton)
var once sync.Once  // goroutine-safe one-time init
once.Do(func() { /* expensive init */ })  // only runs once ever

// sync.Pool — reuse objects
// sync.Map — concurrent map
// atomic — lock-free operations
var counter int64  // shared counter
atomic.AddInt64(&counter, 1)  // increment atomically, no mutex needed

20 Context

// Carries deadlines, cancellation, and request-scoped values
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)  // 5 second deadline
defer cancel()  // always call to free resources

select {
case <-ctx.Done():  // channel closed when cancelled/timed out
    fmt.Println("cancelled:", ctx.Err())  // Err() tells why it stopped
case result := <-doWork(ctx):  // work completed in time
    fmt.Println(result)
}

// Values (use sparingly)
type ctxKey string  // custom type avoids key collisions
ctx = context.WithValue(ctx, ctxKey("reqID"), "abc-123")  // attach request ID
Context Rules
  • Always pass context.Context as the first parameter
  • Never store context in a struct
  • Use context.Background() in main/init/tests
  • Always call cancel() via defer cancel()

21 Generics (Go 1.18+)

// Generic function
func Min[T constraints.Ordered](a, b T) T {  // T must support < operator
    if a < b { return a }  // works for any Ordered type
    return b
}
Min(3, 5)       // 3 (type inferred)
Min("a", "b")   // "a"

// Type constraints
type Number interface {
    ~int | ~int64 | ~float32 | ~float64  // ~ means "underlying type"
}

// Generic struct
type Stack[T any] struct { items []T }  // T is the element type
func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) }  // add to top
func (s *Stack[T]) Pop() (T, bool) {
    var zero T  // zero value of type T
    if len(s.items) == 0 { return zero, false }  // empty stack
    item := s.items[len(s.items)-1]  // get last element
    s.items = s.items[:len(s.items)-1]  // shrink slice
    return item, true
}

// Generic Map/Filter
func Map[T, U any](s []T, fn func(T) U) []U {  // transform T slice into U slice
    r := make([]U, len(s))  // pre-allocate result
    for i, v := range s { r[i] = fn(v) }  // apply fn to each element
    return r
}

22 Closures & Higher-Order Functions

// Closure — captures variables from outer scope
func counter() func() int {  // returns a function
    count := 0  // captured: lives as long as closure
    return func() int { count++; return count }  // closure over count
}
next := counter()  // each call gets its own count
next()  // 1
next()  // 2

// Factory pattern
func multiplier(factor int) func(int) int {  // returns configured func
    return func(n int) int { return n * factor }  // factor is captured
}
double := multiplier(2)  // specialised multiplier
double(5)  // 10

23 Defer, Panic, Recover

// defer — executes when function returns (LIFO order)
f, _ := os.Open("file.txt")  // open file, ignoring error for brevity
defer f.Close()  // guaranteed cleanup

// panic — unrecoverable error (use RARELY)
panic("something terrible happened")  // unwinds stack, prints trace

// recover — catch panics (ONLY inside deferred func)
defer func() {  // must be deferred to catch panics
    if r := recover(); r != nil {  // r is the panic value
        fmt.Println("Recovered:", r)
    }
}()

// Real-world: recovery middleware in HTTP servers
func recoveryMiddleware(next http.HandlerFunc) http.HandlerFunc {  // wraps a handler
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {  // set up panic recovery for this request
            if err := recover(); err != nil {  // caught a panic
                http.Error(w, "Internal Error", 500)  // return 500 to client
            }
        }()
        next(w, r)  // call the actual handler
    }
}

24 Linked List

type Node[T any] struct {  // generic node for any type T
    Val  T  // the data stored in this node
    Next *Node[T]  // pointer to next node (nil if last)
}

type LinkedList[T any] struct {
    Head *Node[T]  // first node (nil if empty)
    Size int  // track length
}

// Prepend — O(1)
func (ll *LinkedList[T]) Prepend(val T) {
    ll.Head = &Node[T]{Val: val, Next: ll.Head}  // new node points to old head
    ll.Size++
}

// Append — O(n)
func (ll *LinkedList[T]) Append(val T) {
    node := &Node[T]{Val: val}  // new tail node
    if ll.Head == nil { ll.Head = node; ll.Size++; return }  // empty list
    curr := ll.Head  // start traversal from head
    for curr.Next != nil { curr = curr.Next }  // walk to last node
    curr.Next = node; ll.Size++  // attach new node at end
}

// Reverse — O(n)
func (ll *LinkedList[T]) Reverse() {
    var prev *Node[T]  // becomes new tail (nil)
    curr := ll.Head  // current node being processed
    for curr != nil {
        next := curr.Next  // save next before overwriting
        curr.Next = prev  // reverse the pointer
        prev = curr  // advance prev
        curr = next  // advance curr
    }
    ll.Head = prev  // prev is now the new head
}

// Print: 0 -> 1 -> 2 -> nil
func (ll *LinkedList[T]) Print() {
    for c := ll.Head; c != nil; c = c.Next {  // walk the list
        fmt.Printf("%v -> ", c.Val)
    }
    fmt.Println("nil")  // mark end of list
}

25 Stack & Queue

// Stack (LIFO)
type Stack[T any] struct { items []T }  // backed by a slice
func (s *Stack[T]) Push(item T)    { s.items = append(s.items, item) }  // add to top
func (s *Stack[T]) IsEmpty() bool { return len(s.items) == 0 }  // check before Pop
func (s *Stack[T]) Pop() (T, bool) {
    var zero T  // safe zero to return on failure
    if s.IsEmpty() { return zero, false }  // nothing to pop
    idx := len(s.items) - 1  // index of top element
    item := s.items[idx]  // save it before shrinking
    s.items = s.items[:idx]  // remove top element
    return item, true
}

// Queue (FIFO)
type Queue[T any] struct { items []T }  // backed by a slice
func (q *Queue[T]) Enqueue(item T) { q.items = append(q.items, item) }  // add to back
func (q *Queue[T]) Dequeue() (T, bool) {
    var zero T  // safe zero to return on failure
    if len(q.items) == 0 { return zero, false }  // empty queue
    item := q.items[0]  // get front element
    q.items = q.items[1:]  // remove front element
    return item, true
}

// Classic: valid parentheses
func isValid(s string) bool {
    stack := &Stack[rune]{}  // stack of opening brackets
    pairs := map[rune]rune{')': '(', ']': '[', '}': '{'}  // closing → opening
    for _, ch := range s {  // iterate each character
        if ch == '(' || ch == '[' || ch == '{' {
            stack.Push(ch)  // push opening bracket
        } else {
            top, ok := stack.Pop()  // must match last opening
            if !ok || top != pairs[ch] { return false }  // mismatch
        }
    }
    return stack.IsEmpty()  // valid only if all brackets matched
}

26 Binary Tree / BST

type TreeNode struct {
    Val         int  // node value
    Left, Right *TreeNode  // child pointers (nil if absent)
}

// Insert — O(log n) avg
func insert(node *TreeNode, val int) *TreeNode {
    if node == nil { return &TreeNode{Val: val} }  // base: create leaf
    if val < node.Val { node.Left = insert(node.Left, val) }  // go left
    else if val > node.Val { node.Right = insert(node.Right, val) }  // go right
    return node  // return unchanged node (or duplicate)
}

// In-order (sorted output for BST)
func inOrder(n *TreeNode, res *[]int) {
    if n == nil { return }  // base case
    inOrder(n.Left, res)  // visit left subtree first
    *res = append(*res, n.Val)  // then visit current node
    inOrder(n.Right, res)  // then visit right subtree
}

// Level-order (BFS)
func levelOrder(root *TreeNode) [][]int {
    if root == nil { return nil }  // empty tree
    var result [][]int  // each inner slice is one level
    queue := []*TreeNode{root}  // BFS queue starts with root
    for len(queue) > 0 {
        sz := len(queue)  // nodes at current level
        var level []int  // collect values for this level
        for i := 0; i < sz; i++ {
            n := queue[0]; queue = queue[1:]  // dequeue front
            level = append(level, n.Val)  // record value
            if n.Left != nil  { queue = append(queue, n.Left) }  // enqueue children
            if n.Right != nil { queue = append(queue, n.Right) }
        }
        result = append(result, level)  // store completed level
    }
    return result
}

// Max depth
func maxDepth(n *TreeNode) int {
    if n == nil { return 0 }  // nil node contributes 0 depth
    l, r := maxDepth(n.Left), maxDepth(n.Right)  // depth of each subtree
    if l > r { return l + 1 }  // +1 for current node
    return r + 1
}

27 Graph

type Graph struct {
    adj      map[string][]string  // adjacency list
    directed bool  // true = directed, false = undirected
}

func (g *Graph) AddEdge(from, to string) {
    g.adj[from] = append(g.adj[from], to)  // add forward edge
    if !g.directed { g.adj[to] = append(g.adj[to], from) }  // add reverse for undirected
}

// BFS
func (g *Graph) BFS(start string) []string {
    visited := map[string]bool{start: true}  // mark start as visited
    queue := []string{start}  // BFS frontier
    var result []string  // visited order
    for len(queue) > 0 {
        node := queue[0]; queue = queue[1:]  // dequeue front
        result = append(result, node)  // record visit
        for _, nb := range g.adj[node] {
            if !visited[nb] { visited[nb] = true; queue = append(queue, nb) }  // enqueue unvisited
        }
    }
    return result
}

// DFS (recursive)
func (g *Graph) DFS(start string) []string {
    visited := make(map[string]bool)  // track visited nodes
    var result []string  // accumulate visit order
    var dfs func(string)  // declare before defining (recursive closure)
    dfs = func(n string) {
        visited[n] = true  // mark before recursing
        result = append(result, n)  // record visit
        for _, nb := range g.adj[n] {
            if !visited[nb] { dfs(nb) }  // recurse into unvisited
        }
    }
    dfs(start)  // kick off from start node
    return result
}

28 Hash Table (from scratch)

// Go maps ARE hash tables. This is for understanding.
const tableSize = 64  // number of buckets

type entry struct { key string; value int; next *entry }  // linked list node
type HashTable struct { buckets [tableSize]*entry; size int }  // fixed array of chains

func hash(key string) int {
    h := 0  // running hash value
    for _, ch := range key { h = (h*31 + int(ch)) % tableSize }  // polynomial rolling hash
    return h  // bucket index
}

func (ht *HashTable) Put(key string, val int) {
    idx := hash(key)  // compute bucket
    for curr := ht.buckets[idx]; curr != nil; curr = curr.next {
        if curr.key == key { curr.value = val; return }  // update existing
    }
    ht.buckets[idx] = &entry{key, val, ht.buckets[idx]}  // prepend to chain
    ht.size++
}

func (ht *HashTable) Get(key string) (int, bool) {
    for curr := ht.buckets[hash(key)]; curr != nil; curr = curr.next {  // walk chain
        if curr.key == key { return curr.value, true }  // found
    }
    return 0, false  // not found
}

29 I/O & Files

// Read entire file
data, err := os.ReadFile("config.json")  // returns []byte

// Write entire file
os.WriteFile("out.txt", []byte("hello"), 0644)  // 0644 = rw-r--r-- perms

// Read line by line
f, _ := os.Open("file.txt")  // open read-only
defer f.Close()  // always close the file
scanner := bufio.NewScanner(f)  // wraps file in a scanner
for scanner.Scan() { fmt.Println(scanner.Text()) }  // Text() returns current line

// Buffered write
f, _ = os.Create("out.txt")  // create or truncate file
w := bufio.NewWriter(f)  // buffer writes for efficiency
w.WriteString("line 1\n")  // write to buffer
w.Flush()  // don't forget!

// io.Copy between reader and writer
io.Copy(dst, src)  // stream bytes from src to dst

// Directory operations
os.MkdirAll("path/to/dir", 0755)  // create all parent dirs too
entries, _ := os.ReadDir(".")  // list current directory
_ = data; _ = err; _ = entries  // suppress unused-var errors

30 JSON & Encoding

type User struct {
    ID    int    `json:"id"`  // maps Go field to JSON key
    Name  string `json:"name"`
    Email string `json:"email,omitempty"`  // omit if empty string
    Pass  string `json:"-"`  // never in JSON
}

// Struct → JSON
jsonBytes, _ := json.Marshal(user)  // compact JSON bytes
jsonBytes, _ = json.MarshalIndent(user, "", "  ")  // pretty-printed JSON

// JSON → Struct
var u User  // target struct to populate
json.Unmarshal([]byte(jsonStr), &u)  // must pass pointer

// JSON → dynamic map
var data map[string]any  // use when structure is unknown
json.Unmarshal([]byte(jsonStr), &data)  // parse into map

// Streaming (for HTTP bodies)
json.NewEncoder(w).Encode(user)     // write to io.Writer
json.NewDecoder(r.Body).Decode(&u)  // read from io.Reader
_ = jsonBytes; _ = data  // suppress unused-var errors

31 HTTP & Networking

// Simple server
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello!")  // write response body
})
log.Fatal(http.ListenAndServe(":8080", nil))  // nil = use DefaultServeMux

// Go 1.22+ enhanced routing
mux := http.NewServeMux()  // custom router, not the default
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")  // extract {id} from URL
    fmt.Fprintf(w, "User: %s", id)
})
mux.HandleFunc("POST /users", createUser)  // method + path matching

// JSON API handler
func getUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")  // set before writing body
    json.NewEncoder(w).Encode(users)  // stream JSON to client
}

// HTTP Client with timeout
client := &http.Client{Timeout: 10 * time.Second}  // always set a timeout
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)  // ctx allows cancellation
req.Header.Set("Authorization", "Bearer token")  // set auth header
resp, err := client.Do(req)  // send request
defer resp.Body.Close()  // always close body to free connection
_ = err  // handle error in real code

32 Testing

myproject/ ├── math.go ← source ├── math_test.go ← tests (MUST end with _test.go) └── testdata/ ← test fixtures (ignored by build)
// Table-driven tests — THE Go pattern
func TestDivide(t *testing.T) {  // must start with Test
    tests := []struct {  // slice of anonymous structs
        name    string  // test case name
        a, b    float64  // inputs
        want    float64  // expected result
        wantErr bool  // expect an error?
    }{
        {"normal", 10, 2, 5, false},  // 10/2 = 5, no error
        {"zero div", 10, 0, 0, true},  // divide by zero, expect error
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {  // subtest for each case
            got, err := Divide(tt.a, tt.b)  // call the function
            if (err != nil) != tt.wantErr {  // error presence check
                t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
            }
            if got != tt.want {  // value check
                t.Errorf("got %v, want %v", got, tt.want)
            }
        })
    }
}

// Benchmark
func BenchmarkAdd(b *testing.B) {  // must start with Benchmark
    for i := 0; i < b.N; i++ { Add(2, 3) }  // b.N auto-tuned by framework
}

33 Reflection

import "reflect"  // runtime type inspection

t := reflect.TypeOf(u)  // get the type of u
fmt.Println(t.Name(), t.Kind(), t.NumField())  // name, kind (struct), field count

// Iterate fields + read tags
for i := 0; i < t.NumField(); i++ {
    f := t.Field(i)  // get StructField info
    fmt.Println(f.Name, f.Type, f.Tag.Get("json"))  // read json struct tag
}

// Modify via reflection (need pointer)
v := reflect.ValueOf(&u).Elem()  // Elem() dereferences pointer
v.FieldByName("Name").SetString("Updated")  // set field by name

// WARNING: Reflection is slow and bypasses type safety.
// Prefer generics for type-safe generic code.

34 Unsafe & Cgo

// Struct padding — field order matters!
type Bad struct  { a bool; b int64; c bool }  // 24 bytes
type Good struct { b int64; a bool; c bool }  // 16 bytes (33% less!)

// unsafe.Sizeof
fmt.Println(unsafe.Sizeof(int64(0)))  // 8
fmt.Println(unsafe.Sizeof(""))         // 16 (ptr + len)

// Go has NO pointer arithmetic (unlike C)
_ = Bad{}; _ = Good{}  // suppress unused-type errors

35 Design Patterns

// Singleton (sync.Once)
var dbOnce sync.Once; var db *DB  // package-level state
func GetDB() *DB {
    dbOnce.Do(func() { db = connect() })  // connect only once
    return db  // always returns same instance
}

// Options Pattern (very common)
type Option func(*Server)  // each option mutates Server
func WithPort(p int) Option          { return func(s *Server) { s.port = p } }  // set port
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }  // set timeout

func NewServer(host string, opts ...Option) *Server {
    s := &Server{host: host, port: 8080, timeout: 30*time.Second}  // sensible defaults
    for _, o := range opts { o(s) }  // apply each option
    return s
}
srv := NewServer("localhost", WithPort(3000))  // override only port

// Repository Pattern
type UserRepo interface {  // abstract over storage
    FindByID(ctx context.Context, id int) (*User, error)  // read
    Create(ctx context.Context, u *User) error  // write
}

// Middleware Chain
type Middleware func(http.Handler) http.Handler  // wraps a handler
func Chain(h http.Handler, mws ...Middleware) http.Handler {
    for i := len(mws)-1; i >= 0; i-- { h = mws[i](h) }  // apply in reverse for correct order
    return h
}
_ = srv  // suppress unused-var error

36 Production Project Structure

myapi/ ├── go.mod / go.sum / Makefile / Dockerfile ├── cmd/ ← entry points │ ├── api/main.go │ └── worker/main.go ├── internal/ ← private application code │ ├── config/ ← loads env/yaml │ ├── domain/ ← entities (no deps) │ ├── repository/ ← data access interfaces + impls │ ├── service/ ← business logic │ ├── handler/ ← HTTP handlers │ ├── middleware/ ← auth, logging, cors │ └── router/ ← route setup ├── pkg/ ← public reusable packages ├── migrations/ ← SQL files └── scripts/
Dependency Flow (Clean Architecture)

handler → service → repository → database

internal/domain has ZERO dependencies — pure Go structs and rules.

37 Performance & Profiling

// pprof — built-in profiler
import _ "net/http/pprof"  // blank import registers pprof routes
go http.ListenAndServe("localhost:6060", nil)  // start debug server

// go tool pprof http://localhost:6060/debug/pprof/heap
// go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

// Key optimizations:
// 1. Pre-allocate: make([]int, 0, 1000)
// 2. strings.Builder for concatenation
// 3. sync.Pool for hot objects
// 4. Avoid interface{}/any (heap allocations)
// 5. go build -gcflags="-m" for escape analysis
// 6. Order struct fields by size (reduce padding)
// 7. Buffered I/O: bufio.NewWriterSize(f, 64*1024)
// 8. Race detector: go run -race main.go

38 Important Commands

The commands you'll actually use every day. Learn these and you're set.

Daily Workflow
go run main.goCompile + execute in one shot. Your "run this file" button. Also go run . for all files in dir.
go build -o myappCompile into a binary. Drop -o to use default name. This is what you ship to production.
go fmt ./...Auto-format ALL your code. Go has ONE style — no debates. Run before every commit.
go vet ./...Static analysis — catches bugs the compiler misses: wrong printf verbs, unreachable code, bad struct tags, etc.
Modules & Dependencies
go mod init github.com/you/projectCreates go.mod. Run once when starting a new project. This is your package.json.
go mod tidySyncs go.mod with your actual imports — adds missing deps, removes unused. Run after adding/removing imports.
go get github.com/pkg@v1.2.3Add or update a dependency. Use @latest for newest. This is your npm install.
Testing
go test ./...Run ALL tests in all packages. The ./... means "recursively". This is the one you run most.
go test -v -run TestNameRun one specific test with verbose output. -run takes a regex.
go test -cover ./...Shows coverage %. For detailed report: -coverprofile=c.out then go tool cover -html=c.out
go test -race ./...Runs tests with the race detector. Catches concurrent data races. Use in CI — it's saved countless bugs.
go test -bench=. -benchmemRun benchmarks. -benchmem shows allocations per op. Essential for perf work.
Debugging & Profiling
go build -gcflags="-m"Escape analysis — tells you what allocates on heap vs stack. Add -m -m for verbose.
go run -race main.goRun your app with race detection. Slower but catches goroutine data races at runtime.
go tool pprof http://localhost:6060/debug/pprof/heapAnalyze memory profile of a running app (needs import _ "net/http/pprof").
Cross-Compile & Ship
GOOS=linux GOARCH=amd64 go build -o appBuild for Linux from your Mac. That's it. No Docker needed for compilation. Change GOOS/GOARCH for any target.
go build -ldflags="-s -w"Smaller binary — strips debug symbols. Typical Go binary: 10-15MB, with this: 6-8MB.
go installBuild + put the binary in $GOPATH/bin. For CLI tools you want available globally.
When Things Go Wrong
go clean -cacheNuke the build cache. Fixes weird "it should work but doesn't" issues.
go clean -testcacheClear cached test results. Use when tests pass but shouldn't (or vice versa).
go envPrint all Go env vars. Useful when GOPATH, GOROOT, or GOPROXY seems off.
go doc fmt.SprintfQuick docs lookup from terminal. Faster than googling.

39 Interview Questions

Show All

Junior Level

Ans

var x int is an explicit declaration — can be used at package level or inside functions. x := 0 is short variable declaration — can ONLY be used inside functions. Both initialize x to 0, but := also infers the type.

var x int    // explicit type, zero value (0)
x := 0       // inferred as int, value 0
x := 0.0     // inferred as float64!
Ans

Go's philosophy is to keep codebases clean. Unused variables and imports are dead code — they confuse readers, slow down compilation, and often indicate bugs. The compiler enforces this as a hard error, not a warning, to prevent code rot.

Use _ (blank identifier) when you intentionally want to discard a value:

_, err := doSomething()  // discard first return value
Ans

Every type has a zero value: int0, string"", boolfalse, pointers/slices/maps → nil. This means variables are always initialized — no undefined behavior. Many Go types are designed to be useful at their zero value (e.g., sync.Mutex{}, bytes.Buffer{}).

Ans

Array: fixed size, size is part of the type ([3]int[5]int), value type (copying copies all elements).

Slice: dynamic, reference type (header: pointer + length + capacity), backed by an array. Use slices 99% of the time.

arr := [3]int{1,2,3}  // array — fixed
slc := []int{1,2,3}   // slice — dynamic
Ans

Go treats errors as values, not exceptions. Functions return an error as the last return value. You check it explicitly with if err != nil. This is intentional — it makes error handling visible and forces you to think about every failure point. No hidden control flow.

f, err := os.Open("file.txt")
if err != nil {
    return fmt.Errorf("open file: %w", err) // wrap & propagate
}
defer f.Close()
Ans

defer schedules a function call to run when the surrounding function returns. Multiple defers execute in LIFO (last-in-first-out) order. Common uses: closing files, unlocking mutexes, flushing buffers.

mu.Lock()
defer mu.Unlock()  // guaranteed even if panic

Mid Level

Ans

Go allocates a new, larger underlying array (roughly 2x capacity for small slices, 1.25x for large), copies existing elements, then appends. This is why append returns a new slice — the pointer may have changed. Always: s = append(s, x).

If two slices shared the same array and one grows past capacity, they now point to different arrays — mutations no longer affect each other.

Ans

Value receiver (r Rect): gets a copy. Use for small, read-only structs.

Pointer receiver (r *Rect): gets a pointer, can mutate. Use when: you need to modify the struct, the struct is large, or for consistency (if one method needs pointer, use pointer for all).

Important: a value receiver can be called on a pointer, and vice versa — Go auto-converts. But an interface is satisfied differently: Shape implemented by *Rect means only *Rect (not Rect) satisfies it.

Ans

Go interfaces are implicit — a type satisfies an interface by implementing its methods, without declaring it. This enables decoupling without import dependencies. Internally, an interface value is a two-word pair: (type, value).

Best practices: keep interfaces small (1-3 methods), define them where they're used (not where implemented), and "accept interfaces, return structs."

Ans

A goroutine is a lightweight thread managed by the Go runtime. It starts with ~2KB stack (vs ~1-8MB for OS threads), is multiplexed onto OS threads by the Go scheduler (M:N scheduling), and context-switches faster since it doesn't need a syscall. You can easily run millions of goroutines.

The Go scheduler uses a work-stealing algorithm with per-P (processor) run queues for efficiency.

Ans

Unbuffered make(chan int): synchronous — sender blocks until receiver is ready. Use for synchronization / guaranteed handoff.

Buffered make(chan int, 10): sender only blocks when full. Use for: decoupling producer/consumer speeds, rate limiting, batch processing, semaphores.

Rule of thumb: start unbuffered, add buffer only when you need to decouple timing.

Ans

select blocks until one of its cases can proceed. If multiple are ready, it picks one randomly (fair scheduling). Common patterns: timeout with time.After, non-blocking with default, done channel for cancellation.

select {
case v := <-dataCh: process(v)
case <-ctx.Done(): return
case <-time.After(5*time.Second): return errTimeout
}

Senior Level

Ans

G = Goroutine (the task). M = Machine/OS thread (the executor). P = Processor (the scheduler context, holds run queue). Default GOMAXPROCS = number of CPU cores.

Each P has a local run queue of Gs. An M must acquire a P to execute Gs. When a goroutine blocks (syscall, channel, etc.), the M releases its P so another M can pick it up. Work-stealing: idle Ps steal Gs from busy Ps' queues.

This M:N model gives goroutine-level concurrency with OS-level parallelism.

Ans

Go uses a concurrent, tri-color mark-and-sweep GC. It runs concurrently with your program (mostly). Three phases: Mark Setup (STW, brief), Marking (concurrent), Mark Termination (STW, brief).

Tri-color: White (unmarked/garbage), Grey (reachable, not fully scanned), Black (reachable, fully scanned). Write barrier ensures correctness during concurrent marking.

Tuning: GOGC=100 (default) — triggers GC when heap doubles. Lower = more frequent GC, less memory. GOMEMLIMIT (Go 1.19+) sets a soft memory limit. Use GODEBUG=gctrace=1 to observe.

Ans

The compiler analyzes whether a variable can stay on the stack or must "escape" to the heap. Stack allocation is free (just move stack pointer), heap allocation requires GC.

Variables escape when: returned as pointer, stored in interface, captured by closure that outlives function, too large for stack.

Check with: go build -gcflags="-m". To optimize: return values not pointers when small, avoid unnecessary interface{}, pre-allocate slices.

Ans

Goroutine leaks happen when a goroutine blocks forever (waiting on channel, mutex, etc.). Prevention:

  • Always use context.Context for cancellation
  • Ensure channels are closed or have done signals
  • Use buffered channels when producers might exit before consumers
  • Set timeouts on operations
  • In tests, use goleak (uber-go/goleak) to detect leaks
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
select {
case r := <-work(ctx): // ...
case <-ctx.Done(): return ctx.Err()
}
Ans

errors.Is(err, target) checks if any error in the chain matches a specific value (for sentinel errors like ErrNotFound). Uses == comparison or custom Is() method.

errors.As(err, &target) checks if any error in the chain matches a specific type and extracts it (for custom error structs). Fills the target variable with the matched error.

if errors.Is(err, sql.ErrNoRows) { /* value match */ }

var ve *ValidationError
if errors.As(err, &ve) { /* type match, ve is populated */ }
Ans

Context carries: cancellation signals, deadlines/timeouts, and request-scoped values across API boundaries and goroutines. It forms a tree — cancelling a parent cancels all children.

Critical in servers: when a client disconnects, the request context is cancelled, which should propagate to all goroutines doing work for that request (DB queries, HTTP calls, etc.), preventing wasted resources.

Rules: first param, don't store in struct, always defer cancel().

Staff / VP Level

Ans

Use the pipeline pattern: chain stages connected by channels. Each stage is a goroutine (or pool) that reads from input channel, processes, writes to output channel.

Fan-out: multiple goroutines reading from same channel (parallel processing). Fan-in: merge multiple channels into one.

Key decisions: buffer sizes (tune to balance memory vs throughput), backpressure (bounded channels), cancellation (context), error handling (dedicated error channel or errgroup), graceful shutdown (close input, drain pipeline).

Use golang.org/x/sync/errgroup for managing groups of goroutines with error propagation.

Ans

CPUs read memory in word-sized chunks. Fields must be aligned to their size (int64 to 8-byte boundary). The compiler inserts padding bytes between misaligned fields.

type Bad  struct { a bool; b int64; c bool } // 24 bytes (7+7 padding)
type Good struct { b int64; a bool; c bool } // 16 bytes (6 padding)

Order fields from largest to smallest. For hot structs in slices, this saves cache lines and memory. Use fieldalignment linter or unsafe.Sizeof to check.

Ans
  1. Measure first: add pprof endpoints, collect CPU + heap profiles under realistic load
  2. CPU profile: go tool pprof → find hot functions, optimize algorithms
  3. Memory profile: heap profile → find allocation-heavy paths, reduce with sync.Pool, pre-allocation, avoid interface{}
  4. Escape analysis: -gcflags="-m" → keep hot-path allocations on stack
  5. Benchmarks: go test -bench -benchmem before and after changes
  6. Trace: go tool trace for goroutine scheduling, GC pauses, latency analysis
  7. GC tuning: adjust GOGC, set GOMEMLIMIT, observe with gctrace

Golden rule: don't optimize without a profile. Measure, change, measure again.

Ans

internal/ is enforced by the Go compiler — packages under it can only be imported by code rooted at the parent of internal. This creates a hard visibility boundary.

pkg/ is a convention (not enforced) for packages intended to be importable by external projects.

Default to internal/ — making something public is an API commitment. Only move to pkg/ when you explicitly want external consumers. Many modern Go projects skip pkg/ entirely and put public packages at the root.

40 Ready for Gin Framework

You now know everything needed for Gin!

Every concept maps to what you learned above.

Go to Gin Guide →
package main  // executable package

import (
    "net/http"  // for status codes like StatusOK
    "strconv"  // for Atoi (string to int)
    "github.com/gin-gonic/gin"  // Gin web framework
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name" binding:"required"`  // required in request body
    Email string `json:"email" binding:"required,email"`  // must be valid email
}

var users = make(map[int]User)  // in-memory store
var nextID = 1  // auto-increment ID counter

func main() {
    r := gin.Default()  // router with Logger + Recovery
    r.Use(authMiddleware())  // apply auth to all routes

    api := r.Group("/api/v1")  // prefix all routes with /api/v1
    {
        api.GET("/users", getUsers)  // list all users
        api.GET("/users/:id", getUserByID)  // :id is a URL param
        api.POST("/users", createUser)  // create new user
    }
    r.Run(":8080")  // start server on port 8080
}

func getUsers(c *gin.Context) {
    list := make([]User, 0)  // ensure JSON array not null
    for _, u := range users { list = append(list, u) }  // collect all users
    c.JSON(http.StatusOK, gin.H{"data": list})  // gin.H is map[string]any
}

func getUserByID(c *gin.Context) {
    id, _ := strconv.Atoi(c.Param("id"))  // Param extracts :id from URL
    u, ok := users[id]  // look up user by ID
    if !ok { c.JSON(404, gin.H{"error": "not found"}); return }  // not found
    c.JSON(http.StatusOK, u)  // return user as JSON
}

func createUser(c *gin.Context) {
    var u User  // target for JSON binding
    if err := c.ShouldBindJSON(&u); err != nil {  // validate & decode body
        c.JSON(400, gin.H{"error": err.Error()}); return  // bad request
    }
    u.ID = nextID; nextID++  // assign auto-increment ID
    users[u.ID] = u  // store the new user
    c.JSON(http.StatusCreated, u)  // 201 Created response
}

func authMiddleware() gin.HandlerFunc {  // returns a Gin handler
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")  // read auth header
        if token == "" {  // no token provided
            c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})  // stop chain
            return
        }
        c.Set("userID", "123")  // attach data to request context
        c.Next()  // proceed to next handler
    }
}

Concept Mapping

What You LearnedUsed in Gin
Structs + JSON tagsRequest/Response models, ShouldBindJSON
InterfacesGin's Handler, custom middleware
Error handlingEvery handler checks errors
ClosuresMiddleware pattern
MapsRoute params, gin.H{}
Pointers*gin.Context everywhere
ContextRequest context, timeouts
GoroutinesEach request = its own goroutine
Options patternServer / middleware configuration
Testinghttptest for handler tests

What's Next?
  1. Practice data structures — implement from memory
  2. Build a REST API with net/http (no framework)
  3. Then move to Gin — it's syntactic sugar over what you know
  4. Add PostgreSQL (sqlx or GORM)
  5. Add JWT authentication
  6. Deploy with Docker

Built for mastery. Now go build something.