Valmistautuminen Middle Go -haastatteluun

Middle Go-backend-kehittäjän haastattelukysymykset

15 valikoitua Middle-tason Go-haastattelukysymystä backend-kehittäjille, joiden tulee osata selittää käytännön kompromisseja, samanaikaisuutta ja palveluiden toimintaa.

Aloita tekoälypohjainen Middle Go -haastatteluEi vaadi luottokorttia. 1 ilmainen sessio saatavilla.
Teknisten haastattelujen harjoittelu englanniksiTila, jossa ei-äidinkieliset puhujat voivat harjoitella teknisen haastattelun läpäisemistä.

Tyyppijärjestelmä

1Selitä, 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`.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

2Mitä 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.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

3Miten 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.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

4Miten tyyppialiakset (type aliases) eroavat määritellyistä tyypeistä (defined types), ja milloin kumpaakin tulisi käyttää?

Määritelty tyyppi, kuten `type UserID int64`, luo uuden, erillisen tyypin, jonka pohjatyyppi on `int64`. Sitä ei voi suoraan sijoittaa `int64`-muuttujaan ilman tyyppimuunnosta, ja sillä voi olla omat metodinsa. Tyyppialias, kuten `type UserID = int64`, on vain toinen nimi samalle tyypille, jolloin tyypin identiteetti ja sijoitettavuus säilyvät. Käytä määriteltyjä tyyppejä toimialuemallinnuksessa (domain modeling), tyyppiturvallisuuden parantamisessa ja metodien luomisessa. Käytä aliaksia pääasiassa uudelleenjärjestelyyn, migraatioihin tai yhteensopivuuden ylläpitämiseen ilman uuden tyypin esittelemistä.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

5Miten Go käsittelee liukulukujen erikoisarvoja ja mitkä yhtäsuuruuteen liittyvät sudenkuopat ovat tärkeitä taustajärjestelmissä?

Go-kielen `float32` ja `float64` noudattavat IEEE-754-tyylistä käyttäytymistä, mukaan lukien erikoisarvot kuten positiivinen ja negatiivinen äärettömyys sekä NaN (Not a Number). `float64`-tyypille löytyy apufunktioita kuten `math.Inf`, `math.IsInf`, `math.NaN` ja `math.IsNaN`. NaN ei ole yhtä suuri minkään kanssa, ei edes itsensä, joten vertailu `x == x` on epätosi, kun `x` on NaN. Laskettujen liukulukujen tarkka yhtäsuuruusvertailu on muutenkin riskialtista, koska pyöristykset ja tarkkuusrajat voivat saada matemaattisesti yhtä suuret arvot eroamaan toisistaan. Käytä toimialueelle sopivia toleransseja tai vältä liukulukuja tarkkojen liiketoiminta-arvojen, kuten rahan, käsittelyssä. Liukuluvut sallitaan `map`-tyyppien avaimina, mutta NaN-avaimet ovat ongelmallisia, koska `map`-haku perustuu tarkkaan yhtäsuuruuteen ja NaN ei ole yhtä suuri edes itsensä kanssa.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

6Kuvaile `struct`-rakenteiden upotusta (struct embedding) Go-kielessä ja miten ylennetyt kentät ja metodit (promoted fields and methods) toimivat.

`struct`-upotus Go-kielessä tarkoittaa kentän määrittämistä sen tyypin avulla ilman erillistä kentän nimeä, esimerkiksi `type User struct { Person }`. Upotettu arvo on edelleen todellinen kenttä, johon pääsee käsiksi muodossa `u.Person`, mutta sen julkiset kentät ja metodit voidaan ylentää (promote) niin, että kutsujat voivat kirjoittaa valitsimia kuten `u.Name` tai `u.Greet()` lyhenteenä upotetun kentän kautta kulkemiselle. Upottaminen on koosteisuutta (composition), ei klassista periytymistä: ulompi tyyppi ei ole automaattisesti upotetun tyypin alityyppi. Jos ylennetyt valitsimet ovat ristiriidassa keskenään, Go ei arvaa oikeaa vaihtoehtoa; moniselitteiset nimet on tarkennettava nimiavaruudella, tai niitä ei voi valita ulomman arvon kautta.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Tietorakenteet

7Kuvaile, 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.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

8Selitä `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.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Paketit

9Mitä ovat init-vaiheen rekisteröintimallit (init-time registration patterns) Gossa, ja mitä riskejä liittyy tyhjien tuontien (blank import) sivuvaikutuksiin ja globaaleihin rekistereihin?

Init-vaiheen rekisteröintimallissa paketti rekisteröi toteutuksen jaettuun rekisteriin `init`-funktiosta käsin. Tyhjää tuontia (blank import), kuten `_ "example.com/driver"`, käytetään usein paketin tuomiseen pelkästään sen sivuvaikutusten vuoksi, mikä saa sen `init`-funktion suorittumaan, vaikka mihinkään vietyihin (exported) nimiin ei viitattaisi. Tämä on yleistä ajureiden, koodekkien, liitännäisten, metriikan tai sarjallistajien laajennuspisteissä. Riskeinä ovat piilotetut riippuvuudet ja käynnistyksen aikaiset sivuvaikutukset, globaali muuttuva tila, päällekkäinen tai suoritusjärjestyksestä riippuvainen rekisteröinti, vaikeampi testien eristäminen ja vähemmän eksplisiittinen riippuvuuksien kytkentä. Sitä tulisi käyttää harkitusti, dokumentoida selkeästi ja usein lieventää eksplisiittisellä rekisteröinnillä, idempotenteilla tai samanaikaisuuden kestävillä rekistereillä taikka injektoitavilla/nollattavilla rekistereillä testejä varten.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Virheiden käsittely

10Miten `defer`, `panic` ja nimetyt paluuarvot vuorovaikuttavat keskenään, kun toteutetaan siivoustoimenpiteitä, jotka saattavat muokata palautettavia virheitä?

`defer`-funktiot suoritetaan sen jälkeen, kun paluuarvot on asetettu, mutta ennen kuin funktio palaa kutsujalleen. Tämän vuoksi `defer`-määritelty sulkeuma voi lukea tai muokata nimettyjä paluuarvoja, kuten nimettyä `err`-muuttujaa. Tätä käytetään yleisesti lisäämään siivousvirheitä `Close`-, `Commit`- tai vastaavista operaatioista palautettavaan virheeseen, ihanteellisesti säilyttäen ensisijaisen virheen ylikirjoittamisen sijaan. Paniikin purkamisen (panic unwinding) aikana `defer`-funktiot suoritetaan yhä; `defer`-funktio voi kutsua `recover`-funktiota toipuakseen ja asettaa nimetyn paluuarvon, mutta tämä tulisi rajoittaa vain tarkoituksellisiin paniikin pysäytyskohtiin. Tulee olla varovainen, ettei nimettyä paluuarvomuuttujaa, kuten `err`, peitetä vahingossa, koska tällöin `defer` saattaa havainnoida tai muokata eri muuttujaa kuin oli tarkoitus.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

11Miten `errors.Is`, `errors.As` ja `%w` toimivat Go:n virheketjuissa?

`fmt.Errorf` yhdessä `%w`:n kanssa luo uuden virheen, joka paketoi alkuperäisen virheen lisäten siihen samalla kontekstia. Paketoidut virheet paljastavat taustavirheet `Unwrap`-metodin avulla muodostaen ketjun tai puun, jota standardikirjasto voi tutkia. `errors.Is(err, target)` tarkistaa, vastaako `err` tai mikään sen paketoimista virheistä annettua kohdevirhettä. `errors.As(err, &target)` tarkistaa, onko `err` tai mikään sen paketoimista virheistä sijoitettavissa kohdetyyppiin, ja tallentaa löydetyn arvon annettuun osoittimeen.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

12Mitä ovat sentinel-virheet (sentinel errors), ja mitkä ovat niiden hyvät ja huonot puolet verrattuna kustomoituihin tyypitettyihin virheisiin tai rikkaampiin sovellusalueen virhemalleihin?

Sentinel-virhe on nimetty virhearvo, usein pakettitason muuttuja kuten `var ErrNotFound = errors.New("not found")`, jota käytetään edustamaan tiettyä tilaa. Kutsujat voivat testata tätä tyypillisesti `errors.Is`-funktiolla, kun virheitä on paketoitu (wrapped). Sentinel-virheet ovat yksinkertaisia ja hyödyllisiä laajoille, vakaille luokille, mutta julkisista sentinel-virheistä tulee osa API-rajapintaa, mikä voi kytkeä kutsujat tiettyihin arvoihin. Kustomoidut tyypitetyt virheet voivat sisältää rakenteellisia kenttiä, ja ne voidaan löytää `errors.As`-funktiolla. Rikkaammat sovellusalueen virhemallit luokittelevat epäonnistumiset tyypin tai koodin mukaan ja voivat sisältää turvallisia viestejä tai metatietoja, mikä on hyödyllistä, kun kutsujat tarvitsevat vakaata käyttäytymistä yhden kiinteän virhearvon lisäksi.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

13Miten Go-taustajärjestelmän tulisi muuntaa sisäiset virheet asiakkaalle hyödyllisiksi vastauksiksi antaen samalla ylläpitäjille riittävät diagnostiikkatiedot?

Go-taustajärjestelmän tulisi muuntaa sisäiset virheet sovellus- tai siirtokerroksen rajapinnassa vakaiksi, asiakkaalle turvallisiksi kategorioiksi ja vastauksiksi. Näiden kategorioiden tulisi vastata asianmukaisia HTTP (Hypertext Transfer Protocol) -tilakoodeja tai vastaavia siirtotilakoodeja. Niiden tulisi sisältää turvallisia viestejä ja koneellisesti luettavia koodeja paljaiden sisäisten virheiden sijaan. Ylläpitäjien tulisi edelleen saada diagnostiikkatietoa rakenteellisista lokeista (structured logs), jäljityksistä (traces), metriikoista, korrelaatio- tai pyyntötunnisteista ja säilytetyistä alkuperäisistä virhesyistä. Lokitus on yleensä parasta tehdä kerran siinä rajapinnassa, jolla on käytettävissään pyynnön konteksti, jotta vältetään sekä vikatilanteiden huomaamatta jääminen että meluisat kaksoiskappaleet lokeissa.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

14Miten omat virhetyypit toteutetaan oikein Go-kielessä, ja miten `errors.Join` vaikuttaa virheiden tarkasteluun ja siivouspolkujen virheidenkäsittelyyn?

Oma Go-virhetyyppi toteuttaa `error`-rajapinnan toteuttamalla `Error() string` -metodin. Se voi sisältää myös rakenteellisia kenttiä ja toteuttaa `Unwrap() error` -metodin paljastaakseen alkuperäisen syyn. Osoitin- ja arvovastaanottimen valinnalla on väliä: osoitinvastaanotin tarkoittaa, että vain `*T` toteuttaa `error`-rajapinnan, kun taas arvovastaanotin tarkoittaa yleensä, että sekä `T` että `*T` toteuttavat sen. Tämä vaikuttaa kopiointiin ja siihen, mitä tyyppiä kutsujien tulisi käyttää `errors.As`-funktion kanssa. `errors.Join` yhdistää useita virheitä yhdeksi virheeksi; `errors.Is` ja `errors.As` voivat tarkastella yhdistettyjä alivirheitä. Tämä on hyödyllistä, kun halutaan palauttaa sekä varsinaisen operaation virhe että siivouksessa tai viivästetyssä suorituksessa tapahtunut virhe menettämättä kumpaakaan epäonnistumista.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Samanaikaisuus

15Miten `select` toimii kanavien kanssa, mukaan lukien valmiiden tapausten valinta, oletustapaukset (`default`) ja peruutettavat operaatiot?

`select` odottaa useita kanavaoperaatioita ja suorittaa yhden tapauksen (`case`), jonka lähetys tai vastaanotto voi edetä. Jos yksikään kanavatapaus ei ole valmis, suoritus pysähtyy, ellei oletustapausta ole määritetty. Oletustapaus suoritetaan välittömästi vain silloin, kun mikään kanavaoperaatio ei voi edetä, mikä on hyödyllistä estämättömissä lähetys- tai vastaanottoyrityksissä. Jos useampi tapaus on valmiina, Go valitsee niistä yhden näennäissatunnaisesti koodin lähdejärjestyksen sijaan. Peruutettavat kanavaoperaatiot lisäävät yleensä tapauksen, joka vastaanottaa arvon `ctx.Done()` -kanavalta, jotta goroutine voi lopettaa odottamisen, kun konteksti perutaan tai sen aikakatkaisu umpeutuu.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa