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