Priprema za Junior Go

Junior Go Backend interview pitanja

15 izabranih Junior Go interview pitanja za backend developere koji treba jasno i sigurno da objasne osnove.

Započni Junior Go 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

4Kako funkcionišu pokazivači u jeziku Go i koje operacije su namerno onemogućene u poređenju sa jezikom C?

Pokazivač u jeziku Go je tipizirana vrednost koja sadrži adresu druge vrednosti, kao što je *int koji pokazuje na int. Koristi se &x za uzimanje adrese vrednosti i *p za dereferenciranje pokazivača radi čitanja ili upisivanja vrednosti na koju pokazuje. Prosleđivanje ili čuvanje pokazivača omogućava da više mesta posmatra ili menja istu osnovnu vrednost, a pokazivač može biti nil. Za razliku od jezika C, bezbedni Go namerno ne dozvoljava pokazivačku aritmetiku i proizvoljno rukovanje sirovim adresama; izuzeci na niskom nivou zahtevaju paket unsafe.

package main

import "fmt"

func inc(p *int) {
    *p = *p + 1
}

func main() {
    x := 10
    p := &x
    inc(p)
    fmt.Println(x)
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

5Objasnite kako funkcionišu konstante i iota u jeziku Go, uključujući tipizirane u odnosu na netipizirane konstante.

Konstante u jeziku Go su vrednosti poznate u vreme kompajliranja: logičke, tekstualne ili numeričke. Tipizirana konstanta ima specifičan tip. Netipizirana konstanta nema fiksiran konkretan tip sve dok se ne upotrebi u tipiziranom kontekstu, pri čemu se numeričke netipizirane konstante do tog trenutka predstavljaju sa visokom preciznošću; vrednost mora biti predstavljiva u izabranom tipu. `iota` je unapred deklarisani identifikator koji se koristi u `const` deklaracijama: počinje od 0 u svakom bloku konstanti i uvećava se za svaku specifikaciju konstante, što ga čini korisnim za definisanje konstanti nalik enumeracijama i bitskih zastavica.

type Status int

const (
    StatusPending Status = iota // 0, typed Status
    StatusActive                // 1, same expression/type repeated
    StatusClosed                // 2
)

const big = 1 << 100 // untyped integer constant with high precision

var x int64 = 42      // untyped 42 can become int64
// var y int64 = big  // compile error: not representable as int64
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

6Kako funkcionišu eksplicitne konverzije u jeziku Go i zašto jezik izbegava mnoge implicitne konverzije?

Go generalno zahteva eksplicitne konverzije pomoću `T(x)` prilikom promene tipa vrednosti, kao što su `int64(i)` ili `MyID(n)`. Time promene tipa ostaju vidljive i izbegavaju se neočekivane implicitne konverzije brojeva, logičkih vrednosti ili niski. Konverzije su dozvoljene samo prema Go pravilima konverzije i mogu promeniti vrednost, na primer usled prekoračenja ili odsecanja celih brojeva, odsecanja razlomljenog dela ka nuli pri prelazu sa brojeva sa pokretnim zarezom na cele brojeve, ili gubitka preciznosti. Netipizirane konstante su fleksibilnije: mogu se koristiti u tipiziranom kontekstu ako se vrednost konstante može predstaviti tim tipom.

var x int64 = 300
var y int8 = int8(x)   // explicit conversion; value changes

var f float64 = 3.9
var i int = int(f)     // truncates toward zero

const c = 42
var n int32 = c        // ok: untyped constant is representable as int32
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

Upravljanje memorijom

9Kada u programskom jeziku Go strukturu ili drugu vrednost treba preneti po vrednosti, a kada po pokazivaču?

Prenosite po vrednosti kada je vrednost mala, namenjena samo za čitanje tokom poziva ili kada želite semantiku nezavisnog kopiranja. Prenosite po pokazivaču kada funkcija mora da izmeni vrednost pozivaoca, kada bi kopiranje bilo računski skupo, kada tip ne bi trebalo da se kopira ili kada su `nil` odnosno deljeni identitet deo API-ja (Application Programming Interface). Nemojte pretpostavljati da su pokazivači uvek brži: oni mogu uneti alijasing, bekstva na hip (heap escapes), dodatni rad za GC (Garbage Collector) i pristup memoriji koji je manje pogodan za keš memoriju. Dajte prednost jasnoj semantici i proveravajte performanse merenjem (benchmark) za delove koda kritične za performanse.

package main

import "fmt"

type Counter struct{ N int }

func Read(c Counter) int {
    c.N++
    return c.N
}

func Inc(c *Counter) {
    c.N++
}

func main() {
    c := Counter{N: 5}
    fmt.Println(Read(c), c.N)
    Inc(&c)
    fmt.Println(c.N)
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Semantika jezika

10Kako se ponašaju dodela i kopiranje za strukture, nizove, mape i isečke (slices) u jeziku Go?

Dodela vrednosti u jeziku Go kopira vrednost koja se dodeljuje, ali efekat zavisi od samog tipa. Dodela strukture kopira polja strukture, a dodela niza kopira svaki element. Dodela isečka kopira zaglavlje isečka — pokazivač, dužinu i kapacitet — pa isečci uglavnom dele isti osnovni niz (backing array). Dodela mape kopira deskriptor mape koji funkcioniše poput reference, pa obe promenljive referenciraju iste podatke u mapi. Za nezavisno vlasništvo potrebno je napraviti defanzivnu kopiju, na primer korišćenjem funkcije `copy` ili `slices.Clone` za isečke, odnosno kreiranjem nove mape i kopiranjem stavki za mape.

package main

import "fmt"

type Point struct{ X int }

func main() {
    a := Point{X: 1}
    b := a
    b.X = 2

    x := [2]int{1, 2}
    y := x
    y[0] = 99

    fmt.Println(a.X, b.X)
    fmt.Println(x, y)
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

11Kako zatvorenja (closures) hvataju promenljive u jeziku Go i koje greške mogu nastati u radu sa petljama i gorutinama?

Zatvorenje u jeziku Go je funkcijska vrednost koja referencira promenljive iz svog okružujućeg leksičkog opsega. Ono hvata same promenljive, pa zatvorenje može uočiti promene njihovih vrednosti, a uhvaćene promenljive mogu živeti duže od funkcije koja ih je kreirala ukoliko zatvorenje pobegne na hip. Greške u radu sa petljama i gorutinama nastaju kada zatvorenja dele ili očitavaju promenljivu nakon što se ona promenila, što često dovodi do toga da gorutine ili odloženi povratni pozivi vide neočekivanu vrednost ili izazovu trku za podacima (data race) nad deljenim stanjem. Uobičajeno rešenje je prosleđivanje željene vrednosti kao argumenta zatvorenju ili pravljenje nove lokalne kopije po iteraciji, kao i sinhronizacija konkurentnog pristupa kada je to potrebno. Od verzije Go 1.22, promenljive petlje deklarisane unutar `for`/`range` vezuju se za svaku iteraciju ponaosob, što eliminiše mnoge klasične greške pri hvatanju promenljivih petlje, ali ponovno korišćenje promenljivih van petlje i drugo deljeno promenljivo stanje i dalje mogu izazvati probleme.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(v int) {
            defer wg.Done()
            fmt.Println(v)
        }(i)
    }
    wg.Wait()
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

12Opišite semantiku ključne reči range nad nizovima, isečcima (slices), mapama, stringovima i kanalima.

`range` iterira u zavisnosti od tipa operanda. Nad nizom ili isečkom (`slice`) vraća indeks i vrednost elementa; vrednost elementa je kopija, pa dodeljivanje nove vrednosti toj promenljivoj ne menja samu kolekciju. Iteracija kroz vrednost niza kopira ceo niz radi iteracije, dok iteracija kroz isečak koristi zaglavlje isečka i indeksira osnovni niz. Nad mapom vraća ključ i vrednost u nedefinisanom redosledu, pri čemu je vrednost kopija. Nad stringom vraća bajtni indeks i dekodovanu Unikod kodnu tačku (`rune`), a ne indeks po poziciji rune. Nad kanalom prima vrednosti sve dok kanal ne bude zatvoren i ispražnjen; `range` nad `nil` kanalom blokira zauvek.

package main

import "fmt"

func main() {
    xs := []int{1, 2, 3}
    for _, v := range xs {
        v *= 10 // modifies only the copy
    }
    fmt.Println(xs)

    for i := range xs {
        xs[i] *= 10 // modifies the slice element
    }
    fmt.Println(xs)
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Metode

13Koja je razlika između primaoca po vrednosti i primaoca po pokazivaču, i kako izbor primaoca utiče na promenljivost, kopiranje i zadovoljavanje interfejsa?

Metod sa primaocem po vrednosti dobija kopiju primaoca, pa je pogodan za ponašanje koje samo čita podatke i za male nepromenljive tipove; izmene na kopiji primaoca nisu vidljive pozivaocu. Metod sa primaocem po pokazivaču dobija kopiju pokazivača na primalac, pa može da menja original i izbegava kopiranje velikih vrednosti ili vrednosti koje ne bi trebalo kopirati. Izbor primaoca utiče na skupove metoda: metodi sa primaocima po vrednosti nalaze se u skupu metoda i za T i za *T, dok se metodi sa primaocima po pokazivaču nalaze samo u skupu metoda za *T. Zbog toga interfejs koji zahteva metod sa primaocem po pokazivaču zadovoljava *T, a ne T, iako adresibilna vrednost tipa T može pozvati taj metod koristeći uobičajenu sintaksu poziva metoda.

package main

import "fmt"

type Counter struct{ N int }

func (c Counter) AddValue() { c.N++ }
func (c *Counter) AddPointer() { c.N++ }

func main() {
    c := Counter{N: 1}
    c.AddValue()
    fmt.Println(c.N)
    c.AddPointer() // compiler rewrites to (&c).AddPointer() because c is addressable
    fmt.Println(c.N)
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Interfaces

14Opišite kako interfejsi funkcionišu u jeziku Go i šta znači da tip implicitno zadovoljava interfejs.

Go interfejs definiše skup potrebnih metoda. Konkretan tip implicitno zadovoljava interfejs kada njegov skup metoda (method set) sadrži te metode; ne postoji ključna reč ili deklaracija `implements`. Promenljiva tipa interfejsa može sadržati konkretnu dinamičku vrednost čiji tip zadovoljava interfejs, a pozivi metoda interfejsa se prosleđuju implementaciji te dinamičke vrednosti. Prazan interfejs, koji se piše kao `interface{}` ili obično `any`, nema zahtevane metode, pa ga svaki tip zadovoljava. Da li tip `T`, `*T` ili oba zadovoljavaju interfejs zavisi od njihovih skupova metoda i izbora prijemnika (receiver).

package main

import "fmt"

type Greeter interface {
    Greet() string
}

type User struct{ Name string }

func (u User) Greet() string { return "hello, " + u.Name }

func Say(g Greeter) {
    fmt.Println(g.Greet())
}

func main() {
    Say(User{Name: "Ana"}) // no 'implements Greeter' declaration needed
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

15Kako funkcionišu tvrdnje o tipu (type assertions) i ispitivanje tipa (type switches) sa vrednostima interfejsa?

Tvrdnja o tipu (type assertion) proverava dinamičku vrednost sačuvanu u interfejsu: `v := x.(T)` uspeva ako je dinamički tip vrednosti interfejsa `T`, ili ako je `T` interfejs koji dinamička vrednost implementira. Oblik sa jednim povratnim rezultatom izaziva paniku (`panic`) ako ne uspe. Oblik sa zarezom i statusom uspeha (comma-ok), `v, ok := x.(T)`, vraća status bez izazivanja panike. Konstrukcija za ispitivanje tipa (type switch), `switch v := x.(type)`, grana se na osnovu dinamičkog tipa vrednosti interfejsa, pri čemu je `v` tipiziran u skladu sa poklopljenim slučajem (`case`).

func describe(x any) string {
    if s, ok := x.(string); ok {
        return "string: " + s
    }

    switch v := x.(type) {
    case int:
        return fmt.Sprintf("int: %d", v)
    case fmt.Stringer:
        return "stringer: " + v.String()
    case nil:
        return "nil interface"
    default:
        return fmt.Sprintf("unknown %T", v)
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom