Top 25 Go-Interviewfragen: vollständiger Entwicklerleitfaden

Go-Interviews meistern mit den 25 häufigsten Fragen. Goroutinen, Channels, Schnittstellen und Concurrency-Muster mit Codebeispielen.

Go-Interviewfragen - vollständiger Vorbereitungsleitfaden

Technische Go-Interviews prüfen das Verständnis der Kernkonzepte der Sprache: Concurrency, Speicherverwaltung und idiomatische Muster. Dieser Leitfaden behandelt die 25 am häufigsten gestellten Fragen mit ausführlichen Antworten und Codebeispielen.

Interview-Tipp

Go legt Wert auf Einfachheit und Lesbarkeit. Interviewer bevorzugen knappe Antworten, die ein tiefes Verständnis zeigen, gegenüber übermäßig komplexen Lösungen.

Grundlagen der Sprache Go

1. Was ist der Unterschied zwischen var und :=?

Die Deklaration var erlaubt eine explizite Typangabe und funktioniert auf Paketebene. Der Operator := leitet den Typ automatisch ab, funktioniert aber nur innerhalb von Funktionen.

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

Innerhalb von Funktionen wird die Kurzform := aus Gründen der Knappheit bevorzugt, während var für Paketebene-Variablen weiterhin nötig ist.

2. Wie funktioniert das Typsystem von Go?

Go verwendet statische Typisierung mit Typinferenz. Die Sprache unterscheidet Werttypen (bei der Zuweisung kopiert) von Referenztypen (die die zugrunde liegende Struktur teilen).

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
}

Arrays sind Werttypen, während Slices, Maps und Channels Referenztypen sind.

3. Erkläre den Unterschied zwischen Arrays und Slices

Arrays haben eine zur Compile-Zeit festgelegte feste Größe. Slices sind dynamische Sichten auf ein zugrunde liegendes Array mit drei Komponenten: Pointer, Länge und Kapazität.

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

Slices sind in Go der bevorzugte Typ für dynamische Sammlungen.

4. Wie funktioniert die defer-Anweisung?

defer plant einen Funktionsaufruf für das Ende der umgebenden Funktion ein. Aufgeschobene Aufrufe werden gestapelt und in LIFO-Reihenfolge ausgeführt (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 garantiert die Ausführung sogar bei einem Panic, wodurch es ideal für die Freigabe von Ressourcen ist.

5. Was ist eine Schnittstelle in Go?

Eine Schnittstelle definiert eine Menge von Methoden. Jeder Typ, der diese Methoden implementiert, erfüllt die Schnittstelle implizit, ohne dass eine ausdrückliche Deklaration nötig ist.

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

Die implizite Schnittstellenimplementierung ermöglicht eine starke Entkopplung zwischen Paketen.

Concurrency und Goroutinen

6. Was ist eine Goroutine und worin unterscheidet sie sich von einem Thread?

Eine Goroutine ist ein leichter Thread, der von der Go-Runtime verwaltet wird. Sie verwendet wenige KB Stack (gegenüber mehreren MB eines Systemthreads), und der Go-Scheduler multiplext Tausende von Goroutinen auf wenige Systemthreads.

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")
}
Häufige Falle

Schleifenvariablen sollten stets per Wert an Goroutinen übergeben werden. Andernfalls könnten alle Goroutinen denselben Endwert erfassen.

7. Erkläre, wie Channels funktionieren

Channels ermöglichen die Kommunikation und Synchronisation zwischen Goroutinen. Sie können gepuffert (mit Kapazität) oder ungepuffert (synchron) sein.

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

Ungepufferte Channels garantieren Synchronisation, gepufferte Channels erlauben eine zeitliche Entkopplung.

8. Wie verwendet man select mit mehreren Channels?

select wartet gleichzeitig auf mehrere Channel-Operationen. Die zuerst bereite Operation wird ausgeführt, bei Gleichstand zufällig ausgewählt.

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 ist das grundlegende Werkzeug, um Concurrency in Go elegant zu steuern.

9. Wie verhindert man Race Conditions?

Race Conditions entstehen, wenn mehrere Goroutinen ohne Synchronisation auf gemeinsame Daten zugreifen. Go bietet mehrere Schutzmechanismen.

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

Die Compiler-Flag -race erkennt Race Conditions zur Laufzeit.

10. Erkläre das Worker-Pool-Muster

Das Worker-Pool-Muster begrenzt die Concurrency, indem eine feste Anzahl von Goroutinen Aufgaben aus einer Queue verarbeitet.

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

Dieses Muster verhindert den Speicher- und CPU-Mehraufwand, der durch das Erzeugen zu vieler Goroutinen entsteht.

Bereit für deine Go-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Fehlerbehandlung sowie panic/recover

11. Wie werden Fehler in Go behandelt?

Go verwendet explizite Rückgabewerte für Fehler, ohne Exceptions. Per Konvention ist error der zuletzt zurückgegebene Parameter.

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

Das Wrapping mit %w verkettet die Fehler und erhält gleichzeitig die Möglichkeit, den ursprünglichen Fehler zu prüfen.

12. Wann sollten panic und recover verwendet werden?

panic unterbricht den normalen Ablauf und wickelt den Stack ab. recover fängt das Panic in einem defer ab und ermöglicht die Fortsetzung der Ausführung.

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

Panic sollte nur für Programmierfehler verwendet werden (verletzte Invarianten). Für erwartete Fehler (fehlende Datei, Netzwerkprobleme) sollte stets ein error zurückgegeben werden.

Structs, Methoden und Embedding

13. Was ist der Unterschied zwischen Wert- und Pointer-Receivern?

Ein Wert-Receiver erhält eine Kopie des Structs, während ein Pointer-Receiver eine Referenz erhält und das Original verändern kann.

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
}

Regel: Wenn eine Methode einen Pointer-Receiver nutzt, sollten alle Methoden des Typs aus Konsistenzgründen ebenfalls Pointer-Receiver verwenden.

14. Wie funktioniert Embedding in Go?

Beim Embedding wird ein Typ in einen anderen aufgenommen und erbt dessen Methoden und Felder. Das ist keine klassische Vererbung, sondern Komposition.

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 ermöglicht flexible Kompositionen ohne die Starrheit klassischer Vererbung.

15. Wie wird das Singleton-Muster in Go umgesetzt?

Das Paket sync bietet sync.Once, um eine einmalige Ausführung der Initialisierung zu garantieren, auch bei nebenläufigen Goroutinen.

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 ist threadsicher und eleganter als ein Mutex mit Double-Check-Locking.

Context und Cancellation

16. Wozu dient das Paket context?

Das Paket context verwaltet Deadlines, Cancellation-Signale und an einen Request gebundene Werte über den Aufrufbaum hinweg.

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

Jede potenziell lang laufende Funktion sollte einen context.Context als ersten Parameter akzeptieren.

17. Wie wird ein Graceful Shutdown des Programms umgesetzt?

Systemsignale wie SIGINT und SIGTERM lassen sich abfangen, um ein sauberes Beenden zu ermöglichen.

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

Dieses Muster sorgt dafür, dass aktive Verbindungen vor dem Beenden ordnungsgemäß abgeschlossen werden.

Tests und Benchmarks

18. Wie werden Tests in Go geschrieben?

Das eingebaute Paket testing stellt die Grundfunktionalität bereit. Tests befinden sich in *_test.go-Dateien.

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

Tabellengetriebene Tests sind in Go das idiomatische Muster, um mehrere Fälle zu prüfen.

19. Wie werden Benchmarks geschrieben?

Benchmarks verwenden testing.B und werden mit go test -bench ausgeführt.

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

Benchmarks zeigen Performance-Unterschiede zwischen Implementierungen auf.

Generics (Go 1.18+)

20. Wie werden Generics in Go verwendet?

Go 1.18 hat Typparameter eingeführt, die generischen Code bei gleichzeitiger Typsicherheit ermöglichen.

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 machen das Duplizieren von Code oder den Einsatz von interface{} überflüssig.

Module und Abhängigkeiten

21. Wie funktioniert das Modulsystem von Go?

Go-Module verwalten Abhängigkeiten mit semantischer Versionierung. Die Datei go.mod definiert das Modul und seine Abhängigkeiten.

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

Die Datei go.sum enthält kryptografische Prüfsummen, um die Integrität der Abhängigkeiten zu sichern.

22. Wie sollte ein Go-Projekt strukturiert werden?

Die Standardstruktur folgt Konventionen der Community ohne strenge Regeln vorzugeben.

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

Der Ordner internal ist besonders: Sein Inhalt darf nicht von anderen Modulen importiert werden.

Fortgeschrittene Fragen

23. Wie funktioniert der Garbage Collector in Go?

Go verwendet einen nebenläufigen, dreifarbigen Mark-and-Sweep-Garbage-Collector, der auf geringe Latenz optimiert ist.

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

Die Umgebungsvariable GODEBUG=gctrace=1 zeigt GC-Traces an.

24. Erkläre den Go-Scheduler

Der Go-Scheduler verwendet ein M:N-Modell, das N Goroutinen auf M Systemthreads abbildet, mit drei Entitäten: G (Goroutine), M (Thread), P (logischer Prozessor).

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
}

Der Scheduler ist seit Go 1.14 präemptiv und verhindert, dass eine Goroutine ein P monopolisiert.

25. Wie wird die Performance in Go optimiert?

Die Optimierung beginnt mit Profiling, um Engpässe zu identifizieren.

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
Optimierungsregel

Vor dem Optimieren sollte gemessen werden. Profiling liefert oft überraschende Ergebnisse über die tatsächlichen Engpässe.

Fazit

Diese 25 Fragen decken die grundlegenden Konzepte ab, die in Go-Interviews geprüft werden:

Vorbereitungs-Checkliste:

  • ✅ Beherrschung von Goroutinen und Channels
  • ✅ Verständnis impliziter Schnittstellen
  • ✅ Idiomatische Fehlerbehandlung
  • ✅ Sachgerechte Nutzung von context
  • ✅ Concurrency-Muster (Mutex, Worker Pool)
  • ✅ Tests und Benchmarks
  • ✅ Kenntnis der Generics ab Go 1.18

Der Schlüssel zum Erfolg im Go-Interview: das Verständnis der Trade-offs zwischen Einfachheit und Performance zeigen und wissen, wann welches Concurrency-Muster angebracht ist.

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#go
#golang
#interview
#concurrency
#goroutines

Teilen

Verwandte Artikel