Priprema za Go intervju

Pitanja za Go Backend Developer intervju

Izabrana Go interview pitanja za backend developere, grupisana po temama i prikazana iz istog kataloga koji pokreće EngineerSpeak vežbu.

Započni Go Backend AI intervjuKreditna kartica nije potrebna. Dostupna je 1 besplatna sesija.
Vežba tehničkog intervjua na engleskomRežim u kome kandidati kojima engleski nije maternji mogu da vežbaju tehničke intervjue.

Sistem tipova

1Objasnite kako funkcionišu nulte vrednosti u jeziku Go za ugrađene tipove i tipove nalik referencama, i zašto su one važne prilikom deklarisanja promenljivih bez eksplicitne inicijalizacije.

U jeziku Go, promenljiva deklarisana bez eksplicitnog inicijalizatora automatski se inicijalizuje na nultu vrednost svog tipa. Numerički tipovi postaju 0, bool postaje false, string postaje "", a nizovi ili strukture se postavljaju na nulu element po element ili polje po polje. Tipovi nalik pokazivačima ili referencama, kao što su pokazivači, isečci (slices), mape, kanali, funkcije i interfejsi, imaju nil kao svoju nultu vrednost. Ovo je važno zato što promenljive i izostavljena polja struktura u Go-u počinju u determinističkom stanju umesto da sadrže nasumične memorijske ostatke (garbage), a mnogi API-ji su projektovani tako da nulta vrednost predstavlja korisnu podrazumevanu vrednost, iako neke nil vrednosti i dalje zahtevaju inicijalizaciju pre određenih operacija.

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)
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

2Kako Go obrađuje jednakost za strukture i šta se dešava kada struktura sadrži neuporediva polja?

Vrednosti struktura u jeziku Go mogu se porediti pomoću `==` i `!=` samo kada je svako polje u strukturi uporedivo. Jednakost poredi odgovarajuća polja koristeći sopstveno pravilo jednakosti za svako polje. Ako struktura sadrži neuporedivo polje kao što su isečak (slice), mapa ili funkcija, tip strukture nije uporediv, a poređenje dve vrednosti tog tipa strukture pomoću `==` predstavlja grešku pri prevođenju (compile-time error). Za takve strukture koristite prilagođenu logiku poređenja ili odgovarajuću pomoćnu funkciju za duboko poređenje jednakosti, posebno u testovima.

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
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

3Objasnite nepromenljivost stringova u jeziku Go i odnos između tipova string, []byte, bytes.Buffer i strings.Builder.

U jeziku Go, `string` je nepromenljiv niz bajtova, najčešće UTF-8 tekst, ali nije obavezno da bude validan UTF-8. String ne možete menjati u mestu; da biste promenili sadržaj, obično vršite konverziju u `[]byte` za izmene na nivou bajtova ili u `[]rune` za izmene na nivou kodnih tačaka, a zatim konvertujete nazad. Standardne konverzije između `string` i `[]byte` kopiraju podatke i mogu alocirati memoriju, pa uzastopne konverzije ili uzastopno spajanje unutar petlji mogu biti računski skupe operacije. `strings.Builder` je optimizovan za efikasnu izgradnju stringova, dok je `bytes.Buffer` promenljivi bafer bajtova koristan za rad sa bajtovskim podacima i I/O operacije, a može generisati i string.

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))
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

4Objasnite kako se nil različito ponaša kod pokazivača, isečaka (slices), mapa, kanala, funkcija i interfejsa u jeziku Go.

U jeziku Go, nil je nulta vrednost za pokazivače, isečke (slices), mape, kanale, funkcije i interfejse, ali se operacije nad tim nil vrednostima razlikuju u zavisnosti od tipa. Nil pokazivač se može porediti sa nil, ali njegovo dereferenciranje izaziva paniku (panic). Nil isečak ima dužinu i kapacitet 0, može se iterirati pomoću range petlje i u njega se mogu dodavati elementi pomoću funkcije append. Iz nil mape se može čitati i može se iterirati kroz nju, ali dodeljivanje vrednosti izaziva paniku. Slanje podataka u nil kanal ili primanje iz njega blokira izvršavanje zauvek, a zatvaranje nil kanala izaziva paniku. Pozivanje nil funkcije izaziva paniku. Interfejs je nil samo kada nema ni dinamički tip ni dinamičku vrednost; interfejs koji sadrži tipiziranu nil vrednost, poput nil pokazivača, sam po sebi nije nil.

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)
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

5Šta su uporedivi tipovi u jeziku Go i kako pravila uporedivosti utiču na ključeve mapa, jednakost i ograničenja u genericima?

Uporedivi tipovi u jeziku Go su tipovi čije se vrednosti mogu porediti pomoću `==` i `!=`. Osnovni tipovi, pokazivači, kanali, interfejsi i strukture/nizovi čija su polja ili elementi uporedivi jesu uporedivi; isečci (slices), mape i funkcije nisu uporedivi osim sa `nil`. Ključevi mapa moraju biti uporedivi. Jednakost prati pravila poređenja datog tipa, a poređenje interfejsa zavisi od dinamičkih konkretnih vrednosti; ako interfejs koji se poredi sadrži neuporedivu dinamičku vrednost, poređenje izaziva paniku (panic). U genericima, unapred deklarisano ograničenje `comparable` omogućava poređenje parametara tipa pomoću `==`/`!=` i njihovo korišćenje kao ključeva mape.

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
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

6Kako Go predstavlja bajtove, rune i tekst kodiran u UTF-8 formatu, i zašto se len(s) može razlikovati od broja karaktera vidljivih korisniku?

U jeziku Go, `byte` je alijas za `uint8` i predstavlja jedan sirovi bajt, dok je `rune` alijas za `int32` i predstavlja Unicode kodnu tačku. `string` je niz bajtova samo za čitanje, uglavnom UTF-8 kodiran tekst, ali može sadržati i proizvoljne bajtove. Funkcija `len(s)` vraća broj bajtova, a ne broj runa ili karaktera vidljivih korisniku. Indeksiranje stringa vraća bajt; iteracija kroz string pomoću `range` dekodira UTF-8 i daje indekse bajtova zajedno sa runama. `len(s)` se može razlikovati od broja vidljivih karaktera jer UTF-8 može koristiti više bajtova po jednoj kodnoj tački i zato što jedan korisnički vidljiv karakter može biti sastavljen od više kodnih tačaka, kao što su kombinujući znaci ili sekvence emodžija.

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)
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Strukture podataka

7Opišite razliku između nizova (arrays) i isečaka (slices) u programskom jeziku Go, uključujući ponašanje dužine, kapaciteta i osnovnog skladišta.

Niz u jeziku Go ima fiksnu dužinu koja je deo njegovog tipa, kao što je `[3]int`; on direktno skladišti svoje elemente, pa dodela ili prosleđivanje niza kopira celu vrednost niza. Isečak, kao što je `[]int`, jeste mali deskriptor preko osnovnog niza: konceptualno sadrži pokazivač na elemente, dužinu i kapacitet. Dužina isečka predstavlja broj vidljivih elemenata, dok je njegov kapacitet broj elemenata koji se mogu iskoristiti od početka isečka do kraja osnovnog niza. Isečci su fleksibilni: ponovno sečenje menja deskriptor, a `append` može ponovo iskoristiti isti osnovni niz ako kapacitet to dozvoljava ili alocirati novi ako to nije slučaj.

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))
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

8Kako se ponaša tip `map` u jeziku Go u pogledu tipova ključeva, nepostojećih ključeva, `nil` mapa i redosleda iteracije?

Tipovi ključeva u Go mapi moraju biti uporedivi; isečci (`slices`), mape i funkcije se ne mogu direktno koristiti kao ključevi. Pretraga ključa koji ne postoji vraća podrazumevanu nultu vrednost tog tipa elementa, pa se forma „zarez-ok“ (`v, ok := m[k]`) koristi za razlikovanje odsustva ključa od prisutne nulte vrednosti. Iz `nil` mape se može čitati i može se iterirati kroz nju pomoću `range`, ali dodela vrednosti izaziva paniku (`panic`); inicijalizujte je pre pisanja. Redosled iteracije kroz mapu nije definisan i kod ne sme zavisiti od njega.

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
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

9Opišite kako ponovno isecanje (reslicing) i dodela isečka (slice) mogu dovesti do toga da više isečaka deli isti osnovni niz, kao i kakve greške to može izazvati.

Vrednost isečka (`slice`) predstavlja zaglavlje koje pokazuje na osnovni niz. Dodeljivanje isečka ili njegovo prosleđivanje funkciji kopira samo to zaglavlje, a ne i same elemente. Ponovno isecanje kreira novo zaglavlje koje pokazuje na opseg istog osnovnog niza. Zbog toga više isečaka može deliti istu memorijsku lokaciju: izmena elementa kroz jedan isečak može biti vidljiva kroz drugi, a dodavanje elemenata (`append`) u jedan isečak može prepisati podatke vidljive drugom ako i dalje ima preostalog kapaciteta. Greške koje mogu nastati obuhvataju neočekivane izmene podataka, oštećene rezultate, zadržavanje velikih osnovnih nizova u memoriji putem malih podisečaka, kao i trke podataka (`data races`) kada se deljeni isečci koriste konkurentno. Da bi se izbeglo neželjeno deljenje, napravite defanzivno kopiranje pomoću funkcije `copy` ili `append([]T(nil), s...)`, ili ograničite kapacitet izrazom za puni isečak (`full-slice expression`) pre poziva `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)
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

10Objasnite rast slajsa (slice) tokom operacije append na konceptualnom nivou i uticaj uzastopne realokacije na performanse.

Kada append dodaje elemente u slajs, upisuje ih u postojeći osnovni niz ako slajs ima dovoljno kapaciteta. Ako je kapacitet nedovoljan, Go alocira veći osnovni niz, kopira postojeće elemente, upisuje nove elemente i vraća zaglavlje slajsa koje pokazuje na novu memoriju. Tačna strategija rasta zavisi od implementacije, ali konceptualno kapacitet raste dovoljno da ponovljeni pozivi append budu amortizovano efikasni. Uzastopne realokacije i dalje troše CPU (Central Processing Unit) za kopiranje, prave nove alokacije, povećavaju pritisak na GC (Garbage Collector) i mogu prekinuti deljenje memorije sa starim alijasima slajsa. Ako unapred znate očekivanu veličinu, uradite prealokaciju pomoću make([]T, 0, n) kada gradite slajs pozivima append ili make([]T, n) kada ga popunjavate po indeksu kako biste smanjili broj realokacija.

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
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Semantika jezika

11Kako prazan identifikator funkcioniše u jeziku Go za neiskorišćene vrednosti, uvoze paketa i provere implementacije interfejsa tokom kompilacije?

Prazan identifikator `_` je rezervisano mesto koje služi isključivo za upis. Dodeljivanje vrednosti praznom identifikatoru odbacuje tu vrednost i ne stvara promenljivu koja se može koristiti. Koristi se za ignorisanje nepotrebnih povratnih vrednosti ili promenljivih u petlji, za uvoz paketa isključivo radi sporednih efekata pomoću `import _ "pkg"`, kao i za proveru implementacije interfejsa tokom kompilacije, kao što je `var _ io.Reader = (*MyReader)(nil)`. Uvoz paketa pomoću praznog identifikatora i dalje izvršava inicijalizaciju uvezenog paketa. Dodeljivanje radi provere interfejsa ne uspeva da se kompajlira ukoliko skup metoda konkretnog tipa ne zadovoljava traženi interfejs.

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)
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Paketi

12Kako funkcioniše redosled inicijalizacije paketa u programskom jeziku Go, uključujući `init` funkcije i uvezene zavisnosti?

Go inicijalizuje pakete po redosledu zavisnosti. Uvezene zavisnosti paketa se inicijalizuju pre samog paketa koji ih uvozi. Unutar paketa, promenljive na nivou paketa se inicijalizuju pre bilo koje `init` funkcije, pri čemu je redosled inicijalizacije promenljivih određen njihovom međusobnom zavisnošću i redosledom deklarisanja kako je definisano jezikom. Zatim se automatski izvršavaju `init` funkcije paketa; paket može imati više `init` funkcija i one se ne mogu pozivati direktno. Svaki paket se inicijalizuje tačno jednom. Za izvršne programe, prvo se inicijalizuje graf uvoza, zatim se inicijalizuje paket `main`, i na kraju se poziva `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") }
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

13Objasnite pravila vidljivosti paketa u jeziku Go, uključujući izvezene identifikatore i konvenciju o internal/ direktorijumu.

U jeziku Go, vidljivost paketa kontroliše se imenovanjem identifikatora, a ne ključnim rečima za pristup. Identifikator čije ime počinje velikim Unicode slovom je izvezen i može se referencirati iz drugih paketa; ostali identifikatori su neizvezeni i mogu se koristiti samo unutar istog paketa. Ovo važi za funkcije, tipove, metode, promenljive, konstante i polja struktura. Paketi koriste izvezene identifikatore da definišu svoj javni API (Application Programming Interface) i zadrže detalje implementacije neizvezenim. Zasebno, paket koji se nalazi unutar `internal/` direktorijuma može da uveze samo kôd čija se putanja uvoza nalazi unutar roditeljskog stabla tog `internal` direktorijuma; ovo pravilo nameće sam Go skup alata.

// 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.
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Tok kontrole

14Kako funkcioniše defer u programskom jeziku Go, uključujući redosled izvršavanja, vreme evaluacije argumenata i interakciju sa povratnim vrednostima?

`defer` raspoređuje poziv funkcije tako da se izvrši kada se funkcija u kojoj se nalazi završava, bilo usled normalnog povratka ili usled odmotavanja izazvanog panikom. Više odloženih poziva izvršava se po redosledu gde se poslednji dodati poziv prvi izvršava. Vrednost odložene funkcije i njeni argumenti evaluiraju se odmah pri izvršavanju naredbe `defer`, ali se sam poziv izvršava kasnije. Kod imenovanih povratnih vrednosti, naredba return prvo dodeljuje povratne vrednosti, a zatim se izvršavaju odložene funkcije, pa odloženo zatvaranje može pročitati ili izmeniti imenovane promenljive rezultata pre nego što ih pozivalac primi. Zbog toga je `defer` koristan za poslove čišćenja kao što su zatvaranje fajlova, otključavanje muteksa i oslobađanje resursa.

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())
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Obrada grešaka

15Objasnite model obrade grešaka u jeziku Go i uobičajene načine na koje se greške kreiraju, vraćaju i proveravaju.

Go tretira greške kao obične vrednosti, a ne kao izuzetke. Ugrađeni interfejs `error` zadovoljava svaki tip koji ima metodu `Error() string`. Funkcije po konvenciji vraćaju `error` kao poslednju povratnu vrednost, gde `nil` označava uspeh, a vrednost različita od `nil` znači da pozivalac mora da obradi ili prosledi grešku. Jednostavne greške se obično kreiraju pomoću `errors.New`, formatirane greške pomoću `fmt.Errorf`, a pozivaoci ih obično proveravaju preko `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
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

16Kako bi trebalo rukovati oporavkom od panike u Go backend servisima, uključujući šta se dešava kada se u gorutini javi panika i kada proces treba da se oporavi, a kada da se sruši?

Panika odmotava trenutnu gorutinu, izvršavajući njene odložene funkcije. Funkcija `recover` radi samo kada se pozove iz odložene funkcije u toj istoj gorutini; jedna gorutina ne može da oporavi paniku druge gorutine. Ako se panika ne obradi, proces se ruši. U backend servisima, oporavak obično treba postaviti na granicama izolacije, kao što su funkcije za obradu zahteva, RPC (Remote Procedure Call) posrednički slojevi ili ulazne tačke radnih gorutina, kako jedan neuspeli zahtev ili posao ne bi oborio ceo servis. Međutim, ako postoji mogućnost da je panika narušila deljeno stanje ili dovela u pitanje integritet procesa, bezbednije je pustiti proces da se sruši i ponovo pokrene, umesto oporavka i slepog nastavka rada.

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)
    })
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Konkurentnost

17Šta su nil kanali u jeziku Go i kako mogu slučajno pokvariti kôd ili namerno onemogućiti slučajeve unutar select bloka?

nil kanal je promenljiva kanala čija je vrednost nil, najčešće zato što nije inicijalizovana pomoću make ili je eksplicitno postavljena na nil. Slanje podataka na nil kanal ili čitanje iz njega zauvek blokira izvršavanje. Unutar select bloka, case koji koristi nil kanal nikada nije spreman, pa dodeljivanje vrednosti nil promenljivoj kanala može namerno onemogućiti taj case. Slučajno korišćenje nil kanala može dovesti do blokiranja gorutina ili prouzrokovati da select logika prestane da obrađuje očekivane događaje.

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
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

18Kako se atomske operacije u paketu sync/atomic razlikuju od sinhronizacije zasnovane na muteksima i kada su one prikladne?

sync/atomic pruža nedeljive operacije nad pojedinačnim memorijskim lokacijama, kao što su load, store, add, swap i compare-and-swap, sa garancijama sinhronizacije i redosleda pristupa memoriji (memory-ordering). Muteks štiti kritičnu sekciju, pa može čuvati proizvoljan kôd i invarijante koje uključuju višestruka čitanja, upise ili polja. Atomske operacije su prikladne za jednostavno, nezavisno stanje kao što su brojači, zastavice, redni brojevi ili pažljivo dizajnirane strukture bez zaključavanja (lock-free). Dajte prednost muteksu kada su operacije složene, kada više vrednosti mora ostati konzistentno ili kada bi o atomskoj verziji bilo teško rasuđivati ili dokazati njenu ispravnost.

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
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

19Kako treba projektovati vlasništvo nad kanalima i životni vek gorutina da bi se izbeglo curenje gorutina?

Projektujte gorutine sa eksplicitnim vlasnikom, jasnim signalom za gašenje i garantovanom putanjom za izlazak. Strana proizvođača uglavnom poseduje zatvaranje kanala, posebno izlaznog kanala; primaoci ne bi trebalo da zatvaraju kanal dok su pošiljaoci možda još uvek aktivni. Svako blokirajuće slanje, primanje, petlja, tajmer ili eksterni poziv treba ili da ima garanciju završetka ili mogućnost deblokiranja pri otkazivanju, obično putem `context.Context` ili namenskog done kanala. Koristite `sync.WaitGroup`, `errgroup` ili slične mehanizme koordinacije tako da se radnici sačekaju i kanali zatvore tek nakon što pošiljaoci završe rad.

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
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

20Koji su uobičajeni uzroci curenja gorutina (goroutine leaks) u Go servisima i kako ih otkrivate i rešavate u produkciji?

Uobičajeni uzroci curenja gorutina u Go servisima potiču od gorutina koje su trajno blokirane prilikom slanja ili primanja sa kanala, čekanja na druge blokirajuće operacije bez mehanizma otkazivanja, blokiranih I/O (Input/Output) operacija bez vremenskih rokova, pozadinskih petlji ili tajmera koji se nikada ne zaustavljaju, kao i gorutina vezanih za obradu zahteva koje nadžive sam zahtev. U produkciji pratite kontinuirani rast broja gorutina i povezane simptome, a zatim pregledate ispise stanja gorutina ili `pprof` profile gorutina kako biste videli gde su one blokirane. Otklanjanje curenja zahteva izmenu koda kako bi te gorutine mogle da se završe: dodajte otkazivanje i vremenske rokove, zaustavljajte tajmere, pravilno zatvarajte kanale, izbegavajte nekontrolisane gorutine vezane za zahteve i ograničite nivo konkurentnosti gde je to potrebno.

func handler(w http.ResponseWriter, r *http.Request) {
    go func() {
        result := <-slowCh // may block forever
        _ = result
    }()
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom