All topics
Frontend · Learning hub

Go notes for developers

Master Go with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Frontend notes
Go

Core Language

Go — Core Language Basics package main import ( "fmt" "strings" "strconv" ) func main() { // Variable declaration var name string = "Alice" age := 28 // short d

Go — Core Language

Basics

package main

import (
    "fmt"
    "strings"
    "strconv"
)

func main() {
    // Variable declaration
    var name string = "Alice"
    age := 28               // short declaration (type inferred)
    var x, y int = 1, 2
    var isActive = true

    // Zero values: int=0, float=0.0, bool=false, string=""

    // Constants
    const Pi = 3.14159
    const (
        StatusOK    = 200
        StatusNotFound = 404
    )

    // Type conversion (explicit, no implicit)
    n := 42
    f := float64(n)
    s := strconv.Itoa(n)    // int to string
    n2, err := strconv.Atoi("42")  // string to int

    // String operations
    fmt.Println(len(name))
    fmt.Println(strings.ToUpper(name))
    fmt.Println(strings.Contains(name, "Ali"))
    fmt.Println(strings.Split("a,b,c", ","))
    fmt.Sprintf("Hello, %s! You are %d.", name, age)
}

Control Flow

// If — can include an init statement
if x > 0 {
    fmt.Println("positive")
} else if x < 0 {
    fmt.Println("negative")
} else {
    fmt.Println("zero")
}

// If with init statement
if val, err := strconv.Atoi("42"); err == nil {
    fmt.Println(val * 2)
}

// For — Go's only loop
for i := 0; i < 10; i++ { ... }         // C-style
for i < 10 { i++ }                       // while-style
for { break }                             // infinite loop

// Range
nums := []int{1, 2, 3}
for i, v := range nums { fmt.Println(i, v) }
for _, v := range nums { fmt.Println(v) }  // ignore index

m := map[string]int{"a": 1}
for k, v := range m { fmt.Println(k, v) }

// Switch — no fallthrough by default
switch day {
case "Mon", "Tue", "Wed", "Thu", "Fri":
    fmt.Println("weekday")
case "Sat", "Sun":
    fmt.Println("weekend")
default:
    fmt.Println("unknown")
}

// Switch with no condition (like if-else chain)
switch {
case x < 0:
    fmt.Println("negative")
case x == 0:
    fmt.Println("zero")
default:
    fmt.Println("positive")
}

Functions

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

result, err := divide(10, 3)
if err != nil {
    log.Fatal(err)
}

// Named return values
func minMax(arr []int) (min, max int) {
    min, max = arr[0], arr[0]
    for _, v := range arr[1:] {
        if v < min { min = v }
        if v > max { max = v }
    }
    return  // naked return
}

// Variadic
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
sum(1, 2, 3)
nums := []int{1, 2, 3}
sum(nums...)   // spread slice

// First-class functions
add := func(a, b int) int { return a + b }
apply := func(f func(int, int) int, a, b int) int { return f(a, b) }

// Defer — runs when function returns (LIFO order)
func readFile(path string) {
    f, _ := os.Open(path)
    defer f.Close()    // guaranteed cleanup
    // ... use f
}

Data Structures

// Arrays (fixed size)
var a [3]int = [3]int{1, 2, 3}
b := [...]int{1, 2, 3}   // size inferred

// Slices (dynamic, reference to underlying array)
s := []int{1, 2, 3}
s = append(s, 4, 5)
s2 := s[1:3]             // slice [2, 3] — shares memory!
s3 := make([]int, 5)     // len=5, cap=5
s4 := make([]int, 0, 10) // len=0, cap=10

copy(dst, src)           // copy elements
len(s), cap(s)

// Maps
m := map[string]int{"a": 1, "b": 2}
m["c"] = 3
val, ok := m["a"]    // ok=false if key missing
delete(m, "b")

m2 := make(map[string][]string)

// Structs
type User struct {
    ID       int
    Name     string
    Email    string
    IsActive bool
}

u := User{ID: 1, Name: "Alice", Email: "a@b.com"}
u2 := User{1, "Bob", "b@b.com", true}  // positional (avoid)
fmt.Println(u.Name)

// Struct embedding (composition over inheritance)
type Admin struct {
    User            // embed User fields/methods
    Level int
}
admin := Admin{User: u, Level: 5}
fmt.Println(admin.Name)   // promoted field
Go

Interfaces, Goroutines & Channels

Interfaces, Goroutines & Channels Interfaces // Interface — implicit implementation (no "implements") type Animal interface { Sound() string Name() string } typ

Interfaces, Goroutines & Channels

Interfaces

// Interface — implicit implementation (no "implements")
type Animal interface {
    Sound() string
    Name() string
}

type Dog struct{ name string }
func (d Dog) Sound() string { return "Woof" }
func (d Dog) Name() string { return d.name }

type Cat struct{ name string }
func (c Cat) Sound() string { return "Meow" }
func (c Cat) Name() string { return c.name }

func describe(a Animal) {
    fmt.Printf("%s says %s
", a.Name(), a.Sound())
}

// Works for any type implementing Animal
describe(Dog{"Rex"})
describe(Cat{"Whiskers"})

// Common interfaces (stdlib)
// io.Reader — Read(p []byte) (n int, err error)
// io.Writer — Write(p []byte) (n int, err error)
// fmt.Stringer — String() string
// error — Error() string

// Empty interface — any type
var any interface{} = 42
any = "hello"
any = Dog{}

// Type assertion
s, ok := any.(string)
if !ok { ... }

// Type switch
switch v := any.(type) {
case int:
    fmt.Printf("int: %d
", v)
case string:
    fmt.Printf("string: %s
", v)
default:
    fmt.Printf("other: %T
", v)
}

Error Handling

// Idiomatic Go: explicit error returns
func fetchUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("invalid id: %d", id)
    }
    // ...
    return &User{ID: id}, nil
}

// Check errors immediately
user, err := fetchUser(5)
if err != nil {
    return fmt.Errorf("fetchUser failed: %w", err)  // wrap with context
}

// Custom error type
type NotFoundError struct {
    Resource string
    ID       int
}
func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with id %d not found", e.Resource, e.ID)
}

// Unwrap errors
var nfe *NotFoundError
if errors.As(err, &nfe) {
    fmt.Println("not found:", nfe.Resource)
}
if errors.Is(err, ErrTimeout) { ... }  // compare sentinel errors

// Panic/recover (only for truly unexpected failures)
defer func() {
    if r := recover(); r != nil {
        fmt.Println("recovered:", r)
    }
}()

Goroutines & Channels

// Goroutine — lightweight thread, launched with 'go'
go func() {
    fmt.Println("running concurrently")
}()

// Channel — typed pipe for communication between goroutines
ch := make(chan int)         // unbuffered — blocks until both sides ready
bch := make(chan int, 10)    // buffered — blocks when full

// Send / receive
go func() { ch <- 42 }()
val := <-ch                  // receive (blocks)

// Range over channel
go func() {
    for _, v := range []int{1, 2, 3} { ch <- v }
    close(ch)
}()
for v := range ch { fmt.Println(v) }   // until closed

// Select — wait on multiple channels
select {
case msg := <-ch1:
    fmt.Println("from ch1:", msg)
case msg := <-ch2:
    fmt.Println("from ch2:", msg)
case <-time.After(1 * time.Second):
    fmt.Println("timeout")
default:
    fmt.Println("no message ready")
}

// WaitGroup — wait for goroutines to finish
var wg sync.WaitGroup
for _, url := range urls {
    wg.Add(1)
    go func(u string) {
        defer wg.Done()
        fetch(u)
    }(url)
}
wg.Wait()

// Mutex — protect shared state
var mu sync.Mutex
mu.Lock()
sharedMap[key] = value
mu.Unlock()

// sync.Once — run exactly once
var once sync.Once
once.Do(func() { initExpensiveResource() })

Context

import "context"

// Pass context for cancellation/timeout
func fetchUser(ctx context.Context, id int) (*User, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    // ...
}

// Timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
user, err := fetchUser(ctx, 1)

// Cancellation
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(2 * time.Second)
cancel()   // signal worker to stop

// Check cancellation in worker
func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("cancelled:", ctx.Err())
            return
        default:
            doWork()
        }
    }
}
Go

Interview Questions

Go Interview Questions Q: What makes Go different from other languages? Go's key differentiators: (1) Built-in concurrency with goroutines (cheap, ~2KB stack) a

Go Interview Questions

Q: What makes Go different from other languages?

Go's key differentiators: (1) Built-in concurrency with goroutines (cheap, ~2KB stack) and channels. (2) Compiles to a single static binary — no runtime dependencies. (3) Fast compilation. (4) Explicit error handling (no exceptions). (5) Implicit interface satisfaction. (6) Garbage collected but with minimal pause. (7) Simplicity — deliberately small language spec.

Q: What is a goroutine and how does it differ from a thread?

Goroutines are lightweight, user-space threads managed by the Go runtime. They start with ~2KB stack (grows dynamically) vs OS threads (~1-8MB). The Go scheduler multiplexes many goroutines onto a few OS threads (M:N threading). Switching goroutines is much faster than OS context switches. You can run millions of goroutines concurrently.

Q: What is the difference between a channel and a mutex?

Channels are for communication and synchronization between goroutines — pass data via channels instead of sharing memory. Mutexes protect shared memory from concurrent access. Go's motto: "Don't communicate by sharing memory; share memory by communicating." Use channels for pipelines/worker patterns. Use mutexes when you have simple shared state (counters, caches).

Q: How does Go handle errors?

Go has no exceptions. Functions return an error as the last return value. Callers must explicitly check errors. This makes error handling visible and forces you to think about every failure path. Use fmt.Errorf("context: %w", err) to wrap errors with context. Use errors.Is() to check for specific error values and errors.As() to check for specific error types.

Q: What is a nil pointer dereference and how do you avoid it?

Accessing a field or method on a nil pointer causes a panic. Avoid by checking for nil before use: if user != nil { ... }. Return errors instead of nil values when operations fail. Use the "comma ok" pattern for map access and type assertions. Pointer receivers work on nil pointers if the method handles it explicitly.

Q: What is a slice vs array in Go?

An array has a fixed size ([3]int) and is a value type — copying it copies all elements. A slice is a dynamic view into an underlying array (pointer + length + capacity) and is a reference type — slices created from the same array share memory. Slices are almost always preferred over arrays in Go.

Q: What is defer and when is it used?

defer schedules a function call to run when the surrounding function returns (even if it panics). Deferred calls execute in LIFO (last-in, first-out) order. Primary use: resource cleanup — defer f.Close(), defer mu.Unlock(), defer wg.Done(). Arguments to deferred functions are evaluated immediately at the defer statement.

Keep your Go knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever