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