Top 25 domande di colloquio Go: guida completa per sviluppatori

Padroneggia i colloqui Go con le 25 domande più frequenti. Goroutine, channel, interfacce e pattern di concorrenza con esempi di codice.

Domande di colloquio Go - Guida completa alla preparazione

I colloqui tecnici su Go valutano la padronanza dei concetti centrali del linguaggio: concorrenza, gestione della memoria e pattern idiomatici. Questa guida raccoglie le 25 domande più frequenti con risposte dettagliate ed esempi di codice.

Consiglio per il colloquio

Go privilegia semplicità e leggibilità. Gli intervistatori preferiscono risposte concise che dimostrino una comprensione profonda piuttosto che soluzioni eccessivamente complesse.

Fondamenti del linguaggio Go

1. Qual è la differenza tra var e :=?

La dichiarazione var consente di specificare il tipo in modo esplicito e funziona a livello di package. L'operatore := deduce il tipo automaticamente, ma è utilizzabile solo all'interno delle funzioni.

declaration.gogo
package main

// Package level - var required
var globalConfig = "production"

func main() {
    // var with explicit type
    var count int = 10

    // var with type inference
    var name = "Alice"

    // Short declaration - functions only
    age := 25

    // Multiple declarations
    var (
        host = "localhost"
        port = 8080
    )
}

La dichiarazione breve := è preferita all'interno delle funzioni per la sua concisione, mentre var resta necessaria per le variabili a livello di package.

2. Come funziona il sistema di tipi di Go?

Go adotta una tipizzazione statica con inferenza di tipi. Il linguaggio distingue i tipi per valore (copiati al momento dell'assegnazione) dai tipi per riferimento (che condividono la struttura sottostante).

types.gogo
package main

import "fmt"

func main() {
    // Value types - full copy
    a := [3]int{1, 2, 3}
    b := a          // Copies the array
    b[0] = 100      // Doesn't modify a
    fmt.Println(a)  // [1 2 3]

    // Reference types - share data
    slice1 := []int{1, 2, 3}
    slice2 := slice1    // Same underlying array
    slice2[0] = 100     // Also modifies slice1
    fmt.Println(slice1) // [100 2 3]

    // Maps are also references
    m1 := map[string]int{"a": 1}
    m2 := m1
    m2["a"] = 100
    fmt.Println(m1["a"]) // 100
}

Gli array sono tipi per valore, mentre slice, map e channel sono tipi per riferimento.

3. Spiega la differenza tra array e slice

Gli array hanno una dimensione fissa definita a tempo di compilazione. Gli slice sono viste dinamiche su un array sottostante con tre componenti: puntatore, lunghezza e capacità.

arrays_slices.gogo
package main

import "fmt"

func main() {
    // Array - fixed size, value type
    arr := [5]int{1, 2, 3, 4, 5}

    // Slice - view over the array
    slice := arr[1:4]  // [2 3 4]
    fmt.Printf("len=%d, cap=%d\n", len(slice), cap(slice))
    // len=3, cap=4

    // Modifications affect original array
    slice[0] = 20
    fmt.Println(arr) // [1 20 3 4 5]

    // Direct creation with make
    dynamic := make([]int, 3, 10)
    // len=3, cap=10

    // Append may reallocate
    dynamic = append(dynamic, 1, 2, 3, 4, 5)
}

Gli slice sono il tipo preferito per le collezioni dinamiche in Go.

4. Come funziona l'istruzione defer?

defer programma l'esecuzione di una chiamata alla fine della funzione che la contiene. Le chiamate differite vengono accodate ed eseguite in ordine LIFO (Last In, First Out).

defer.gogo
package main

import (
    "fmt"
    "os"
)

func main() {
    // LIFO order
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    // Prints: 3, 2, 1
}

// Typical use case: resource cleanup
func readFile(path string) ([]byte, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer file.Close() // Always executes

    // Read file...
    return os.ReadFile(path)
}

// Caution: arguments are evaluated immediately
func deferArgs() {
    x := 10
    defer fmt.Println(x) // Captures 10
    x = 20
    // Prints: 10
}

defer garantisce l'esecuzione anche durante un panic, rendendolo ideale per il rilascio delle risorse.

5. Cos'è un'interfaccia in Go?

Un'interfaccia definisce un insieme di metodi. Qualsiasi tipo che implementa quei metodi soddisfa implicitamente l'interfaccia, senza bisogno di una dichiarazione esplicita.

interfaces.gogo
package main

import "fmt"

// Interface definition
type Writer interface {
    Write([]byte) (int, error)
}

// Type that implicitly implements Writer
type FileLogger struct {
    path string
}

func (f *FileLogger) Write(data []byte) (int, error) {
    // Write to file
    fmt.Println("Writing to", f.path)
    return len(data), nil
}

// Empty interface - accepts any type
func printAny(v interface{}) {
    fmt.Printf("Type: %T, Value: %v\n", v, v)
}

// Type assertion
func process(w Writer) {
    // Type check
    if fl, ok := w.(*FileLogger); ok {
        fmt.Println("FileLogger with path:", fl.path)
    }
}

L'implementazione implicita delle interfacce permette un forte disaccoppiamento tra package.

Concorrenza e goroutine

6. Cos'è una goroutine e in cosa differisce da un thread?

Una goroutine è un thread leggero gestito dal runtime di Go. Usa pochi KB di stack (rispetto ai diversi MB di un thread del sistema) e lo scheduler di Go multiplexa migliaia di goroutine su pochi thread del sistema.

goroutines.gogo
package main

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

func main() {
    var wg sync.WaitGroup

    // Launch 1000 goroutines
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            time.Sleep(100 * time.Millisecond)
            fmt.Printf("Goroutine %d finished\n", id)
        }(i) // Pass i by value
    }

    wg.Wait()
    fmt.Println("All goroutines completed")
}
Trappola comune

Conviene passare sempre per valore le variabili di ciclo alle goroutine. Altrimenti tutte potrebbero catturare lo stesso valore finale.

7. Spiega come funzionano i channel

I channel consentono comunicazione e sincronizzazione tra goroutine. Possono essere bufferizzati (con capacità) o non bufferizzati (sincroni).

channels.gogo
package main

import "fmt"

func main() {
    // Unbuffered channel - blocks until received
    ch := make(chan int)

    go func() {
        ch <- 42 // Blocks until read
    }()

    value := <-ch // Receives value
    fmt.Println(value)

    // Buffered channel - doesn't block until full
    buffered := make(chan string, 2)
    buffered <- "first"
    buffered <- "second"
    // buffered <- "third" // Would block

    fmt.Println(<-buffered) // "first"
    fmt.Println(<-buffered) // "second"
}

I channel non bufferizzati garantiscono la sincronizzazione, mentre quelli bufferizzati permettono un disaccoppiamento temporale.

8. Come si usa select con più channel?

select attende contemporaneamente più operazioni su channel. La prima operazione pronta viene eseguita, con scelta casuale in caso di parità.

select.gogo
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "from ch1"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "from ch2"
    }()

    // Wait with timeout
    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println(msg)
        case msg := <-ch2:
            fmt.Println(msg)
        case <-time.After(500 * time.Millisecond):
            fmt.Println("Timeout")
        }
    }

    // Non-blocking select with default
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    default:
        fmt.Println("No message available")
    }
}

select è lo strumento fondamentale per gestire la concorrenza in Go con eleganza.

9. Come si evitano le race condition?

Le race condition si verificano quando più goroutine accedono a dati condivisi senza sincronizzazione. Go offre vari meccanismi di protezione.

race_conditions.gogo
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

// Solution 1: Mutex
type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

// Solution 2: RWMutex for read-heavy workloads
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) string {
    c.mu.RLock()         // Multiple readers allowed
    defer c.mu.RUnlock()
    return c.data[key]
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()          // Single writer
    defer c.mu.Unlock()
    c.data[key] = value
}

// Solution 3: atomic for simple counters
var atomicCounter int64

func incrementAtomic() {
    atomic.AddInt64(&atomicCounter, 1)
}

func main() {
    // Detection: go run -race main.go
    counter := SafeCounter{}
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }

    wg.Wait()
    fmt.Println("Count:", counter.count)
}

Il flag -race del compilatore individua le race condition a runtime.

10. Spiega il pattern worker pool

Il pattern worker pool limita la concorrenza creando un numero fisso di goroutine che processano task da una coda.

worker_pool.gogo
package main

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

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, job)
        time.Sleep(100 * time.Millisecond) // Simulate work
        results <- job * 2
    }
}

func main() {
    const numJobs = 10
    const numWorkers = 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    var wg sync.WaitGroup

    // Start workers
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // Send jobs
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

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

    // Collect results
    for result := range results {
        fmt.Println("Result:", result)
    }
}

Questo pattern evita il sovraccarico di memoria e CPU dovuto alla creazione di troppe goroutine.

Pronto a superare i tuoi colloqui su Go?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Gestione degli errori e panic/recover

11. Come si gestiscono gli errori in Go?

Go usa valori di ritorno espliciti per gli errori, senza eccezioni. Per convenzione, error è l'ultimo parametro restituito.

errors.gogo
package main

import (
    "errors"
    "fmt"
)

// Sentinel errors for comparison
var (
    ErrNotFound     = errors.New("resource not found")
    ErrUnauthorized = errors.New("access unauthorized")
)

// Custom error type
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation %s: %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age",
            Message: "must be positive",
        }
    }
    return nil
}

func main() {
    // Basic check
    if err := validateAge(-5); err != nil {
        // Type assertion for custom error
        var valErr *ValidationError
        if errors.As(err, &valErr) {
            fmt.Printf("Field: %s\n", valErr.Field)
        }
    }

    // Sentinel error comparison
    err := findUser("unknown")
    if errors.Is(err, ErrNotFound) {
        fmt.Println("User not found")
    }
}

func findUser(id string) error {
    // Error wrapping with context
    return fmt.Errorf("findUser %s: %w", id, ErrNotFound)
}

Il wrapping con %w concatena gli errori mantenendo la possibilità di verificare l'errore originale.

12. Quando conviene usare panic e recover?

panic interrompe il normale flusso ed esegue lo srotolamento dello stack. recover cattura il panic in un defer e consente di proseguire l'esecuzione.

panic_recover.gogo
package main

import "fmt"

func safeOperation() (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()

    riskyOperation()
    return nil
}

func riskyOperation() {
    // Simulates an operation that can panic
    panic("something went wrong")
}

// Legitimate use case: initialization validation
func MustCompileRegex(pattern string) *Regexp {
    r, err := regexp.Compile(pattern)
    if err != nil {
        panic(err) // Programming error
    }
    return r
}

func main() {
    err := safeOperation()
    if err != nil {
        fmt.Println("Recovered error:", err)
    }
    fmt.Println("Program continues")
}
Regola d'oro

Conviene usare panic solo per errori di programmazione (invarianti violate). Per errori previsti (file mancante, problemi di rete) è meglio restituire sempre un error.

Struct, metodi ed embedding

13. Qual è la differenza tra receiver per valore e per puntatore?

Un receiver per valore riceve una copia dello struct, mentre un receiver per puntatore riceve un riferimento e può modificare l'originale.

receivers.gogo
package main

import "fmt"

type Counter struct {
    value int
}

// Value receiver - works on copy
func (c Counter) GetValue() int {
    return c.value
}

// Pointer receiver - modifies original
func (c *Counter) Increment() {
    c.value++
}

// Pointer receiver for large structs (avoids copy)
type LargeStruct struct {
    data [1000]int
}

func (l *LargeStruct) Process() {
    // Avoids copying 8000 bytes
}

func main() {
    c := Counter{value: 0}
    c.Increment() // Go automatically converts
    fmt.Println(c.GetValue()) // 1

    // Careful with interfaces
    var _ fmt.Stringer = &c // OK if method on *Counter
}

Regola: se un metodo usa un receiver per puntatore, tutti i metodi del tipo dovrebbero usare receiver per puntatore per coerenza.

14. Come funziona l'embedding in Go?

L'embedding include un tipo all'interno di un altro, ereditandone metodi e campi. Non è ereditarietà classica, ma composizione.

embedding.gogo
package main

import "fmt"

type Logger struct {
    prefix string
}

func (l *Logger) Log(msg string) {
    fmt.Printf("[%s] %s\n", l.prefix, msg)
}

// Embedding Logger
type Service struct {
    *Logger // Pointer embedding
    name    string
}

func NewService(name string) *Service {
    return &Service{
        Logger: &Logger{prefix: name},
        name:   name,
    }
}

func main() {
    svc := NewService("API")

    // Promoted method - direct access
    svc.Log("Starting")

    // Explicit access also works
    svc.Logger.Log("Explicit")

    // Promoted field
    fmt.Println(svc.prefix) // "API"
}

L'embedding consente composizioni flessibili evitando la rigidità dell'ereditarietà.

15. Come si implementa il pattern singleton in Go?

Il package sync offre sync.Once per garantire una singola esecuzione dell'inizializzazione, anche con goroutine concorrenti.

singleton.gogo
package main

import (
    "fmt"
    "sync"
)

type Database struct {
    connectionString string
}

var (
    instance *Database
    once     sync.Once
)

func GetDatabase() *Database {
    once.Do(func() {
        fmt.Println("Single initialization")
        instance = &Database{
            connectionString: "postgres://...",
        }
    })
    return instance
}

func main() {
    // Concurrent calls - single initialization
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            db := GetDatabase()
            fmt.Printf("Instance: %p\n", db)
        }()
    }
    wg.Wait()
}

sync.Once è thread-safe e più elegante rispetto a un mutex con double-check locking.

Context e cancellazione

16. A cosa serve il package context?

Il package context gestisce deadline, segnali di cancellazione e valori associati a una request lungo l'albero delle chiamate.

context.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // Context with timeout
    ctx, cancel := context.WithTimeout(
        context.Background(),
        2*time.Second,
    )
    defer cancel() // Always call cancel

    result := make(chan string, 1)

    go func() {
        // Simulate long operation
        time.Sleep(3 * time.Second)
        result <- "completed"
    }()

    select {
    case res := <-result:
        fmt.Println(res)
    case <-ctx.Done():
        fmt.Println("Timeout:", ctx.Err())
    }
}

// Propagation through functions
func fetchData(ctx context.Context, url string) ([]byte, error) {
    // Early check
    if ctx.Err() != nil {
        return nil, ctx.Err()
    }

    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }

    // HTTP client respects context
    resp, err := http.DefaultClient.Do(req)
    // ...
}

Ogni funzione potenzialmente lunga dovrebbe accettare un context.Context come primo parametro.

17. Come si gestisce un graceful shutdown del programma?

Segnali di sistema come SIGINT e SIGTERM possono essere catturati per consentire una chiusura pulita.

graceful_shutdown.gogo
package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    // Context cancelled on signal
    ctx, stop := signal.NotifyContext(
        context.Background(),
        syscall.SIGINT,
        syscall.SIGTERM,
    )
    defer stop()

    // Start server
    server := startServer()

    // Wait for signal
    <-ctx.Done()
    fmt.Println("\nShutting down...")

    // Timeout for graceful shutdown
    shutdownCtx, cancel := context.WithTimeout(
        context.Background(),
        5*time.Second,
    )
    defer cancel()

    if err := server.Shutdown(shutdownCtx); err != nil {
        fmt.Println("Shutdown error:", err)
    }

    fmt.Println("Shutdown complete")
}

Questo pattern garantisce che le connessioni attive si concludano correttamente prima della chiusura.

Test e benchmark

18. Come si scrivono i test in Go?

Il package integrato testing fornisce le funzionalità di base. I test risiedono in file *_test.go.

calculator_test.gogo
package calculator

import "testing"

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

// Table-driven tests
func TestAddTableDriven(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -1, -2},
        {"mixed", -1, 5, 4},
        {"zero", 0, 0, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

I table-driven test sono il pattern idiomatico in Go per provare più casi.

19. Come si scrivono i benchmark?

I benchmark usano testing.B e si eseguono con go test -bench.

benchmark_test.gogo
package main

import (
    "strings"
    "testing"
)

func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var s string
        for j := 0; j < 100; j++ {
            s += "a"
        }
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        for j := 0; j < 100; j++ {
            sb.WriteString("a")
        }
        _ = sb.String()
    }
}

// Typical results:
// BenchmarkStringConcat-8      50000    28000 ns/op
// BenchmarkStringBuilder-8   1000000     1200 ns/op

I benchmark mettono in luce le differenze di prestazioni tra implementazioni.

Generics (Go 1.18+)

20. Come si usano i generics in Go?

Go 1.18 ha introdotto i parametri di tipo, consentendo codice generico mantenendo la sicurezza dei tipi.

generics.gogo
package main

import "fmt"

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

// Custom type constraint
type Number interface {
    int | int64 | float64
}

func Sum[T Number](values []T) T {
    var sum T
    for _, v := range values {
        sum += v
    }
    return sum
}

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

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

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

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

    fmt.Println(Sum([]int{1, 2, 3, 4, 5})) // 15

    stack := &Stack[string]{}
    stack.Push("hello")
    stack.Push("world")
    val, _ := stack.Pop()
    fmt.Println(val) // "world"
}

I generics eliminano la necessità di duplicare il codice o di usare interface{}.

Module e dipendenze

21. Come funziona il sistema di moduli di Go?

I moduli Go gestiscono le dipendenze con il versionamento semantico. Il file go.mod definisce il modulo e le sue dipendenze.

go.mod examplego
module github.com/user/myproject

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/lib/pq v1.10.9
)

// Essential commands:
// go mod init github.com/user/project
// go mod tidy        - clean dependencies
// go get package@v1.2.3 - add/update
// go mod vendor      - copy locally
bash
# Updating dependencies
go get -u ./...           # All dependencies
go get -u=patch ./...     # Patches only

Il file go.sum contiene i checksum crittografici per garantire l'integrità delle dipendenze.

22. Come strutturare un progetto Go?

La struttura standard segue convenzioni della community senza imporre regole rigide.

text
myproject/
├── cmd/
│   └── api/
│       └── main.go      # Entry point
├── internal/            # Private to module
│   ├── handler/
│   ├── service/
│   └── repository/
├── pkg/                 # Reusable external code
├── go.mod
├── go.sum
└── README.md

La cartella internal è speciale: il suo contenuto non può essere importato da altri moduli.

Domande avanzate

23. Come funziona il garbage collector in Go?

Go utilizza un garbage collector concorrente, tricolore mark-and-sweep, ottimizzato per la bassa latenza.

gc_optimization.gogo
package main

import "runtime"

func main() {
    // GC configuration
    // GOGC=100 (default) - triggers GC when heap doubles

    // Force GC
    runtime.GC()

    // Memory statistics
    var stats runtime.MemStats
    runtime.ReadMemStats(&stats)

    println("Alloc:", stats.Alloc)
    println("NumGC:", stats.NumGC)
    println("PauseTotalNs:", stats.PauseTotalNs)
}

// Optimization techniques
// 1. Reuse allocations with sync.Pool
// 2. Pre-allocate slices with make([]T, 0, cap)
// 3. Avoid repeated string/[]byte conversions
// 4. Use pointers for large structs

La variabile d'ambiente GODEBUG=gctrace=1 mostra le tracce del GC.

24. Spiega lo scheduler di Go

Lo scheduler di Go usa un modello M:N che mappa N goroutine su M thread del sistema, con tre entità: G (goroutine), M (thread) e P (processore logico).

scheduler.gogo
package main

import (
    "fmt"
    "runtime"
)

func main() {
    // Number of logical processors (P)
    fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))

    // Number of active goroutines
    fmt.Println("NumGoroutine:", runtime.NumGoroutine())

    // Yield processor to other goroutines
    runtime.Gosched()

    // M:P:G model
    // - G: goroutine (lightweight stack ~2KB)
    // - M: OS thread (machine)
    // - P: logical processor (execution context)
    //
    // Each P has a local queue of Gs
    // Work stealing when queue is empty
}

Lo scheduler è preemptivo dalla versione 1.14, evitando che una goroutine monopolizzi un P.

25. Come si ottimizzano le prestazioni in Go?

L'ottimizzazione parte dal profiling per individuare i colli di bottiglia.

profiling.gogo
package main

import (
    "os"
    "runtime/pprof"
)

func main() {
    // CPU profiling
    f, _ := os.Create("cpu.prof")
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()

    // Code to profile...

    // Memory profiling
    mf, _ := os.Create("mem.prof")
    defer mf.Close()
    pprof.WriteHeapProfile(mf)
}

// Analysis: go tool pprof cpu.prof

// Common optimization techniques:
// 1. Avoid allocations in hot loops
// 2. Use sync.Pool for reusable objects
// 3. Prefer []byte over string for mutations
// 4. Use bufio for I/O
// 5. Batch database operations
Regola di ottimizzazione

Conviene misurare prima di ottimizzare. Il profiling spesso rivela sorprese sui veri colli di bottiglia.

Conclusione

Queste 25 domande coprono i concetti fondamentali valutati nei colloqui Go:

Checklist di preparazione:

  • ✅ Padronanza di goroutine e channel
  • ✅ Comprensione delle interfacce implicite
  • ✅ Gestione idiomatica degli errori
  • ✅ Uso corretto del context
  • ✅ Pattern di concorrenza (mutex, worker pool)
  • ✅ Test e benchmark
  • ✅ Conoscenza dei generics di Go 1.18+

La chiave per riuscire in un colloquio Go: dimostrare comprensione dei trade-off tra semplicità e prestazioni e sapere quando applicare ogni pattern di concorrenza.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

#go
#golang
#interview
#concurrency
#goroutines

Condividi

Articoli correlati