Toolmingo
Guides22 min read

50 Go (Golang) Interview Questions (With Answers)

Top Golang interview questions with clear answers and code examples — covering goroutines, channels, interfaces, error handling, concurrency patterns, and Go idioms.

Go interviews test understanding of goroutines, channels, interfaces, the type system, error handling, and idiomatic Go patterns. This guide covers the 50 most common questions — with concise answers and runnable code examples.

Quick reference

Topic Most asked questions
Basics Variables, types, zero values, pointers
Functions Multiple returns, variadic, defer, closures
Types Structs, interfaces, embedding, type assertion
Goroutines go keyword, WaitGroup, goroutine leaks
Channels Buffered vs unbuffered, select, closing
Error handling error interface, wrapping, sentinel errors
Concurrency Mutex, RWMutex, atomic, sync primitives
Standard library context, http, testing, io

Basics

1. What are Go's key design goals?

Go was designed at Google by Ken Thompson, Rob Pike, and Robert Griesemer to address:

Goal How Go achieves it
Fast compilation Simple grammar, no header files
Readable code Gofmt, explicit over implicit
Safe concurrency Goroutines + channels (CSP model)
Garbage collection Automatic memory management
Static typing Compile-time type safety
Single binary Statically linked output

Go deliberately omits generics (added in 1.18), inheritance, operator overloading, and exceptions.


2. What is the zero value in Go?

Every variable is initialized to its zero value if not explicitly set:

var i int       // 0
var f float64   // 0.0
var b bool      // false
var s string    // ""
var p *int      // nil
var sl []int    // nil
var m map[string]int // nil

This eliminates uninitialized variable bugs common in C.


3. What is the difference between var x = 5 and x := 5?

var x = 5   // package or function scope, explicit var keyword
x := 5      // short variable declaration — function scope only

:= declares and assigns; you cannot use it at package level. var works everywhere. Both infer the type.

// var can declare without initialization (zero value)
var timeout time.Duration // 0

// := requires a right-hand side value
timeout := 30 * time.Second

4. What are Go pointers and when should you use them?

A pointer holds a memory address. Go has *T (pointer to T) and & (address-of operator):

x := 42
p := &x   // p is *int
*p = 100  // dereference — modifies x
fmt.Println(x) // 100

Use pointers when:

  • Mutating a value in a function (Go passes everything by value)
  • Working with large structs (avoid copying)
  • Indicating optional/nullable values (*string vs string)

Go does not have pointer arithmetic (unlike C).


5. Explain the difference between arrays and slices.

Feature Array Slice
Size Fixed at compile time Dynamic
Value type Yes (copied on assignment) No (reference type — shares backing array)
Declaration [3]int{1,2,3} []int{1,2,3}
Length len() len() + cap()
// Array — copying
a := [3]int{1, 2, 3}
b := a   // full copy
b[0] = 99
fmt.Println(a[0]) // 1 — unchanged

// Slice — shared backing array
s := []int{1, 2, 3}
t := s[0:2]  // shares memory
t[0] = 99
fmt.Println(s[0]) // 99 — changed!

Use slices in practice; arrays are mainly used as fixed-size building blocks.


6. How does make differ from new?

// new — allocates zeroed memory, returns pointer
p := new(int)   // *int pointing to 0

// make — initializes slice/map/channel, returns value (not pointer)
s := make([]int, 3, 10)       // len=3, cap=10
m := make(map[string]int)      // ready-to-use map
ch := make(chan int, 5)         // buffered channel

new is rarely used directly; prefer &T{} for structs. make is required for slices, maps, and channels because they need internal initialization.


7. What are variadic functions?

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

sum(1, 2, 3)       // 6
nums := []int{1, 2, 3}
sum(nums...)        // spread slice with ...

The variadic parameter must be last. Inside the function it behaves as a []int.


Functions and Defer

8. What is defer and when is it executed?

defer schedules a function call to run when the surrounding function returns — after the return value is set but before the caller sees it.

func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()  // always runs, even if function panics
    // ... read file
    return nil
}

Multiple defers execute in LIFO (last in, first out) order:

func example() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
}
// Output: 3, 2, 1

9. Can a deferred function modify the return value?

Yes — if the return value is named:

func double(n int) (result int) {
    defer func() {
        result *= 2  // modifies the named return value
    }()
    result = n
    return
}

fmt.Println(double(5)) // 10

With anonymous return values, the deferred function cannot modify them.


10. What is a closure in Go?

A closure is a function that captures variables from its surrounding scope:

func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3

Common gotcha — loop variable capture:

// WRONG — all goroutines capture the same i
for i := 0; i < 3; i++ {
    go func() { fmt.Println(i) }()  // prints 3, 3, 3
}

// CORRECT — pass i as argument
for i := 0; i < 3; i++ {
    go func(n int) { fmt.Println(n) }(i)
}

Types and Interfaces

11. How do interfaces work in Go?

Interfaces are implicit — a type satisfies an interface by implementing its methods, without declaring it:

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

type File struct{ /* ... */ }

// File implements Writer implicitly
func (f *File) Write(p []byte) (int, error) {
    // ...
    return len(p), nil
}

var w Writer = &File{} // valid — File satisfies Writer

The empty interface interface{} (or any since Go 1.18) accepts any value.


12. What is the difference between a value receiver and a pointer receiver?

type Counter struct{ count int }

// Value receiver — works on a copy
func (c Counter) Value() int { return c.count }

// Pointer receiver — can modify the original
func (c *Counter) Increment() { c.count++ }

Rules:

  • Use pointer receivers if you need to mutate state or if the struct is large
  • Be consistent — mix of value and pointer receivers on the same type causes confusion
  • Interfaces are satisfied by both, but *T method set includes all T methods; T method set excludes *T methods

13. What is embedding in Go?

Go uses embedding instead of inheritance:

type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

type Dog struct {
    Animal        // embedded — promotes methods and fields
    Breed string
}

d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Husky"}
fmt.Println(d.Speak())  // "Rex makes a sound" — promoted
fmt.Println(d.Name)     // "Rex" — promoted field

The embedding type can override promoted methods by defining its own.


14. What is a type assertion vs a type switch?

var i interface{} = "hello"

// Type assertion — panics if wrong type without ok
s, ok := i.(string)  // ok form — safe
if ok {
    fmt.Println(s) // "hello"
}

// Type switch
switch v := i.(type) {
case string:
    fmt.Println("string:", v)
case int:
    fmt.Println("int:", v)
default:
    fmt.Printf("unknown: %T\n", v)
}

Always use the ok form unless you are certain of the type.


15. What are generics in Go (1.18+)?

Go 1.18 introduced type parameters:

// Generic function
func Map[T, U any](s []T, f func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

doubled := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })
// [2, 4, 6]

// Generic type with constraint
type Number interface {
    ~int | ~float64
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

Error Handling

16. How does Go handle errors?

Go treats errors as values, not exceptions:

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

The error interface is:

type error interface {
    Error() string
}

This makes error handling explicit and avoids hidden control flow.


17. What is the difference between errors.Is, errors.As, and ==?

var ErrNotFound = errors.New("not found")

// errors.Is — checks error chain (works with wrapping)
if errors.Is(err, ErrNotFound) { /* ... */ }

// errors.As — extracts a specific error type from chain
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println(pathErr.Path)
}

// == — only works if error is NOT wrapped
// Avoid for library errors — use errors.Is instead

Use fmt.Errorf("context: %w", err) to wrap; %w enables errors.Is/errors.As to unwrap the chain.


18. What is a sentinel error?

A sentinel error is a package-level error variable used as a signal:

var (
    ErrNotFound   = errors.New("not found")
    ErrPermission = errors.New("permission denied")
)

func FindUser(id int) (*User, error) {
    if id == 0 {
        return nil, ErrNotFound
    }
    // ...
}

// Caller checks with errors.Is
user, err := FindUser(0)
if errors.Is(err, ErrNotFound) {
    // handle missing user
}

19. When should you use panic and recover?

panic should only be used for truly unrecoverable situations (programmer errors, invariant violations):

func MustParse(s string) *url.URL {
    u, err := url.Parse(s)
    if err != nil {
        panic(err)  // only for startup-time / programming errors
    }
    return u
}

recover catches panics, typically in a deferred function:

func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    return a / b, nil
}

Do not use panic/recover as a substitute for normal error handling.


Goroutines and Concurrency

20. What is a goroutine?

A goroutine is a lightweight, concurrently executing function managed by the Go runtime:

go func() {
    fmt.Println("runs concurrently")
}()

Goroutines start with ~2KB of stack (which grows dynamically). A Go program can run millions of goroutines, unlike OS threads (which are MB each).

The Go scheduler multiplexes goroutines onto OS threads (M:N scheduling).


21. How do you wait for goroutines to finish?

Use sync.WaitGroup:

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        fmt.Printf("worker %d done\n", n)
    }(i)
}

wg.Wait() // blocks until all goroutines call Done()

22. What is a channel and what are the types?

Channels are typed conduits for goroutine communication:

// Unbuffered — sender blocks until receiver is ready
ch := make(chan int)

// Buffered — sender blocks only when buffer is full
ch := make(chan int, 10)

// Directional channels
func producer(out chan<- int) { out <- 42 }   // send-only
func consumer(in <-chan int) { fmt.Println(<-in) }  // receive-only

Closing a channel:

close(ch)
val, ok := <-ch  // ok is false when channel is closed and drained
for v := range ch { /* reads until closed */ }

23. Explain the select statement.

select waits on multiple channel operations, running the first ready case:

func merge(ch1, ch2 <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for {
            select {
            case v, ok := <-ch1:
                if !ok { ch1 = nil; continue }
                out <- v
            case v, ok := <-ch2:
                if !ok { ch2 = nil; continue }
                out <- v
            }
            if ch1 == nil && ch2 == nil {
                return
            }
        }
    }()
    return out
}

select with a default case becomes non-blocking.


24. What causes a goroutine leak?

A goroutine leak occurs when a goroutine is stuck forever — usually blocked on a channel with no sender/receiver:

// LEAK — nobody sends on ch, goroutine hangs forever
func leaky() {
    ch := make(chan int)
    go func() {
        v := <-ch  // blocked forever
        fmt.Println(v)
    }()
}

// FIX — use context for cancellation
func noLeak(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case v := <-ch:
            fmt.Println(v)
        case <-ctx.Done():
            return
        }
    }()
}

Detect leaks with runtime.NumGoroutine() or the goleak library in tests.


25. What is the difference between sync.Mutex and sync.RWMutex?

// Mutex — exclusive lock (one reader OR one writer at a time)
var mu sync.Mutex
mu.Lock()
// critical section
mu.Unlock()

// RWMutex — multiple readers OR one writer
var rw sync.RWMutex
rw.RLock()   // multiple goroutines can hold RLock simultaneously
// read
rw.RUnlock()

rw.Lock()    // exclusive write lock
// write
rw.Unlock()

Use RWMutex when reads are frequent and writes are rare.


26. What is sync.Once?

sync.Once guarantees a function runs exactly once, even across goroutines:

var (
    instance *DB
    once     sync.Once
)

func GetDB() *DB {
    once.Do(func() {
        instance = connectToDB()
    })
    return instance
}

Common use: lazy initialization of singletons.


27. What is sync/atomic used for?

For simple counter operations without a full mutex:

import "sync/atomic"

var counter int64

atomic.AddInt64(&counter, 1)
val := atomic.LoadInt64(&counter)
atomic.StoreInt64(&counter, 0)
ok := atomic.CompareAndSwapInt64(&counter, 0, 1)

Atomics are faster than mutexes for simple numeric operations but limited to basic types.


Context

28. What is context.Context and why is it important?

context.Context carries deadlines, cancellation signals, and request-scoped values across API boundaries and goroutines:

func fetchData(ctx context.Context, url string) ([]byte, error) {
    req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

// Caller controls the timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
data, err := fetchData(ctx, "https://example.com")

Always pass ctx as the first parameter. Always call cancel() to release resources.


29. What are the main context functions?

context.Background()          // root context — no cancellation, no deadline
context.TODO()                // placeholder when unsure which context to use

context.WithCancel(parent)    // returns (ctx, cancelFn) — cancel manually
context.WithTimeout(parent, d) // auto-cancels after duration d
context.WithDeadline(parent, t) // auto-cancels at absolute time t
context.WithValue(parent, k, v) // attaches a value (use unexported key type)

30. What is the context.WithValue key gotcha?

Using a plain string/int as a context key causes collisions across packages:

// WRONG — collides with any package using key "userID"
ctx := context.WithValue(ctx, "userID", 42)

// CORRECT — use an unexported type to prevent collisions
type contextKey string
const userIDKey contextKey = "userID"

ctx = context.WithValue(ctx, userIDKey, 42)
val := ctx.Value(userIDKey).(int) // 42

Maps and Memory

31. What happens if you read from a nil map?

Reading from a nil map returns the zero value — it does not panic:

var m map[string]int
val := m["key"] // returns 0 — safe

But writing to a nil map panics:

m["key"] = 1 // panic: assignment to entry in nil map

Always initialize with make or a map literal before writing.


32. Is a map safe for concurrent use?

No — concurrent reads are safe, but concurrent writes (or a concurrent read + write) will cause a data race and panic at runtime:

// WRONG — data race
m := map[string]int{}
go func() { m["a"] = 1 }()
go func() { _ = m["a"] }()

// CORRECT — use sync.RWMutex or sync.Map
var mu sync.RWMutex
mu.Lock()
m["a"] = 1
mu.Unlock()

// OR — use sync.Map for highly concurrent access
var sm sync.Map
sm.Store("a", 1)
val, _ := sm.Load("a")

33. How does Go's garbage collector work?

Go uses a tri-color mark-and-sweep GC with concurrent marking:

  1. GC marks the root set (globals, goroutine stacks)
  2. Concurrently traverses the object graph, marking reachable objects
  3. Sweeps unreachable memory

Key points:

  • Concurrent with mutator (small stop-the-world pauses)
  • Write barrier during marking prevents races
  • GOGC env var controls GC trigger (default 100 = trigger when heap doubles)
  • runtime.GC() forces collection

Testing

34. How do you write tests in Go?

// sum_test.go
package math

import "testing"

func TestSum(t *testing.T) {
    got := Sum(1, 2)
    want := 3
    if got != want {
        t.Errorf("Sum(1,2) = %d, want %d", got, want)
    }
}

// Table-driven tests (idiomatic Go)
func TestSumTable(t *testing.T) {
    tests := []struct {
        name    string
        a, b    int
        want    int
    }{
        {"positive", 1, 2, 3},
        {"zero", 0, 0, 0},
        {"negative", -1, -2, -3},
    }
    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            if got := Sum(tc.a, tc.b); got != tc.want {
                t.Errorf("got %d, want %d", got, tc.want)
            }
        })
    }
}

Run with go test ./... — no external framework needed.


35. What is t.Parallel() and when should you use it?

func TestSlow(t *testing.T) {
    t.Parallel() // marks test as parallelizable
    time.Sleep(1 * time.Second)
}

Tests marked t.Parallel() run concurrently with other parallel tests. Use for:

  • Tests that call external services (HTTP, DB)
  • CPU-independent tests to speed up the suite

Do not use t.Parallel() for tests that share mutable global state.


36. How do you test HTTP handlers?

Use net/http/httptest:

func TestHelloHandler(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/hello", nil)
    w := httptest.NewRecorder()

    HelloHandler(w, req)

    resp := w.Result()
    if resp.StatusCode != http.StatusOK {
        t.Errorf("status = %d, want 200", resp.StatusCode)
    }
    body, _ := io.ReadAll(resp.Body)
    if string(body) != "hello\n" {
        t.Errorf("body = %q", body)
    }
}

HTTP and Standard Library

37. How do you build an HTTP server in Go?

package main

import (
    "encoding/json"
    "net/http"
)

type Response struct {
    Message string `json:"message"`
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(Response{Message: "hello"})
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /hello", helloHandler)  // Go 1.22+ method-specific routing

    srv := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }
    log.Fatal(srv.ListenAndServe())
}

38. What is the idiomatic way to handle JSON in Go?

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email,omitempty"`  // omit if empty
    Pass  string `json:"-"`                // never marshal
}

// Decode request body
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
    http.Error(w, "bad request", http.StatusBadRequest)
    return
}

// Encode response
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(u)

// Marshal/unmarshal to/from bytes
data, _ := json.Marshal(u)
json.Unmarshal(data, &u)

39. How does io.Reader / io.Writer composition work?

Go's standard library is built around small interfaces:

// Read from any source — file, HTTP body, string, network
func process(r io.Reader) error {
    buf := make([]byte, 4096)
    for {
        n, err := r.Read(buf)
        if n > 0 {
            // process buf[:n]
        }
        if err == io.EOF { return nil }
        if err != nil { return err }
    }
}

// Chain readers — wrap and compose
gz, _ := gzip.NewReader(file)
defer gz.Close()
process(gz)  // transparently decompresses

// io.TeeReader — read and write simultaneously
tee := io.TeeReader(r, os.Stdout)  // prints everything it reads

Go Toolchain and Modules

40. How does Go modules work?

go mod init module/path   # creates go.mod
go get github.com/pkg@v1  # adds dependency
go mod tidy               # remove unused, add missing
go mod vendor             # copy deps to ./vendor
go mod download           # cache modules locally

go.mod example:

module github.com/user/myapp

go 1.22

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/stretchr/testify v1.8.4
)

go.sum records cryptographic hashes of all dependency versions.


41. What is go build vs go run vs go install?

go run main.go           # compile & run (temp binary — dev only)
go build ./...           # compile, output binary in current dir
go build -o myapp ./...  # named binary
go install ./...         # compile & install to $GOPATH/bin

go build is used for production artifacts. go run is for quick iteration.


42. What does go vet do?

go vet catches common correctness errors that the compiler doesn't:

go vet ./...

Examples it catches:

  • fmt.Printf format/argument mismatches
  • Unreachable code after return
  • Calling sync.Mutex by value (not pointer)
  • Wrong number of arguments in goroutine calls
  • Using append result without assignment

Run go vet before every commit. Most CI pipelines include it.


Common Patterns

43. What is the functional options pattern?

Used to provide optional configuration to constructors:

type Server struct {
    host    string
    port    int
    timeout time.Duration
}

type Option func(*Server)

func WithHost(h string) Option {
    return func(s *Server) { s.host = h }
}

func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.timeout = d }
}

func NewServer(opts ...Option) *Server {
    s := &Server{host: "localhost", port: 8080, timeout: 30 * time.Second}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// Usage
srv := NewServer(
    WithHost("0.0.0.0"),
    WithTimeout(10*time.Second),
)

44. What is the worker pool pattern?

Limits concurrency to prevent resource exhaustion:

func workerPool(jobs <-chan int, numWorkers int) <-chan int {
    results := make(chan int, len(jobs))
    var wg sync.WaitGroup

    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                results <- job * 2  // do work
            }
        }()
    }

    go func() {
        wg.Wait()
        close(results)
    }()

    return results
}

45. How do you implement graceful shutdown?

func main() {
    srv := &http.Server{Addr: ":8080", Handler: mux}

    go func() {
        if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
            log.Fatal(err)
        }
    }()

    // Wait for interrupt
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("shutdown error:", err)
    }
    log.Println("server stopped")
}

46. What is the difference between string and []byte?

s := "hello"
b := []byte(s)       // convert string → []byte (copies)
s2 := string(b)      // convert []byte → string (copies)

// strings.Builder — efficient string building without copies
var sb strings.Builder
for i := 0; i < 10; i++ {
    sb.WriteString("a")
}
result := sb.String()
Feature string []byte
Mutability Immutable Mutable
Iterating runes for _, r := range s Need rune() conversion
HTTP body string(body) Direct from io.ReadAll
Concatenation Use strings.Builder append

47. What is iota in Go?

iota is a constant generator that resets to 0 on each const block:

type Direction int

const (
    North Direction = iota // 0
    East                   // 1
    South                  // 2
    West                   // 3
)

// Bit flags with iota
type Permission uint

const (
    Read    Permission = 1 << iota // 1 (1<<0)
    Write                          // 2 (1<<1)
    Execute                        // 4 (1<<2)
)

perm := Read | Write  // 3

48. How do you profile a Go program?

import _ "net/http/pprof"

func main() {
    go http.ListenAndServe(":6060", nil)  // expose profiling endpoints
    // ... your app
}
# CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Memory profile
go tool pprof http://localhost:6060/debug/pprof/heap

# Goroutine profile
go tool pprof http://localhost:6060/debug/pprof/goroutine

# Benchmark and profile
go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof

In the pprof interactive shell: top, list FuncName, web (opens flame graph).


49. What is the init function?

init runs automatically before main, after package-level variables are initialized:

var db *sql.DB

func init() {
    var err error
    db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal(err)
    }
}

Rules:

  • A package can have multiple init functions (even in the same file)
  • They run in order they appear, files processed alphabetically
  • Cannot be called explicitly
  • Used for registration, validation, and side effects of imports

50. What common Go anti-patterns should you avoid?

Anti-pattern Problem Fix
Ignoring errors _ = err Silent failures Always handle errors
Goroutine without exit Memory/goroutine leak Use context cancellation
Using panic for normal errors Crashes the program Return error instead
sync.Mutex copied by value Corrupts state Always use pointer *sync.Mutex
Global mutable state Race conditions Inject dependencies
interface{} everywhere Loses type safety Use generics or specific types
Premature goroutines Unnecessary complexity Start with sequential, add concurrency when needed
Naked return with named results Confusing to read Explicit return values

Go vs other languages

Feature Go Java Python Rust
Concurrency model Goroutines + channels Threads + CompletableFuture asyncio / threads async/await + threads
Error handling Return values Exceptions Exceptions Result<T, E>
Memory GC GC GC Ownership (no GC)
Compilation Fast native binary JVM bytecode Interpreted / PyPy Slow but optimized
Generics 1.18+ Since Java 5 Duck typing Full generics
Binary size ~5-10MB static Needs JVM Needs interpreter ~1-5MB static

Common mistakes

Mistake Why it's wrong Fix
Loop variable capture in goroutine All goroutines share same variable Pass loop var as argument
Nil pointer dereference Didn't check err != nil Always check errors immediately
Forgetting defer cancel() Context leak ctx, cancel := ...; defer cancel()
Reading from closed channel Returns zero value, not panics Check ok in v, ok := <-ch
Writing to nil map Panics at runtime Initialize with make
Using sync.Mutex by value Copies the lock state Embed sync.Mutex or use pointer
append result not assigned Original slice unchanged s = append(s, val)
HTTP response body not closed File descriptor leak defer resp.Body.Close()

FAQ

Q: Is Go object-oriented?
A: Go has types, methods, and interfaces but no classes or inheritance. It favors composition over inheritance via struct embedding. You can achieve OOP patterns but Go encourages a simpler, more direct style.

Q: When should I use channels vs mutexes?
A: Go proverb — "Share memory by communicating, don't communicate by sharing memory." Use channels to pass ownership of data between goroutines. Use mutexes to protect shared state accessed by multiple goroutines. Channels are better for pipelines and fan-out; mutexes are simpler for caches and counters.

Q: What is the difference between context.Background() and context.TODO()?
A: Semantically, none — both return an empty context. Use Background() as the root of a context tree (in main, tests, top-level handlers). Use TODO() as a placeholder when you know you need a context but haven't determined which one yet.

Q: How does Go handle circular imports?
A: Go forbids circular imports at compile time. Fix by extracting the shared type into a third package, inverting the dependency, or using interfaces to break the cycle.

Q: Is Go good for microservices?
A: Yes — Go's fast startup, low memory footprint, static binary, and built-in HTTP server make it excellent for microservices. Popular frameworks include Gin, Echo, Fiber, and Chi. gRPC support via google.golang.org/grpc is first-class.

Q: What is the recommended way to structure a Go project?
A: Flat structure for small projects. For larger projects, the unofficial standard is cmd/ (entry points), internal/ (private packages), pkg/ (public packages), api/ (proto/OpenAPI specs). Avoid over-engineering early — start flat and refactor as the project grows. See golang-standards/project-layout on GitHub for reference.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools