Příprava na pohovor z Go

Otázky na pohovor pro backendové vývojáře v Go

Vybrané otázky na pohovor z Go pro backendové vývojáře, seskupené podle témat a generované ze stejného katalogu otázek, který pohání procvičování na EngineerSpeak.

Spustit backendový AI pohovor z GoNení vyžadována platební karta. K dispozici je 1 bezplatná relace.
Příprava na technické pohovory v angličtiněRežim pro lidi, pro které angličtina není rodným jazykem, určený k procvičování technických pohovorů.

Typový systém

1Vysvětlete, jak fungují nulové hodnoty (zero values) v jazyce Go pro vestavěné a referenční typy a proč jsou důležité při deklaraci proměnných bez explicitní inicializace.

V jazyce Go je proměnná deklarovaná bez explicitního inicializátoru automaticky inicializována na nulovou hodnotu (zero value) svého typu. Číselné typy se nastaví na 0, bool na false, string na "" a pole nebo struktury (structs) jsou vynulovány prvek po prvku nebo položka po položce. Typy fungující jako ukazatele nebo reference, jako jsou ukazatele, řezy (slices), mapy, kanály (channels), funkce a rozhraní (interfaces), mají jako svou nulovou hodnotu nil. Je to důležité, protože proměnné v Go a vynechané položky struktur začínají v deterministickém stavu, místo aby obsahovaly nesmyslná data v paměti (garbage). Mnoho API (Application Programming Interfaces) je navíc navrženo tak, že nulová hodnota představuje užitečnou výchozí hodnotu, ačkoli některé hodnoty nil stále vyžadují před určitými operacemi dodatečnou inicializaci.

package main

import "fmt"

type Config struct {
	Port    int
	Debug   bool
	Name    string
	Tags    []string
	Options map[string]string
}

func main() {
	var c Config
	fmt.Printf("port=%d debug=%v name=%q tags==nil:%v options==nil:%v\n",
		c.Port, c.Debug, c.Name, c.Tags == nil, c.Options == nil)
}
Zkusit odpovědět na tuto otázku s trenérem AI

2Jak Go řeší rovnost u struktur a co se stane, když struktura obsahuje neporovnatelné položky?

Hodnoty struktur v Go lze porovnávat operátory `==` a `!=` pouze tehdy, když je každá položka struktury porovnatelná. Rovnost porovnává odpovídající položky pomocí jejich vlastních pravidel pro rovnost. Pokud struktura obsahuje neporovnatelnou položku, jako je řez (slice), mapa nebo funkce, typ struktury není porovnatelný a porovnání dvou hodnot tohoto typu operátorem `==` vede k chybě při překladu (compile-time error). Pro takové struktury použijte vlastní logiku porovnávání nebo vhodnou pomocnou funkci pro hluboké porovnání (deep-equality), zejména v testech.

package main

import "fmt"

type UserID struct {
    Name string
    Age  int
}

type UserProfile struct {
    Name string
    Tags []string
}

func main() {
    a := UserID{"Ann", 30}
    b := UserID{"Ann", 30}
    fmt.Println(a == b)

    x := UserProfile{Name: "Ann", Tags: []string{"go"}}
    y := UserProfile{Name: "Ann", Tags: []string{"go"}}
    _, _ = x, y
    // fmt.Println(x == y) // compile error: struct containing []string cannot be compared
}
Zkusit odpovědět na tuto otázku s trenérem AI

3Vysvětlete neměnnost (immutability) řetězců v Go a vztah mezi `string`, `[]byte`, `bytes.Buffer` a `strings.Builder`.

Řetězec (`string`) v Go je neměnná sekvence bajtů, často text v kódování UTF-8, i když to není podmínkou. Řetězec nelze měnit na místě (in-place); pro změnu obsahu jej obvykle převedete na `[]byte` pro úpravy na úrovni bajtů nebo na `[]rune` pro úpravy kódových bodů (code points) a následně jej převedete zpět. Běžné převody mezi `string` a `[]byte` kopírují data a mohou alokovat paměť, takže opakované převody nebo zřetězování ve smyčkách mohou být výkonnostně náročné. Typ `strings.Builder` je optimalizován pro efektivní sestavování řetězců, zatímco `bytes.Buffer` je měnitelný bajtový buffer vhodný pro bajtově orientovaná data a I/O operace, který také dokáže vygenerovat řetězec.

package main

import (
    "bytes"
    "fmt"
    "strings"
)

func main() {
    var sb strings.Builder
    sb.WriteString("hello")
    sb.WriteByte(' ')
    sb.WriteString("world")
    fmt.Println(sb.String())

    var buf bytes.Buffer
    buf.Write([]byte{0x48, 0x69})
    buf.WriteByte('!')
    fmt.Println(buf.String())

    s := "cat"
    b := []byte(s) // normally copies
    b[0] = 'b'
    fmt.Println(s, string(b))
}
Zkusit odpovědět na tuto otázku s trenérem AI

4Vysvětlete, jak v jazyce Go funguje hodnota nil odlišně pro ukazatele, řezy (slices), mapy, kanály (channels), funkce a rozhraní (interfaces).

V jazyce Go je nil nulovou hodnotou (zero value) pro ukazatele, řezy (slices), mapy, kanály (channels), funkce a rozhraní (interfaces), ale operace nad těmito hodnotami nil se liší podle typu. Ukazatel na nil lze s nil porovnávat, ale jeho dereference vyvolá paniku (panic). Řez s hodnotou nil má délku i kapacitu 0, lze přes něj iterovat a přidávat do něj prvky. Z mapy s hodnotou nil lze číst a iterovat přes ni, ale přiřazení do ní způsobí paniku. Odesílání do kanálu s hodnotou nil nebo přijímání z něj blokuje donekonečna a uzavření takového kanálu vyvolá paniku. Volání funkce s hodnotou nil rovněž způsobí paniku. Rozhraní (interface) je nil pouze tehdy, když nemá žádný dynamický typ ani dynamickou hodnotu; rozhraní obsahující typovou hodnotu nil, například nil ukazatel, samo o sobě nil není.

package main

import "fmt"

type MyErr struct{}

func (*MyErr) Error() string { return "my error" }

func returnsTypedNil() error {
	var e *MyErr = nil
	return e
}

func main() {
	var p *int = nil
	var s []int = nil
	var m map[string]int = nil
	var err error = returnsTypedNil()

	fmt.Println(p == nil)
	fmt.Println(len(s), cap(s), s == nil)
	fmt.Println(m["missing"])
	fmt.Println(err == nil)
}
Zkusit odpovědět na tuto otázku s trenérem AI

5Co jsou porovnatelné typy v Go a jak pravidla pro porovnatelnost ovlivňují klíče map, rovnost a omezení generických typů (generics constraints)?

Porovnatelné typy v Go jsou typy, jejichž hodnoty lze porovnávat operátory `==` a `!=`. Základní typy, ukazatele, kanály, rozhraní (interfaces) a struktury nebo pole, jejichž položky či prvky jsou porovnatelné, jsou rovněž porovnatelné; řezy (slices), mapy a funkce porovnatelné nejsou, s výjimkou porovnání s `nil`. Klíče map musí být porovnatelné. Rovnost se řídí pravidly porovnávání daného typu a porovnání rozhraní závisí na dynamických konkrétních hodnotách; pokud porovnávané rozhraní obsahuje neporovnatelnou dynamickou hodnotu, porovnání vyvolá výjimku panic. V generikách umožňuje předdefinované omezení (constraint) `comparable` porovnávat parametry typu pomocí `==`/`!=` a používat je jako klíče v mapách.

package main

import "fmt"

type Point struct{ X, Y int }      // comparable
type Bag struct{ Items []string }  // not comparable because of slice field

func Contains[T comparable](xs []T, target T) bool {
    for _, x := range xs {
        if x == target {
            return true
        }
    }
    return false
}

func main() {
    p1, p2 := Point{1, 2}, Point{1, 2}
    fmt.Println(p1 == p2)

    m := map[Point]string{p1: "value"}
    fmt.Println(m[p2])

    fmt.Println(Contains([]string{"a", "b"}, "b"))

    var a any = []int{1}
    var b any = []int{1}
    _, _ = a, b
    // fmt.Println(a == b) // panic: comparing uncomparable type []int
}
Zkusit odpovědět na tuto otázku s trenérem AI

6Jak v Go reprezentujeme bajty, runy (runes) a text v kódování UTF-8, a proč se `len(s)` může lišit od počtu viditelných znaků pro uživatele?

V Go je `byte` alias pro `uint8` a představuje jeden surový bajt, zatímco `rune` je alias pro `int32` a představuje kódový bod (code point) v Unicode. Řetězec (`string`) je sekvence bajtů určená pouze pro čtení, obvykle obsahující text v kódování UTF-8, ale může obsahovat libovolné bajty. Funkce `len(s)` vrací počet bajtů, nikoliv run nebo znaků viditelných pro uživatele. Indexování řetězce vrací bajt; iterace přes řetězec dekóduje UTF-8 a vrací indexy bajtů a příslušné runy. Hodnota `len(s)` se může lišit od počtu viditelných znaků, protože UTF-8 může na jeden kódový bod použít více bajtů, a také proto, že jeden znak viditelný pro uživatele se může skládat z několika kódových bodů, jako jsou například kombinující značky (combining marks) nebo sekvence emotikonů (emoji).

package main

import "fmt"

func main() {
    s := "é🙂"

    fmt.Println(len(s))         // bytes: é is 2 bytes, 🙂 is 4 bytes
    fmt.Println(len([]rune(s))) // Unicode code points

    fmt.Printf("first byte: %x\n", s[0])

    for i, r := range s {
        fmt.Printf("byte index %d: %q U+%04X\n", i, r, r)
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

Datové struktury

7Popište rozdíl mezi poli (arrays) a řezy (slices) v Go, včetně toho, jak se chová jejich délka, kapacita a podkladové úložiště.

Pole (array) má v Go pevnou délku, která je součástí jeho typu, například `[3]int`; ukládá své prvky přímo a přiřazení nebo předání pole zkopíruje celou jeho hodnotu. Řez (slice), například `[]int`, je malý deskriptor nad podkladovým polem: koncepčně obsahuje ukazatel na prvky, délku a kapacitu. Délka řezu je počet viditelných prvků; jeho kapacita určuje, kolik prvků lze použít od začátku řezu, než se dosáhne konce podkladového pole. Řezy jsou flexibilní: opětovné vytvoření řezu (reslicing) změní deskriptor a funkce `append` může znovu použít stejné podkladové pole, pokud to kapacita dovoluje, nebo alokovat nové, pokud ne.

package main

import "fmt"

func main() {
	a := [3]int{1, 2, 3}
	b := a
	b[0] = 99
	fmt.Println(a, b)

	s := []int{1, 2, 3}
	t := s
	t[0] = 99
	fmt.Println(s, t)
	fmt.Println(len(s), cap(s))
}
Zkusit odpovědět na tuto otázku s trenérem AI

8Jak se chová typ map v jazyce Go, pokud jde o typy klíčů, chybějící klíče, mapy s hodnotou nil a pořadí iterace?

Typy klíčů mapy v jazyce Go musí být porovnatelné (comparable); řezy (slices), mapy a funkce nelze použít přímo jako klíče. Vyhledání klíče, který není přítomen, vrací nulovou hodnotu daného typu prvku (zero value), proto se používá forma comma-ok (`v, ok := m[k]`) k rozlišení nepřítomnosti od přítomné nulové hodnoty. Z mapy s hodnotou `nil` lze číst a lze přes ni iterovat pomocí `range`, ale přiřazení do ní vyvolá chybu `panic`; před zápisem ji proto inicializujte. Pořadí iterace mapy není specifikováno a kód na něm nesmí záviset.

package main

import "fmt"

func main() {
    counts := map[string]int{"a": 0, "b": 2}

    fmt.Println(counts["missing"]) // zero value for int

    v, ok := counts["a"]
    fmt.Println(v, ok) // present even though value is zero

    var m map[string]int
    fmt.Println(m["x"]) // read from nil map is OK
    // m["x"] = 1     // panic: assignment to entry in nil map

    m = make(map[string]int)
    m["x"] = 1

    for k, v := range counts {
        fmt.Println(k, v) // order is not guaranteed
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

9Popište, jak může opětovné vytvoření řezu (reslicing) a přiřazení způsobit, že více řezů bude sdílet stejné podkladové pole, a jaké chyby to může způsobit.

Hodnota řezu je hlavička ukazující do podkladového pole. Přiřazení řezu nebo jeho předání funkci zkopíruje pouze tuto hlavičku, nikoli samotné prvky. Opětovné vytvoření řezu (reslicing) vytvoří další hlavičku ukazující na rozsah stejného podkladového pole. Proto může více řezů odkazovat na stejné úložiště: změna prvku prostřednictvím jednoho řezu může být viditelná v jiném a použití funkce `append` na jeden řez může přepsat data viditelná pro jiný řez, pokud má stále volnou kapacitu. Mezi chyby patří neočekávané mutace, poškozené výsledky, zadržování velkých podkladových polí v paměti kvůli malým podřízeným řezům (subslices) a souběhy dat (data races), když jsou aliasy používány souběžně. Chcete-li se vyhnout nechtěnému sdílení, vytvořte defenzivní kopii pomocí funkce `copy` nebo `append([]T(nil), s...)`, případně omezte kapacitu pomocí výrazu pro úplný řez (full-slice expression) před voláním `append`.

package main

import "fmt"

func main() {
	base := []int{1, 2, 3, 4}
	a := base[:2] // len 2, cap 4
	b := base[2:] // len 2, cap 2

	a[0] = 99
	fmt.Println(base, a, b)

	a = append(a, 77) // reuses base's backing array, overwrites base[2]
	fmt.Println(base, a, b)

	c := append([]int(nil), base[:2]...) // defensive copy
	c[0] = 42
	fmt.Println(base, c)
}
Zkusit odpovědět na tuto otázku s trenérem AI

10Vysvětlete koncept růstu řezu (slice) během operace append a výkonnostní důsledky opakované realokace.

Když `append` přidává prvky do řezu (slice), zapisuje do existujícího podkladového pole, pokud má řez dostatečnou kapacitu. Pokud je kapacita nedostatečná, Go alokuje větší podkladové pole, zkopíruje stávající prvky, zapíše nové prvky a vrátí hlavičku řezu (slice header) ukazující na nové úložiště. Přesná strategie růstu závisí na implementaci, ale koncepčně roste kapacita natolik, aby byly opakované operace `append` z hlediska amortizované složitosti efektivní. Opakované realokace stále spotřebovávají čas procesoru na kopírování, vytvářejí alokace, zvyšují zátěž GC (Garbage Collector) a mohou rozbít sdílení se starými aliasy řezu. Pokud znáte očekávanou velikost, předem proveďte alokaci pomocí `make([]T, 0, n)` při budování pomocí `append` nebo `make([]T, n)` při plnění pomocí indexu, abyste omezili realokace.

package main

func collectNoPrealloc(input []int) []int {
	var out []int
	for _, v := range input {
		out = append(out, v*2)
	}
	return out
}

func collectPrealloc(input []int) []int {
	out := make([]int, 0, len(input))
	for _, v := range input {
		out = append(out, v*2)
	}
	return out
}
Zkusit odpovědět na tuto otázku s trenérem AI

Sémantika jazyka

11Jak v Go funguje prázdný identifikátor (blank identifier) pro nevyužité hodnoty, importy a kontroly rozhraní v době překladu?

Prázdný identifikátor (blank identifier) `_` funguje jako zástupný symbol pouze pro zápis (write-only). Přiřazení do tohoto identifikátoru hodnotu zahodí a nevytvoří použitelnou proměnnou. Používá se k ignorování nepotřebných návratových hodnot nebo proměnných v cyklech, k importu balíčku pouze kvůli jeho vedlejším efektům pomocí `import _ "pkg"`, a k ověření implementace rozhraní (interface) v době překladu, například `var _ io.Reader = (*MyReader)(nil)`. Tzv. prázdný import (blank import) stále spouští inicializaci importovaného balíčku. Přiřazení sloužící ke kontrole rozhraní selže při kompilaci, pokud `method set` konkrétního typu dané rozhraní nesplňuje.

package main

import "fmt"

func lookup() (string, bool) {
    return "gopher", true
}

func main() {
    name, _ := lookup() // ignore the bool
    fmt.Println(name)

    for i, _ := range []int{10, 20} {
        fmt.Println(i)
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

Balíčky

12Jak funguje pořadí inicializace balíčků v Go, včetně funkcí `init` a importovaných závislostí?

Go inicializuje balíčky v pořadí jejich závislostí. Importované závislosti balíčku jsou inicializovány před samotným importujícím balíčkem. V rámci balíčku jsou proměnné na úrovni balíčku inicializovány před jakýmikoli funkcemi `init`, přičemž inicializace proměnných je seřazena podle závislostí a pořadí deklarace, jak to definuje jazyk. Následně se automaticky spustí funkce `init` balíčku; balíček může mít více funkcí `init` a nelze je volat přímo. Každý balíček je inicializován pouze jednou. Pro spustitelný program je nejprve inicializován graf importů, poté je inicializován balíček `main` a nakonec je zavolána funkce `main.main`.

// a/a.go
package a

import "fmt"

var V = func() int {
    fmt.Println("a var")
    return 1
}()

func init() { fmt.Println("a init") }

// main.go
package main

import (
    "fmt"
    "example/a"
)

var M = func() int {
    fmt.Println("main var", a.V)
    return 2
}()

func init() { fmt.Println("main init") }

func main() { fmt.Println("main") }
Zkusit odpovědět na tuto otázku s trenérem AI

13Vysvětlete pravidla viditelnosti balíčků v jazyce Go, včetně exportovaných identifikátorů a konvence adresáře internal/.

V jazyce Go je viditelnost v rámci balíčku řízena pojmenováním identifikátorů, nikoliv přístupovými modifikátory. Identifikátor, jehož název začíná velkým písmenem ze sady kódování Unicode, je exportován a může být odkazován z jiných balíčků; ostatní identifikátory jsou neexportované a lze je použít pouze v rámci stejného balíčku. To platí pro funkce, typy, metody, proměnné, konstanty a pole struktur (struct fields). Balíčky využívají exportované identifikátory k definování svého veřejného API (Application Programming Interface) a uchovávají detaily implementace jako neexportované. Kromě toho platí, že balíček umístěný v adresáři internal/ smí být importován pouze kódem, jehož cesta importu se nachází v rodičovském stromu daného adresáře internal; to je vynucováno sadou nástrojů jazyka Go (Go toolchain).

// module example.com/app

// internal/store/store.go
package store

type Client struct { // exported type
    dsn string // unexported field
}

func New(dsn string) *Client { return &Client{dsn: dsn} } // exported
func parseDSN(s string) string { return s }                // unexported

// cmd/api/main.go -- allowed: inside example.com/app tree
package main

import "example.com/app/internal/store"

func main() {
    c := store.New("db")
    _ = c
    // c.dsn is not accessible here: field is unexported.
}

// Code outside example.com/app cannot import example.com/app/internal/store.
Zkusit odpovědět na tuto otázku s trenérem AI

Řízení toku programu

14Jak funguje `defer` v Go, včetně pořadí provádění, doby vyhodnocení argumentů a interakce s návratovými hodnotami?

Příkaz `defer` naplánuje volání funkce tak, aby se spustilo při opouštění nadřazené funkce, ať už tato končí běžným návratem (return) nebo rozbalováním při chybě (panic unwinding). Více odložených volání se provádí v pořadí „poslední dovnitř, první ven“ (LIFO). Hodnota odložené funkce a její argumenty jsou vyhodnoceny okamžitě při provedení příkazu `defer`, ale samotné volání proběhne až později. V případě pojmenovaných návratových hodnot (named return values) příkaz `return` nejprve přiřadí návratové hodnoty a teprve poté se spustí odložené funkce, takže odložená uzávěrka (closure) může pozorovat nebo upravovat pojmenované proměnné výsledku ještě předtím, než je volající obdrží. Díky tomu je `defer` mimořádně užitečný pro úklidové operace, jako je zavírání souborů, odemykání mutexů a uvolňování prostředků.

package main

import "fmt"

func f() (result int) {
    x := 1
    defer fmt.Println("arg evaluated now:", x)
    defer func() {
        result++
        fmt.Println("deferred closure sees x later:", x)
    }()
    x = 2
    return 10
}

func main() {
    fmt.Println("return:", f())
}
Zkusit odpovědět na tuto otázku s trenérem AI

Zpracování chyb

15Vysvětlete model zpracování chyb v jazyce Go a konvenční způsoby, jak jsou chyby vytvářeny, vraceny a kontrolovány.

Jazyk Go zachází s chybami jako s běžnými hodnotami, nikoli jako s výjimkami. Vestavěné rozhraní `error` je splněno jakýmkoli typem, který obsahuje metodu `Error() string`. Funkce obvykle konvenčně vrací `error` jako svůj poslední výsledek, přičemž `nil` znamená úspěch a nenulová (non-nil) chyba znamená, že volající musí selhání zpracovat nebo jej předat dále. Jednoduché chyby se běžně vytvářejí pomocí `errors.New`, formátované chyby pomocí `fmt.Errorf` a volající obvykle provádí kontrolu pomocí `if err != nil { ... }`.

package main

import (
	"errors"
	"fmt"
)

func findUser(id int) (string, error) {
	if id <= 0 {
		return "", errors.New("invalid user id")
	}
	if id == 42 {
		return "", fmt.Errorf("user %d not found", id)
	}
	return "alice", nil
}

func handler(id int) error {
	name, err := findUser(id)
	if err != nil {
		return fmt.Errorf("find user: %w", err)
	}
	fmt.Println(name)
	return nil
}
Zkusit odpovědět na tuto otázku s trenérem AI

16Jak by mělo být v backendových službách v Go ošetřeno zotavení z paniky (panic recovery), včetně toho, co se stane, když v `goroutine` dojde k panice, a kdy by se měl proces zotavit versus spadnout (crash)?

Panika (panic) rozvine (unwinds) aktuální `goroutine` a spustí její odložené funkce (deferred functions). Funkce `recover` funguje pouze tehdy, když je zavolána z odložené funkce ve stejné `goroutine`; jedna `goroutine` nemůže zachytit paniku jiné `goroutine`. Pokud panika není zachycena, proces spadne. V backendových službách by zotavení mělo být obvykle umístěno na hranicích izolace, jako jsou obslužné rutiny požadavků (request handlers), RPC (Remote Procedure Call) middleware nebo vstupní body pracovních `goroutine` (worker goroutines), aby jeden selhávající požadavek nebo úloha neshodil celou službu. Pokud však panika mohla poškodit sdílený stav nebo způsobit nedůvěryhodnost integrity procesu, je bezpečnější nechat proces spadnout a restartovat jej, než se zotavit a slepě pokračovat.

func Recover(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("panic: %v\n%s", rec, debug.Stack())
                http.Error(w, "internal server error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}
Zkusit odpovědět na tuto otázku s trenérem AI

Souběžnost

17Co jsou v Go nil kanály (channels) a jak mohou nechtěně rozbít kód nebo záměrně deaktivovat větve příkazu select?

`nil` kanál je proměnná typu kanál, jejíž hodnota je `nil`, často proto, že nebyla inicializována pomocí funkce `make` nebo byla na `nil` explicitně nastavena. Odesílání do `nil` kanálu nebo přijímání z něj blokuje navždy. V příkazu `select` není větev (`case`) obsahující `nil` kanál nikdy připravena, takže přiřazení kanálu na hodnotu `nil` může danou větev záměrně deaktivovat. Neúmyslné použití `nil` kanálu může způsobit zamrznutí goroutine nebo to, že logika příkazu `select` přestane zpracovávat očekávané události.

var in <-chan int = source
for in != nil {
    select {
    case v, ok := <-in:
        if !ok {
            in = nil // disables this receive case
            continue
        }
        fmt.Println(v)
    case <-ctx.Done():
        return
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

18Jak se atomické operace v balíčku `sync/atomic` liší od synchronizace založené na mutexech a kdy je vhodné je použít?

Balíček `sync/atomic` poskytuje nedělitelné operace nad jednotlivými paměťovými místy, jako jsou load, store, add, swap a compare-and-swap, se zárukami synchronizace a uspořádání paměti (memory-ordering). Mutex chrání kritickou sekci, takže může střežit libovolný kód a invarianty zahrnující vícero čtení, zápisů nebo položek. Atomické operace jsou vhodné pro jednoduchý nezávislý stav, jako jsou čítače, příznaky, sekvenční čísla nebo pečlivě navržené neblokující (lock-free) datové struktury. Dejte přednost mutexu, pokud jsou operace složené, více hodnot musí zůstat konzistentních, nebo by bylo obtížné pochopit a dokázat správnost atomické verze.

package main

import (
    "sync"
    "sync/atomic"
)

var requests atomic.Int64

func recordRequest() {
    requests.Add(1) // one independent counter update
}

type Account struct {
    mu      sync.Mutex
    balance int
    limit   int
}

func (a *Account) Withdraw(n int) bool {
    a.mu.Lock()
    defer a.mu.Unlock()

    // The check and update must be one protected invariant.
    if a.balance-n < -a.limit {
        return false
    }
    a.balance -= n
    return true
}
Zkusit odpovědět na tuto otázku s trenérem AI

19Jak by mělo být navrženo vlastnictví kanálů a životnost gorutin, aby se předešlo únikům gorutin (goroutine leaks)?

Navrhněte gorutiny s explicitním vlastníkem, jasným signálem k ukončení a zaručenou cestou k opuštění. Strana producenta obecně vlastní zavírání kanálu, zejména výstupního kanálu; přijímající strana by neměla zavírat kanál, pokud mohou být odesílatelé stále aktivní. U každého blokujícího odesílání, přijímání, cyklu, časovače nebo externího volání by mělo být buď zaručeno jeho dokončení, nebo by mělo umožňovat odblokování při zrušení, a to typicky pomocí `context.Context` nebo kanálu `done`. Použijte `WaitGroup`, `errgroup` nebo podobnou koordinaci, aby se počkalo na dokončení workerů a kanály se zavřely až po ukončení odesílatelů.

package worker

import (
    "context"
    "sync"
)

type Job int
type Result int

func StartWorkers(ctx context.Context, jobs <-chan Job, n int) <-chan Result {
    results := make(chan Result)

    var wg sync.WaitGroup
    wg.Add(n)
    for i := 0; i < n; i++ {
        go func() {
            defer wg.Done()
            for {
                select {
                case <-ctx.Done():
                    return
                case j, ok := <-jobs:
                    if !ok {
                        return
                    }
                    r := Result(j * 2)
                    select {
                    case results <- r:
                    case <-ctx.Done():
                        return
                    }
                }
            }
        }()
    }

    go func() {
        wg.Wait()
        close(results) // close after all senders are done
    }()

    return results
}
Zkusit odpovědět na tuto otázku s trenérem AI

20Jaké jsou běžné příčiny úniků `goroutine` (goroutine leaks) ve službách v Go a jak je v produkci detekujete a opravíte?

Běžné úniky `goroutine` ve službách v Go pocházejí z `goroutine`, které jsou trvale zablokované při odesílání nebo přijímání přes kanál (channel), čekají na jiné blokující operace bez možnosti zrušení (cancellation), ze zaseknutých I/O operací bez časových limitů (deadlines), z cyklů na pozadí nebo časovačů (tickers), které se nikdy nezastaví, a z `goroutine` vázaných na požadavek, které tento požadavek přežijí. V produkci hledáte trvalý růst počtu `goroutine` a související příznaky, následně prozkoumáte výpisy `goroutine` nebo profily `goroutine` z nástroje `pprof`, abyste zjistili, kde jsou `goroutine` zaseknuté. Oprava úniku znamená upravit kód tak, aby tyto `goroutine` mohly skončit: přidat zrušení a časové limity, zastavit časovače (tickers), správně zavřít kanály, vyhnout se odtrženým `goroutine` požadavků a omezit souběžnost (concurrency) tam, kde je to nutné.

func handler(w http.ResponseWriter, r *http.Request) {
    go func() {
        result := <-slowCh // may block forever
        _ = result
    }()
}
Zkusit odpovědět na tuto otázku s trenérem AI