Valmistautuminen Go-haastatteluun

Go-backend-kehittäjän haastattelukysymykset

Valikoituja Go-haastattelukysymyksiä backend-kehittäjille aihepiireittäin ryhmiteltynä. Kysymykset on poimittu samasta tietokannasta, joka toimii EngineerSpeak-harjoittelun ytimenä.

Aloita tekoälypohjainen Go-backend-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 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)
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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))
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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)
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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)
    }
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Tietorakenteet

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))
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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
    }
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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)
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Kielen semantiikka

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)
    }
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Paketit

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") }
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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.
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Suorituksen ohjaus

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())
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Virheiden käsittely

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
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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.

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)
    })
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Samanaikaisuus

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
    }
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

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
    }()
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa