Valikoituja Go-haastattelukysymyksiä backend-kehittäjille aihepiireittäin ryhmiteltynä. Kysymykset on poimittu samasta tietokannasta, joka toimii EngineerSpeak-harjoittelun ytimenä.
1Selitä, miten Go-kielen nolla-arvot toimivat sisäänrakennetuille ja viittauksen kaltaisille tyypeille, ja miksi niillä on merkitystä, kun muuttujia esitellään ilman eksplisiittistä alustusta.
Go-kielessä muuttuja, joka on esitelty ilman eksplisiittistä alustusta, alustetaan automaattisesti tyyppinsä nolla-arvoon. Numeerisista tyypeistä tulee `0`, `bool`-tyypistä `false`, `string`-tyypistä `""`, ja taulukot (arrays) tai tietueet (`struct`) nollataan alkio alkiolta tai kenttä kentältä. Osoittimien tai viittausten kaltaisilla tyypeillä, kuten osoittimilla, `slice`- ja `map`-tyypeillä, kanavilla, funktioilla ja rajapinnoilla, on `nil` nolla-arvonaan. Tällä on merkitystä, koska Go-muuttujat ja pois jätetyt `struct`-kentät aloittavat deterministisessä tilassa roskadatan sijaan. Lisäksi monet API-rajapinnat on suunniteltu siten, että nolla-arvo on hyödyllinen oletusarvo, vaikka jotkin `nil`-arvot vaativat yhä alustuksen ennen tiettyjä toimenpiteitä.
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)
}
2Miten Go käsittelee struct-tyyppien yhtäsuuruutta, ja mitä tapahtuu, kun struct sisältää kenttiä, jotka eivät ole vertailtavia?
Gossa `struct`-arvoja voidaan vertailla operaattoreilla `==` ja `!=` vain silloin, kun rakenteen jokainen kenttä on vertailtava. Yhtäsuuruus vertailee vastaavia kenttiä käyttäen kunkin kentän omaa vertailusääntöä. Jos `struct` sisältää kentän, joka ei ole vertailtava (kuten `slice`, `map` tai funktio), koko `struct`-tyyppi ei ole vertailtava, ja kahden kyseisen tyyppisen arvon vertaileminen `==`-operaattorilla aiheuttaa käännösaikaisen virheen. Tällaisten `struct`-tyyppien kohdalla on käytettävä mukautettua vertailulogiikkaa tai sopivaa syvävertailun (deep equality) apufunktiota, erityisesti testeissä.
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
}
3Selitä merkkijonojen muuttumattomuus (string immutability) Go-kielessä ja suhde string-, []byte-, bytes.Buffer- ja strings.Builder-tyyppien välillä.
Go-kielen `string` on muuttumaton tavujono, joka on usein UTF-8-muotoista tekstiä, mutta sen ei ole pakko olla validia UTF-8:aa. Merkkijonoa ei voi muokata paikallaan; sen sisällön muuttamiseksi se tyypillisesti muunnetaan `[]byte`-tyypiksi tavutason muokkauksia varten tai `[]rune`-tyypiksi koodipisteiden (code point) muokkauksia varten, ja sen jälkeen takaisin merkkijonoksi. Normaalit muunnokset `string`- ja `[]byte`-tyyppien välillä kopioivat dataa ja saattavat varata muistia, joten toistuvat muunnokset tai toistuva merkkijonojen yhdistäminen silmukoissa voivat olla raskaita. `strings.Builder` on optimoitu merkkijonojen tehokkaaseen rakentamiseen, kun taas `bytes.Buffer` on muokattava tavupuskuri, joka on hyödyllinen tavupohjaiselle datalle sekä I/O-operaatioille, ja josta voi myös tuottaa merkkijonon.
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))
}
4Selitä, miten `nil` toimii eri tavoin osoittimien, `slice`- ja `map`-tyyppien, kanavien, funktioiden ja rajapintojen kohdalla Go-kielessä.
Go-kielessä `nil` on nolla-arvo osoittimille, `slice`- ja `map`-tyypeille, kanaville, funktioille ja rajapinnoille, mutta näillä `nil`-arvoilla tehtävät operaatiot vaihtelevat tyypin mukaan. `nil`-osoitinta voidaan verrata `nil`-arvoon, mutta sen purkaminen (dereferencing) aiheuttaa paniikin (`panic`). `nil`-arvoisen `slice`:n pituus ja kapasiteetti on 0, ja sen yli voidaan iteroida `range`-rakenteella ja siihen voidaan lisätä alkioita `append`-funktiolla. `nil`-arvoista `map`:ia voidaan lukea ja sen yli voidaan iteroida, mutta siihen sijoittaminen aiheuttaa paniikin. `nil`-kanavalle lähettäminen tai sieltä vastaanottaminen estää suorituksen ikuisesti, ja `nil`-kanavan sulkeminen aiheuttaa paniikin. `nil`-funktion kutsuminen aiheuttaa paniikin. Rajapinta on `nil` vain silloin, kun sillä ei ole dynaamista tyyppiä eikä dynaamista arvoa; tyypitetyn `nil`-arvon, kuten `nil`-osoittimen, sisältävä rajapinta ei itse ole `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)
}
5Mitä ovat vertailtavat tyypit (comparable types) Gossa, ja miten vertailtavuussäännöt vaikuttavat map-rakenteiden avaimiin, yhtäsuuruuteen ja generics-rajoitteisiin?
Vertailtavat tyypit Gossa ovat tyyppejä, joiden arvoja voidaan vertailla operaattoreilla `==` ja `!=`. Perustyypit, osoittimet, kanavat (channels), rajapinnat (interfaces) sekä sellaiset `struct`- ja taulukkotyypit, joiden kentät tai alkiot ovat vertailtavia, ovat vertailtavia tyyppejä. `slice`-, `map`- ja funktiotyypit eivät ole vertailtavia, paitsi arvoon `nil`. `map`-rakenteen avainten on oltava vertailtavia. Yhtäsuuruus noudattaa tyypin omia vertailusääntöjä, ja rajapintojen vertailu riippuu dynaamisista konkreettisista arvoista; jos vertailtava rajapinta sisältää dynaamisen arvon, joka ei ole vertailtava, vertailu aiheuttaa paniikin (panic). Generics-tyyppijärjestelmässä ennalta määritelty `comparable`-rajoite sallii tyyppiparametrien vertailun `==`/`!=` -operaattoreilla ja niiden käytön `map`-avaimina.
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
}
6Miten Go esittää tavuja, rune-merkkejä ja UTF-8-koodattua tekstiä, ja miksi len(s) voi poiketa käyttäjälle näkyvien merkkien määrästä?
Go-kielessä `byte` on alias `uint8`-tyypille ja se edustaa yhtä raakatavua, kun taas `rune` on alias `int32`-tyypille ja edustaa yhtä Unicode-koodipistettä. `string` on vain luku -tyyppinen tavujono, joka on yleensä UTF-8-koodattua tekstiä, mutta voi sisältää mielivaltaisia tavuja. `len(s)` palauttaa tavujen määrän, ei `rune`-merkkien tai käyttäjälle näkyvien merkkien määrää. Merkkijonon indeksointi palauttaa tavun; `range`-rakenteen käyttö merkkijonon yli purkaa UTF-8-koodauksen ja tuottaa tavuindeksien lisäksi `rune`-merkkejä. `len(s)` voi poiketa näkyvien merkkien määrästä, koska UTF-8 saattaa käyttää useita tavuja koodipistettä kohden, ja koska yksi käyttäjälle näkyvä merkki voi koostua useista koodipisteistä, kuten yhdistelmämerkeistä (combining marks) tai emojisarjoista.
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)
}
}
7Kuvaile eroa Go-kielen taulukoiden ja `slice`-tyyppien välillä, mukaan lukien miten pituus, kapasiteetti ja taustalla oleva tallennustila käyttäytyvät.
Go-kielen taulukoilla on kiinteä pituus, joka on osa sen tyyppiä, kuten `[3]int`. Se tallentaa alkionsa suoraan, ja taulukon sijoittaminen muuttujaan tai välittäminen funktiolle kopioi koko taulukon arvon. `slice`, kuten `[]int`, on pieni kuvaaja taustalla olevasta taulukosta: käsitteellisesti se sisältää osoittimen alkioihin, pituuden ja kapasiteetin. `slice`-tyypin pituus on näkyvillä olevien alkioiden määrä; sen kapasiteetti määrittää, kuinka monta alkiota voidaan käyttää `slice`-alkukohdasta taustataulukon loppuun mennessä. `slice`-tyypit ovat joustavia: arvon viipalointi uudelleen (reslicing) muuttaa kuvaajaa, ja `append` voi käyttää uudelleen samaa taustataulukkoa, jos kapasiteetti sallii, tai varata uuden, jos se ei riitä.
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))
}
8Kuinka Go:n `map`-tyyppi käyttäytyy suhteessa avaintyyppeihin, puuttuviin avaimiin, `nil`-arvoisiin `map`-rakenteisiin ja iteroinnin järjestykseen?
Go:n `map`-rakenteen avaintyyppien on oltava vertailukelpoisia (comparable); `slice`-, `map`- ja funktiotyyppejä ei voi käyttää suoraan avaimina. Puuttuvan avaimen hakeminen palauttaa alkiotyypin nolla-arvon, joten puuttuminen ja olemassa oleva nolla-arvo erotetaan toisistaan `comma-ok`-idiomilla (`v, ok := m[k]`). `nil`-arvoista `map`-rakennetta voi lukea ja sen voi käydä läpi `range`-rakenteella, mutta siihen sijoittaminen aiheuttaa paniikin (`panic`); se on siis alustettava ennen kirjoittamista. `map`-rakenteen iterointijärjestystä ei ole määritelty, eikä koodin tulisi luottaa siihen.
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
}
}
9Kuvaile, kuinka `slice`-tyyppien viipalointi uudelleen ja sijoittaminen voivat johtaa siihen, että useat `slice`-arvot jakavat saman taustataulukon, ja millaisia bugeja tämä voi aiheuttaa.
`slice`-arvo on otsikkorakenne (header), joka osoittaa taustalla olevaan taulukkoon. `slice`-arvon sijoittaminen tai välittäminen funktiolle kopioi vain tuon otsikkorakenteen, ei alkioita. Uudelleen viipalointi luo toisen otsikkorakenteen, joka osoittaa samaan taustataulukkoon tietylle välille. Tämän vuoksi useat `slice`-arvot voivat viitata samaan tallennustilaan: alkion muuttaminen yhden `slice`-arvon kautta voi näkyä toisessa, ja `append`-operaatio yhteen `slice`-arvoon voi ylikirjoittaa toiselle näkyvää dataa, jos siinä on vielä vapaata kapasiteettia. Tästä aiheutuvia bugeja ovat yllättävät muutokset, korruptoituneet tulokset, suurten taustataulukoiden tahaton muistissa pitäminen pienten `slice`-osien kautta sekä kilpailutilanteet (data race), kun aliaksia käytetään samanaikaisesti. Tahattoman jakamisen välttämiseksi tee puolustava kopiointi käyttämällä `copy`-funktiota tai lauseketta `append([]T(nil), s...)`, tai rajoita kapasiteettia koko kapasiteetin määrittävällä `slice`-lausekkeella ennen `append`-operaatiota.
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)
}
10Selitä `slice`-rakenteen kasvaminen `append`-funktion aikana käsitteellisellä tasolla sekä toistuvien muistinvarauksien vaikutukset suorituskykyyn.
Kun `append` lisää elementtejä `slice`-rakenteeseen, se kirjoittaa ne olemassa olevaan taustataulukkoon (backing array), jos `slice`:lla on riittävästi kapasiteettia. Jos kapasiteetti ei riitä, Go varaa suuremman taustataulukon, kopioi olemassa olevat alkiot, kirjoittaa uudet alkiot ja palauttaa `slice`-otsikon, joka osoittaa uuteen muistialueeseen. Tarkka kasvatuskäytäntö riippuu toteutuksesta, mutta käsitteellisesti kapasiteetti kasvaa riittävästi, jotta toistuvat `append`-lisäykset ovat kuoletetusti tehokkaita (amortized efficient). Toistuvat uudelleenvaraukset kuluttavat silti suoritinaikaa kopiointiin, luovat uusia muistinvarauksia, lisäävät GC (Garbage Collector) -kuormitusta ja voivat rikkoa tilojen jaon (sharing) vanhojen `slice`-viittausten kanssa. Jos odotettu koko on tiedossa, varaa muisti ennakkoon komennolla `make([]T, 0, n)` rakentaessasi `append`-funktiolla, tai komennolla `make([]T, n)` täyttäessäsi taulukkoa indekseittäin, jotta vältyt turhilta muistinvarauksilta.
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
}
11Miten tyhjä tunniste (blank identifier) toimii Gossa käyttämättömien arvojen, tuontien ja käännösaikaisten rajapintatarkistusten osalta?
Tyhjä tunniste `_` on vain kirjoitusta tukeva paikkamerkki. Siihen sijoittaminen hylkää arvon eikä luo käyttökelpoista muuttujaa. Sitä käytetään tarpeettomien paluuarvojen tai silmukkamuuttujien ohittamiseen, paketin tuomiseen pelkkien sivuvaikutusten vuoksi muodossa `import _ "pkg"`, sekä käännösaikaisiin rajapintatoteutusten tarkistuksiin, kuten `var _ io.Reader = (*MyReader)(nil)`. Tyhjä tuonti suorittaa silti tuodun paketin alustuksen. Rajapintatarkistuksen sijoitus epäonnistuu kääntämisvaiheessa, jos konkreettisen tyypin metodijoukko ei täytä rajapinnan vaatimuksia.
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)
}
}
12Miten pakettien alustusjärjestys toimii Go:ssa ottaen huomioon `init`-funktiot ja tuodut riippuvuudet?
Go alustaa paketit riippuvuusjärjestyksessä. Paketin tuodut riippuvuudet alustetaan aina ennen ne tuovaa pakettia. Paketin sisällä pakettitason muuttujat alustetaan ennen mahdollisia `init`-funktioita, ja muuttujien alustus tapahtuu kielen määrittelemässä riippuvuus- ja esittelyjärjestyksessä. Tämän jälkeen paketin `init`-funktiot suoritetaan automaattisesti; yhdellä paketilla voi olla useita `init`-funktioita, eikä niitä ole mahdollista kutsua suoraan koodista. Jokainen paketti alustetaan vain kerran. Suoritettavan ohjelman tapauksessa tuontiverkko alustetaan ensin, sitten alustetaan `main`-paketti, ja lopuksi kutsutaan `main.main`-funktiota.
// 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") }
13Selitä Go-kielen pakettien näkyvyyssäännöt, mukaan lukien viedyt tunnisteet (exported identifiers) ja `internal/`-hakemistokäytäntö.
Go-kielessä pakettien näkyvyyttä hallitaan tunnisteiden nimeämisellä, ei näkyvyysavainsanoilla. Tunniste, jonka nimi alkaa isolla Unicode-kirjaimella, on viety (exported) ja siihen voidaan viitata muista paketeista; muut tunnisteet ovat sisäisiä (unexported), ja niitä voidaan käyttää vain saman paketin sisällä. Tämä koskee funktioita, tyyppejä, metodeja, muuttujia, vakioita ja `struct`-kenttiä. Paketit käyttävät vietyjä tunnisteita julkisen rajapintansa määrittelyyn ja pitävät toteutusyksityiskohdat sisäisinä. Erillisenä sääntönä `internal/`-hakemiston alla sijaitsevan paketin voi tuoda vain koodi, jonka tuontipolku kuuluu kyseisen `internal`-hakemiston ylähakemistopuuhun; tätä valvoo Go-työkaluketju.
// 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.
14Miten `defer` toimii Go-kielessä, mukaan lukien suoritusjärjestys, argumenttien evaluointiaika ja vuorovaikutus paluuarvojen kanssa?
`defer` ajastaa funktiokutsun suoritettavaksi, kun ympäröivästä funktiosta poistutaan, riippumatta siitä, poistutaanko siitä normaalilla paluulla vai `panic`-tilan purkamisen kautta. Useat viivästetyt kutsut suoritetaan LIFO (Last-In, First-Out) -järjestyksessä. Viivästetyn funktion arvo ja sen argumentit evaluoidaan välittömästi, kun `defer`-lause suoritetaan, mutta itse kutsu ajetaan myöhemmin. Nimetyillä paluuarvoilla `return`-lause sijoittaa paluuarvot ensin, minkä jälkeen viivästetyt funktiot suoritetaan. Tämän ansiosta viivästetty sulkeuma voi havainnoida tai muokata nimettyjä tulosmuuttujia ennen kuin kutsuja vastaanottaa ne. Tämä tekee `defer`-lauseesta hyödyllisen siivoustoimenpiteissä, kuten tiedostojen sulkemisessa, muteksien lukituksen avaamisessa ja resurssien vapauttamisessa.
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())
}
15Selitä Go-kielen virheidenkäsittelymalli ja tavanomaiset tavat, joilla virheitä luodaan, palautetaan ja tarkistetaan.
Go käsittelee virheitä tavallisina arvoina, ei poikkeuksina. Mikä tahansa tyyppi, jolla on `Error() string` -metodi, toteuttaa sisäänrakennetun `error`-rajapinnan. Funktiot palauttavat tavanomaisesti `error`-arvon viimeisenä tuloksenaan, jolloin `nil` tarkoittaa onnistumista ja siitä poikkeava virhe tarkoittaa, että kutsujan on käsiteltävä tai välitettävä virhe eteenpäin. Yksinkertaisia virheitä luodaan yleensä `errors.New`-funktiolla, muotoiltuja virheitä `fmt.Errorf`-funktiolla, ja kutsujat tarkistavat tyypillisesti virheet muodossa `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
}
16Miten `panic`-tilanteista toipumista tulisi käsitellä Go-taustapalveluissa, mukaan lukien mitä tapahtuu, kun `goroutine` panikoi, ja milloin prosessin tulisi toipua kaatumisen sijaan?
`panic` purkaa nykyisen `goroutine`:n kutsupinon ja suorittaa sen viivästetyt funktiot (`defer`). `recover` toimii vain, kun sitä kutsutaan saman `goroutine`:n viivästetystä funktiosta; yksi `goroutine` ei voi toipua toisen `goroutine`:n `panic`-tilanteesta. Jos `panic`-tilanteesta ei toivuta, koko prosessi kaatuu. Taustapalveluissa toipuminen tulisi yleensä sijoittaa eristysrajoille, kuten pyyntöjen käsittelijöihin, RPC (Remote Procedure Call) -välikerroksiin tai taustatehtäviä suorittavien `goroutine`-rutiinien aloituspisteisiin, jotta yksi epäonnistuva pyyntö tai työ ei kaada koko palvelua. Mutta jos on mahdollista, että `panic` on korruptoinut jaettua tilaa tai tehnyt prosessin eheydestä epäluotettavan, on turvallisempaa antaa prosessin kaatua ja käynnistyä uudelleen sen sijaan, että se toipuisi ja jatkaisi toimintaansa sokeasti.
17Mitä ovat `nil`-kanavat Go-kielessä, ja miten ne voivat vahingossa rikkoa koodia tai tarkoituksella poistaa `select`-rakenteen `case`-vaihtoehtoja käytöstä?
`nil`-kanava on kanavamuuttuja, jonka arvo on `nil`. Tämä johtuu usein siitä, että sitä ei ole alustettu `make`-funktiolla tai se on nimenomaisesti asetettu arvoon `nil`. `nil`-kanavalle lähettäminen tai siltä lukeminen estää suorituksen ikuisesti. `select`-rakenteessa `nil`-kanavaa käyttävä `case` ei ole koskaan valmis suoritettavaksi, joten kanavamuuttujan asettaminen arvoon `nil` voi tarkoituksella poistaa kyseisen `case`-vaihtoehdon käytöstä. `nil`-kanavan käyttö vahingossa voi aiheuttaa goroutine-säikeiden jumiutumisen tai estää `select`-logiikkaa käsittelemästä odotettuja tapahtumia.
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
}
}
18Miten `sync/atomic`-paketin atomiset operaatiot eroavat mutex-pohjaisesta synkronoinnista, ja milloin niiden käyttö on perusteltua?
`sync/atomic`-paketti tarjoaa yksittäisiin muistipaikkoihin kohdistuvia jakamattomia operaatioita, kuten lataus, tallennus, lisäys, vaihto sekä vertailu ja vaihto (compare-and-swap), taaten samalla synkronoinnin ja muistioperaatioiden järjestyksen (memory ordering). Mutex-lukko suojaa kriittistä aluetta, joten se voi suojata mielivaltaista koodia ja invariantteja, jotka koskevat useita luku- ja kirjoitusoperaatioita tai kenttiä. Atomiset operaatiot sopivat yksinkertaisiin, toisistaan riippumattomiin tiloihin, kuten laskureihin, tilalippuihin, järjestysnumeroihin tai huolellisesti suunniteltuihin lukottomiin (lock-free) tietorakenteisiin. Suosi mutex-lukkoa, kun operaatiot ovat moniosaisia, useiden arvojen on pysyttävä yhdenmukaisina tai kun koodin atomista versiota olisi vaikea hahmottaa tai todistaa oikeaksi.
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
}
19Kuinka kanavien omistajuus ja goroutine-rutiinien elinkaari tulisi suunnitella, jotta vältytään goroutine-vuodoilta?
Suunnittele `goroutine`-rutiineille selkeä omistaja, yksiselitteinen sammutussignaali ja taattu poistumisreitti. Tuottajapuoli vastaa yleensä kanavan sulkemisesta, erityisesti tulostekanavien osalta; vastaanottajat eivät saisi sulkea kanavaa silloin, kun lähettäjiä saattaa olla vielä aktiivisina. Jokaisen estävän lähetys- tai vastaanotto-operaation, silmukan, ajastimen tai ulkoisen kutsun on joko taatusti valmistuttava tai vapauduttava peruutuksen yhteydessä, yleensä `context.Context`-rajapinnan tai `done`-kanavan avulla. Käytä `WaitGroup`-, `errgroup`- tai vastaavia koordinaatiomekanismeja, jotta työntekijöiden valmistumista odotetaan ja kanavat suljetaan vasta sen jälkeen, kun lähettäjät ovat lopettaneet toimintansa.
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
}
20Mitkä ovat yleisimpiä syitä goroutine-vuotoihin Go-palveluissa, ja miten ne havaitaan ja korjataan tuotannossa?
Yleisimpiä syitä goroutine-vuotoihin Go-palveluissa ovat ikuisesti kanavien lähetys- tai vastaanotto-operaatioihin jumiutuneet goroutinet, muiden estävien operaatioiden odottaminen ilman peruutusta, jumiutunut I/O (Input/Output) ilman aikarajoja, taustasilmukat tai `ticker`-ajastimet, jotka eivät koskaan pysähdy, sekä pyyntökohtaiset goroutinet, jotka jatkavat suoritustaan alkuperäisen pyynnön päätyttyä. Tuotannossa etsitään jatkuvaa kasvua goroutine-määrissä ja siihen liittyviä oireita, minkä jälkeen tutkitaan goroutine-vedoksia tai `pprof`-työkalun goroutine-profiileja, jotta nähdään mihin goroutinet ovat jumiutuneet. Vuodon korjaaminen tarkoittaa koodin muuttamista siten, että kyseiset goroutinet pystyvät poistumaan: lisää peruutuksia ja aikarajoja, pysäytä `ticker`-ajastimet, sulje kanavat oikein, vältä irtaantuneita pyyntökohtaisia goroutineja ja rajoita rinnakkaisuutta tarvittaessa.
func handler(w http.ResponseWriter, r *http.Request) {
go func() {
result := <-slowCh // may block forever
_ = result
}()
}