25 najczęstszych pytań rekrutacyjnych Go: kompletny przewodnik dewelopera

Opanuj rozmowy o pracę z Go, znając 25 najczęstszych pytań. Goroutiny, kanały, interfejsy i wzorce współbieżności z przykładami kodu.

Pytania rekrutacyjne Go - Kompletny przewodnik przygotowawczy

Rozmowy techniczne z Go sprawdzają znajomość kluczowych koncepcji języka: współbieżności, zarządzania pamięcią i wzorców idiomatycznych. Ten przewodnik obejmuje 25 najczęstszych pytań ze szczegółowymi odpowiedziami i przykładami kodu.

Wskazówka na rozmowę

Go ceni prostotę i czytelność. Rekruterzy wolą zwięzłe odpowiedzi pokazujące głębokie zrozumienie niż nadmiernie złożone rozwiązania.

Podstawy języka Go

1. Czym różni się var od :=?

Deklaracja var pozwala podać typ jawnie i działa również na poziomie pakietu. Operator := wnioskuje typ automatycznie, ale działa wyłącznie wewnątrz funkcji.

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
    )
}

Skrócona deklaracja := jest preferowana wewnątrz funkcji ze względu na zwięzłość, podczas gdy var pozostaje konieczna dla zmiennych na poziomie pakietu.

2. Jak działa system typów w Go?

Go używa typowania statycznego z wnioskowaniem typów. Język rozróżnia typy wartości (kopiowane przy przypisaniu) od typów referencyjnych (współdzielących leżącą pod spodem strukturę).

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
}

Tablice są typami wartości, natomiast slice'y, mapy i kanały są typami referencyjnymi.

3. Wyjaśnij różnicę między tablicami a slice'ami

Tablice mają stały rozmiar ustalany w czasie kompilacji. Slice'y to dynamiczne widoki nad tablicą bazową, składające się z trzech elementów: wskaźnika, długości i pojemności.

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)
}

Slice'y są w Go preferowanym typem dla dynamicznych kolekcji.

4. Jak działa instrukcja defer?

defer planuje wywołanie funkcji na koniec funkcji otaczającej. Odroczone wywołania są umieszczane na stosie i wykonywane w kolejności LIFO.

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 gwarantuje wykonanie nawet podczas paniki, dzięki czemu świetnie nadaje się do zwalniania zasobów.

5. Czym jest interfejs w Go?

Interfejs definiuje zestaw metod. Każdy typ, który je implementuje, automatycznie spełnia interfejs, bez deklaracji jawnej.

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)
    }
}

Domyślna implementacja interfejsów umożliwia silne rozdzielenie pakietów.

Współbieżność i goroutiny

6. Czym jest goroutina i czym różni się od wątku?

Goroutina to lekki wątek zarządzany przez runtime Go. Używa kilku KB stosu (w porównaniu do kilku MB wątku systemowego), a scheduler Go multipleksuje tysiące goroutin na garstce wątków systemowych.

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")
}
Częsta pułapka

Zmienne pętli zawsze warto przekazywać do goroutin przez wartość. Inaczej wszystkie goroutiny mogą uchwycić tę samą wartość końcową.

7. Wyjaśnij, jak działają kanały

Kanały zapewniają komunikację i synchronizację między goroutinami. Mogą być buforowane (z pojemnością) lub niebuforowane (synchroniczne).

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"
}

Kanały niebuforowane gwarantują synchronizację, a kanały buforowane pozwalają na czasowe rozłączenie nadawcy i odbiorcy.

8. Jak używa się select z wieloma kanałami?

select czeka jednocześnie na kilka operacji na kanałach. Wykonywana jest pierwsza gotowa operacja, a w razie remisu wybór jest losowy.

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 to fundamentalne narzędzie do eleganckiego zarządzania współbieżnością w Go.

9. Jak unikać race conditions?

Race conditions powstają, gdy kilka goroutin jednocześnie korzysta ze wspólnych danych bez synchronizacji. Go oferuje kilka mechanizmów ochrony.

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)
}

Flaga kompilatora -race wykrywa race conditions w czasie wykonania.

10. Wyjaśnij wzorzec worker pool

Wzorzec worker pool ogranicza współbieżność, tworząc stałą liczbę goroutin przetwarzających zadania z kolejki.

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)
    }
}

Ten wzorzec zapobiega obciążeniu pamięci i CPU spowodowanemu tworzeniem zbyt wielu goroutin.

Gotowy na rozmowy o Go?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Obsługa błędów oraz panic/recover

11. Jak obsługuje się błędy w Go?

Go używa jawnych wartości zwracanych dla błędów, bez wyjątków. Zgodnie z konwencją error jest ostatnim zwracanym parametrem.

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)
}

Owijanie z %w łączy błędy w łańcuch i pozostawia możliwość sprawdzenia oryginalnego błędu.

12. Kiedy używać panic i recover?

panic przerywa normalny przepływ i odwija stos. recover przechwytuje panikę w defer i pozwala kontynuować wykonanie.

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")
}
Złota zasada

Panic stosuje się tylko do błędów programistycznych (naruszone niezmienniki). Dla błędów oczekiwanych (brak pliku, problem sieciowy) lepiej zawsze zwrócić error.

Struktury, metody i embedding

13. Jaka jest różnica między receiverami wartościowymi a wskaźnikowymi?

Receiver wartościowy otrzymuje kopię struktury, podczas gdy receiver wskaźnikowy dostaje referencję i może modyfikować oryginał.

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
}

Zasada: jeśli jedna metoda używa receivera wskaźnikowego, wszystkie metody danego typu powinny go używać dla zachowania spójności.

14. Jak działa embedding w Go?

Embedding osadza jeden typ wewnątrz drugiego, dziedzicząc jego metody i pola. Nie jest to klasyczne dziedziczenie, lecz kompozycja.

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"
}

Embedding pozwala na elastyczne kompozycje, omijając sztywność dziedziczenia.

15. Jak zaimplementować wzorzec singleton w Go?

Pakiet sync udostępnia sync.Once, który gwarantuje pojedyncze wykonanie inicjalizacji nawet w przypadku współbieżnych goroutin.

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 jest bezpieczny w środowisku współbieżnym i bardziej elegancki niż mutex z double-check locking.

Context i anulowanie

16. Do czego służy pakiet context?

Pakiet context zarządza terminami, sygnałami anulowania i wartościami związanymi z zapytaniem w całym drzewie wywołań.

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)
    // ...
}

Każda potencjalnie długo działająca funkcja powinna przyjmować context.Context jako pierwszy parametr.

17. Jak obsłużyć graceful shutdown programu?

Sygnały systemowe takie jak SIGINT i SIGTERM można przechwycić, aby pozwolić na czyste zakończenie pracy.

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")
}

Ten wzorzec pozwala aktywnym połączeniom zakończyć się prawidłowo przed wyłączeniem serwera.

Testy i benchmarki

18. Jak pisze się testy w Go?

Wbudowany pakiet testing zapewnia podstawową funkcjonalność. Testy znajdują się w plikach *_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)
            }
        })
    }
}

Testy tablicowe (table-driven) są w Go idiomatycznym wzorcem do sprawdzania wielu przypadków.

19. Jak pisze się benchmarki?

Benchmarki używają testing.B i są uruchamiane przez 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

Benchmarki ujawniają różnice w wydajności między implementacjami.

Generics (Go 1.18+)

20. Jak korzystać z generics w Go?

Go 1.18 wprowadziło parametry typu, dzięki czemu można pisać kod generyczny z zachowaniem bezpieczeństwa typów.

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"
}

Generics eliminują potrzebę duplikowania kodu lub używania interface{}.

Moduły i zależności

21. Jak działa system modułów Go?

Moduły Go zarządzają zależnościami z wersjonowaniem semantycznym. Plik go.mod definiuje moduł i jego zależności.

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

Plik go.sum zawiera kryptograficzne sumy kontrolne, które gwarantują integralność zależności.

22. Jak strukturyzować projekt Go?

Standardowa struktura wynika z konwencji społeczności i nie narzuca sztywnych zasad.

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

Folder internal jest specjalny: jego zawartość nie może być importowana przez inne moduły.

Pytania zaawansowane

23. Jak działa garbage collector w Go?

Go używa współbieżnego, trójkolorowego garbage collectora typu mark-and-sweep, zoptymalizowanego pod kątem niskich opóźnień.

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

Zmienna środowiskowa GODEBUG=gctrace=1 pokazuje ślady GC.

24. Wyjaśnij scheduler w Go

Scheduler Go używa modelu M:N, mapując N goroutin na M wątków systemowych z trzema bytami: G (goroutina), M (wątek) i P (procesor logiczny).

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
}

Od wersji 1.14 scheduler jest preemptywny, co zapobiega monopolizacji P przez jedną goroutinę.

25. Jak optymalizować wydajność w Go?

Optymalizacja zaczyna się od profilingu, aby zidentyfikować wąskie gardła.

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
Zasada optymalizacji

Najpierw mierzyć, potem optymalizować. Profiling często ujawnia zaskakujące wnioski o prawdziwych wąskich gardłach.

Podsumowanie

Te 25 pytań obejmuje fundamentalne pojęcia oceniane na rozmowach Go:

Lista kontrolna przygotowań:

  • ✅ Opanowanie goroutin i kanałów
  • ✅ Zrozumienie domyślnych interfejsów
  • ✅ Idiomatyczna obsługa błędów
  • ✅ Poprawne użycie context
  • ✅ Wzorce współbieżności (mutex, worker pool)
  • ✅ Testy i benchmarki
  • ✅ Znajomość generics z Go 1.18+

Klucz do sukcesu na rozmowie Go: pokazać zrozumienie kompromisów między prostotą a wydajnością i wiedzieć, kiedy stosować dany wzorzec współbieżności.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#go
#golang
#interview
#concurrency
#goroutines

Udostępnij

Powiązane artykuły