Najpopularniejsze pytania rekrutacyjne z Go (GoLang) z odpowiedziami

Mateusz Bogolubow

09-11-2026

Slice'y i mapy, interfejsy, defer, goroutine, kanały i context. Sprawdź, jakie pytania z Go (GoLang) padają na rozmowach i jak na nie odpowiadać, z uruchomionymi przykładami.

Najpopularniejsze pytania rekrutacyjne z Go (GoLang) z odpowiedziami
Na rozmowach z Go najczęściej pada kilkanaście pytań: czym różni się slice od tablicy, dlaczego zapis do nil mapy powoduje panikę, jak działają interfejsy i obsługa błędów, co robi defer oraz czym są goroutine, kanały i context. Rekruter nie sprawdza, czy znasz całą bibliotekę standardową, tylko czy rozumiesz, jak Go zarządza pamięcią i współbieżnością. Dobra odpowiedź to jedno zdanie definicji i krótki przykład kodu.

Go (GoLang) pojawia się w ofertach na backend i infrastrukturę, a rozmowa o nim ma zwykle prosty schemat: kilkanaście tematów, w których rekruter sprawdza, czy rozumiesz mechanizm, a nie czy pamiętasz nazwy funkcji. Poniżej znajdziesz pytania, które wracają najczęściej, razem z tym, jak na nie odpowiadać. Jeśli dopiero zaczynasz, najpierw przeczytaj artykuł o tym, jak zainstalować Go, gdzie jest używany i jak wygląda rynek pracy, a ten tekst potraktuj jako listę kontrolną. Podobne pytania o język, który często jest drugim w CV Go developera, znajdziesz w artykule o pytaniach rekrutacyjnych z Pythona.

Wszystkie przykłady uruchomiliśmy w Go 1.27.3: sprawdziliśmy zachowanie slice'ów i map, defer, interfejsów, kanałów, select, context, generyków, detektora wyścigów i testów. Wyniki w tekście, na przykład wartości len i cap czy komunikaty błędów, pochodzą z tych uruchomień.

Jak odpowiadać na pytania rekrutacyjne z Go?

Najlepiej w trzech krokach: jedno zdanie definicji, krótki przykład (kilka linijek kodu albo sytuacja z projektu) i informacja, kiedy to ma znaczenie w praktyce. Taka odpowiedź zajmuje 30–60 sekund i pokazuje, że rozumiesz mechanizm, a nie wkułeś formułkę. Jeśli czegoś nie wiesz, powiedz, co wiesz, i rozumuj na głos. Rekruterzy oceniają sposób myślenia, a nie tylko wynik.

Pytania są ułożone od podstaw do tematów, które częściej pojawiają się przy ofertach na mid-poziom.

Podstawy Go

Czym jest Go i czym różni się od Pythona i Javy?

Go to kompilowany, statycznie typowany język stworzony w Google, z automatycznym zarządzaniem pamięcią (garbage collector) i wbudowaną współbieżnością. Kompilator zamienia kod na jeden plik wykonywalny, więc nie potrzebujesz maszyny wirtualnej ani interpretera na serwerze, jak w Javie czy Pythonie. Go nie ma klas ani dziedziczenia, tylko struktury, metody i interfejsy, a obsługę błędów realizuje przez zwracanie ich jako wartości zamiast wyjątków. Więcej o historii i zastosowaniach piszemy w artykule o języku Go.

Czym jest wartość zerowa (zero value)?

Każda zmienna w Go ma wartość domyślną, nawet jeśli jej nie ustawisz: 0 dla liczb, "" dla napisów, false dla wartości logicznych i nil dla wskaźników, slice'ów, map, kanałów i interfejsów. Dlatego new(int) wskazuje na 0, a nie na „nic”. Dobrze zaprojektowane typy w Go działają od razu po utworzeniu z wartością zerową.

Czym różni się var od :=?

var x int deklaruje zmienną z wartością zerową i działa wszędzie, także poza funkcjami. Skrócone x := 5 deklaruje i przypisuje wartość jednocześnie, ale jest dozwolone tylko wewnątrz funkcji, a typ wynika z wartości.

Czym różni się make od new?

new(T) alokuje pamięć na wartość typu T i zwraca wskaźnik do niej, wypełniony wartością zerową. make działa tylko dla slice'ów, map i kanałów i zwraca gotową do użycia strukturę. W teście new(int) dało 0, make([]int, 2, 5) slice o długości 2 i pojemności 5, a make(chan int, 1) kanał o pojemności 1. Dlatego mapę zawsze tworzysz przez make (albo literał), a nie przez new.

Jak Go decyduje, co jest publiczne?

Przez wielką literę. Nazwa zaczynająca się od wielkiej litery (Divide) jest eksportowana z pakietu, a z małej (divide) jest prywatna. Nie ma słów kluczowych public ani private.

Czym jest moduł i po co go mod init?

Moduł to jednostka zarządzania zależnościami, opisana w pliku go.mod. go mod init nazwa tworzy go w bieżącym katalogu. Uważaj na nazwę: moduł nazwany tak samo jak pakiet biblioteki standardowej (slices, maps, embed) kończy się błędem ambiguous import: found package … in multiple modules. Trafiliśmy na to podczas przygotowywania przykładów do tego artykułu.

Slice'y, mapy i napisy

Czym różni się tablica od slice'a?

Tablica ma stałą długość będącą częścią typu ([3]int) i przy przypisaniu jest kopiowana. Slice to „okno” na tablicę: ma wskaźnik do danych, długość (len) i pojemność (cap), i przy przypisaniu kopiuje się tylko ten opis, a nie dane. W teście zmiana kopii tablicy nie zmieniła oryginału ([0 0 0] i [5 0 0]), a dwa slice'y mogą współdzielić te same dane.

Co się stanie po append, gdy slice współdzieli dane?

To najpopularniejsze pytanie o slice'y. Przykład:

a := []int{1, 2, 3}
b := a[:2]
b = append(b, 99)
fmt.Println(a, b, len(b), cap(b))   // [1 2 99] [1 2 99] 3 3

c := append(a, 4)
c[0] = -1
fmt.Println(a, c, len(c), cap(c))   // [1 2 99] [-1 2 99 4] 4 6

Pierwszy append zmieścił się w pojemności slice'a b, więc nadpisał trzeci element wspólnej tablicy i zmienił a. Drugi append przekroczył pojemność, więc Go zaalokowało nową tablicę (pojemność 6) i zmiana c[0] nie dotknęła a. Wniosek: po append zawsze przypisuj wynik do zmiennej i nie zakładaj, czy dane zostały skopiowane.

Dlaczego zapis do nil mapy powoduje panikę?

Mapa, której nie utworzyłeś przez make, ma wartość nil. Odczyt z niej jest bezpieczny (zwraca wartość zerową i len równe 0), ale zapis kończy się paniką assignment to entry in nil map. Ten błąd ma na Stack Overflow ponad 200 tys. wyświetleń. Rozwiązanie to m := make(map[string]int) albo literał map[string]int{}. Do sprawdzania, czy klucz istnieje, służy forma z drugą wartością: v, ok := m["b"] zwraca 0 false dla brakującego klucza.

Czy kolejność elementów w mapie jest stała?

Nie. Specyfikacja języka nie gwarantuje żadnej kolejności iteracji po mapie, więc nie wolno na niej polegać. Jeśli potrzebujesz porządku, posortuj klucze osobno.

Czym różni się bajt, rune i napis?

Napis w Go to ciąg bajtów w kodowaniu UTF-8. len("zażółć") zwraca 10 (bajtów), a nie 6 (znaków), bo polskie znaki zajmują po dwa bajty. Liczbę znaków (runes) zwraca utf8.RuneCountInString(s) albo len([]rune(s)): w teście oba dały 6. Pętla for i, r := range s iteruje po znakach, ale i to indeks bajtowy, dlatego dla "żó" zobaczyliśmy indeksy 0 i 2.

Metody, interfejsy i błędy

Czym różni się receiver wartościowy od wskaźnikowego?

Metoda z receiverem wartościowym (func (c Counter) Inc()) pracuje na kopii, więc nie zmienia oryginału. Z receiverem wskaźnikowym (func (c *Counter) Inc()) zmienia oryginał. W teście IncValue zostawiło licznik na 0, a IncPointer ustawił go na 1. Receiver wskaźnikowy wybierasz, gdy metoda zmienia stan albo struktura jest duża, a trzymanie jednego rodzaju receivera w typie to dobra praktyka. To jedno z najczęściej pytanych zagadnień: pytanie o receivery ma na Stack Overflow ponad 87 tys. wyświetleń.

Jak działają interfejsy w Go?

Interfejs to zestaw metod, a typ go spełnia, jeśli ma te metody, bez żadnej deklaracji (implicit). Nie piszesz implements. Dzięki temu interfejs możesz zdefiniować po stronie kodu, który go używa, a nie po stronie typu. Pamiętaj o szczegółach: jeśli metoda ma receiver wskaźnikowy, interfejs spełnia wskaźnik (&c), a nie wartość. W teście var i Incer = &c zadziałało właśnie dlatego.

Dlaczego interfejs z nil wskaźnikiem nie jest równy nil?

Interfejs w Go przechowuje dwie rzeczy: typ i wartość. Jest równy nil tylko wtedy, gdy obie są puste. Przykład:

type MyErr struct{}

func (*MyErr) Error() string { return "błąd" }

func doWork() error {
    var e *MyErr   // nil wskaźnik
    return e
}

err := doWork()
fmt.Println(err == nil)   // false

Funkcja zwraca interfejs error z typem *MyErr i pustą wartością, więc porównanie z nil daje false, a kod if err != nil uzna, że wystąpił błąd. Zasada: gdy nie ma błędu, zwracaj jawnie nil, a nie nil-owy wskaźnik konkretnego typu.

Czy w Go jest dziedziczenie?

Nie, zamiast niego jest osadzanie (embedding). W teście Dog z osadzonym Animal dostał jego pola i metody, ale ponieważ zdefiniowaliśmy własną metodę Speak, zadziałała ona jako pierwsza: d.Speak() zwróciło „Reks szczeka”, a d.Animal.Speak() „Reks robi hałas”. To kompozycja, a nie polimorfizm przez klasę bazową.

Jak działa obsługa błędów w Go?

Funkcje zwracają błąd jako ostatnią wartość, a wywołujący sprawdza go przez if err != nil. Błąd można opakować (fmt.Errorf("find %d: %w", id, err)) i później rozpakować funkcjami errors.Is (czy w łańcuchu jest dany błąd) i errors.As (czy jest błąd danego typu). W teście errors.Is(e1, ErrNotFound) zwróciło true mimo opakowania, a errors.As wyciągnęło *ValidationError i jego pole Field. Panikę (panic) zostaw na sytuacje naprawdę wyjątkowe, a nie na zwykłe błędy.

Jak działa defer, panic i recover?

defer odkłada wywołanie funkcji do momentu zakończenia funkcji zewnętrznej. Odroczone wywołania wykonują się w kolejności odwrotnej (LIFO): pętla z defer fmt.Println("defer", i) wypisała 2, 1, 0. Argumenty są obliczane w chwili wywołania defer, nie wykonania: po x := 1; defer fmt.Println(x); x = 2 zobaczyliśmy 1. defer może też zmienić nazwany wynik funkcji: defer func() { n *= 2 }() przy return 21 dało 42. recover wywołany w odroczonej funkcji przechwytuje panikę, jak w przykładzie z nil mapą. Najczęstsze zastosowanie defer to zamykanie plików i zwalnianie blokady (defer mu.Unlock()).

Współbieżność

Czym jest goroutine i czym różni się od wątku?

Goroutine to lekka jednostka wykonania zarządzana przez środowisko uruchomieniowe Go, a nie bezpośrednio przez system operacyjny. Startujesz ją słowem go przed wywołaniem funkcji, a środowisko rozdziela wiele goroutine na niewielką liczbę wątków systemowych. Dzięki temu możesz uruchomić ich tysiące bez dużego kosztu. Podstawowy przykład z sync.WaitGroup opisaliśmy w artykule o języku Go.

Czym różni się kanał buforowany od niebuforowanego i skąd deadlock?

Kanał niebuforowany działa jak przekazanie z ręki do ręki: wysyłający czeka, aż ktoś odbierze. Kanał buforowany (make(chan int, 1)) pozwala wysłać tyle wartości, ile mieści bufor, bez czekania. Wysyłanie i odbieranie na niebuforowanym kanale w tej samej goroutine kończy się komunikatem fatal error: all goroutines are asleep - deadlock! (to pytanie ma na Stack Overflow ponad 90 tys. wyświetleń). Rozwiązanie: odbiorca w osobnej goroutine albo bufor, przy czym bufor często tylko odsuwa problem.

Do czego służy select?

Pozwala czekać na kilka kanałów naraz. Z gałęzią default nie blokuje (w teście wypisał „brak danych”), a z time.After pozwala ustawić limit czasu (po 100 ms wypisał „timeout”). Odczyt z zamkniętego kanału zwraca wartość zerową i false w drugiej zmiennej ({} false), więc tak sprawdza się, że kanał został zamknięty.

Czym jest wyścig danych (data race) i jak go uniknąć?

To sytuacja, w której kilka goroutine jednocześnie czyta i zapisuje tę samą zmienną bez synchronizacji. W teście tysiąc goroutine wykonujących count++ zwróciło 952 zamiast 1000, a wynik był za każdym razem inny. Uruchomienie z flagą -race (go run -race .) wypisało WARNING: DATA RACE ze wskazaniem linii. Rozwiązania to sync.Mutex (z ochroną mu.Lock() i defer mu.Unlock(), co dało 1000), pakiet sync/atomic albo przekazywanie danych kanałami. Zasada, którą powtarza się w świecie Go: nie komunikuj się przez współdzielenie pamięci, współdziel pamięć przez komunikację.

Do czego służy context?

Pakiet context przenosi przez wywołania sygnał anulowania i limit czasu. Funkcja, która wykonuje pracę, nasłuchuje na ctx.Done(). W teście context.WithTimeout z limitem 100 ms przerwał pracę trwającą 500 ms i zwrócił context deadline exceeded. Dobra praktyka: context jest pierwszym argumentem funkcji, a po utworzeniu zawsze wywołujesz defer cancel().

Dlaczego pętla z goroutine wypisywała same trójki?

Do wersji Go 1.21 zmienna pętli i była jedna dla całej pętli, więc goroutine uruchomione w pętli widziały jej końcową wartość. W teście z go 1.21 w pliku go.mod wynik to [3 3 3], a z go 1.22 [0 1 2], bo od wersji 1.22 każda iteracja ma własną kopię zmiennej. To ważne pytanie, bo starsze projekty nadal mogą mieć niższą wersję w go.mod, a rozwiązaniem jest przekazanie i jako argumentu funkcji. Wersję języka sprawdzisz w pliku go.mod.

Jak zbudować prostą pulę workerów?

To popularne zadanie z kodem. Kilka goroutine czyta z kanału zadań i wysyła wyniki na drugi kanał:

jobs := make(chan int, 10)
results := make(chan int, 10)
var wg sync.WaitGroup
for w := 0; w < 3; w++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        for j := range jobs {
            results <- j * j
        }
    }()
}
for i := 1; i <= 5; i++ {
    jobs <- i
}
close(jobs)
wg.Wait()
close(results)

Po zebraniu i posortowaniu wyników otrzymaliśmy [1 4 9 16 25]. Kluczowe elementy: zamknięcie kanału zadań (close(jobs)) kończy pętle range w workerach, WaitGroup czeka na workery, a dopiero potem zamykamy kanał wyników. Kolejność zamykania jest częstym powodem błędów.

Generyki i testy

Czym są generyki (typy generyczne) w Go?

Pozwalają pisać funkcje działające na wielu typach, z ograniczeniem opisanym interfejsem. Pojawiły się w Go 1.18. W teście funkcja Sum[T Number] zsumowała zarówno []int (6), jak i []float64 (4), a Map[T, U any] zamieniła []int na []string ([1 4 9]). Używaj ich, gdy powtarzasz tę samą logikę dla różnych typów, a nie „na zapas”.

Jak testuje się kod w Go?

Narzędziem wbudowanym w język: plik calc_test.go, funkcje TestXxx(t *testing.T) i polecenie go test. Typowy wzorzec to test tabelaryczny (table-driven) z podtestami t.Run:

tests := []struct {
    name    string
    a, b    int
    want    int
    wantErr bool
}{
    {"zwykłe", 6, 3, 2, false},
    {"zero", 1, 0, 0, true},
}
for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        got, err := Divide(tt.a, tt.b)
        if (err != nil) != tt.wantErr || got != tt.want {
            t.Fatalf("Divide(%d,%d) = %d, %v", tt.a, tt.b, got, err)
        }
    })
}

W naszym przykładzie go test -v uruchomiło oba podtesty i zwróciło PASS. Benchmarki (testing.B) i flagę -race wspominaj, gdy rozmowa dotyczy jakości kodu.

Zadania z kodem i mini-test

Jakie zadania z Go pojawiają się na rozmowach?

Najczęściej krótkie zadania typu: „napisz pulę workerów”, „zsumuj liczby z kilku goroutine bez wyścigu”, „odwróć napis ze znakami Unicode” albo „znajdź błąd w tym kodzie”. Przy takich zadaniach liczy się nie szybkość pisania, tylko to, czy rozumiesz, co zrobi środowisko uruchomieniowe. Mów na głos, co się stanie z Twoim kodem.

Co jest nie tak w tym kodzie? Mini-test na rozgrzewkę

func main() {
    var counts map[string]int
    words := []string{"a", "b", "a"}
    for _, w := range words {
        go func() {
            counts[w]++
        }()
    }
    fmt.Println(counts)
}

Są tu co najmniej trzy problemy:

  1. Nil mapa. counts nie została utworzona przez make, więc zapis counts[w]++ powoduje panikę assignment to entry in nil map. W pięciu uruchomieniach program za każdym razem kończył się tą paniką, a w dwóch z nich zdążył wcześniej wypisać pustą mapę (map[]).
  2. Brak oczekiwania na goroutine. main wypisuje mapę bez czekania na goroutine, więc wynik zależy od tego, kto był pierwszy. To dlatego zachowanie różniło się między uruchomieniami. Potrzebny jest sync.WaitGroup albo kanał.
  3. Wyścig danych na mapie. Nawet po poprawce mapy wiele goroutine zapisuje do niej jednocześnie, a mapy w Go nie są bezpieczne współbieżnie. Potrzebny jest sync.Mutex (lub przekazywanie wyników kanałem). Zmienną pętli w w nowszych wersjach Go (od 1.22) każda goroutine widzi poprawnie, ale w starszych byłby tu czwarty problem.

Jak przygotować się do rozmowy z Go?

  1. Zbuduj małą aplikację od zera. Najlepiej prosty serwer HTTP z pakietem net/http albo narzędzie wiersza poleceń, z testami tabelarycznymi. Instalację i pierwszy program opisaliśmy w artykule o języku Go. Gotowy kod pokaż doświadczonej osobie do code review: uwagi o idiomach Go, obsłudze błędów i goroutine to najszybszy sposób, żeby poprawić się przed rozmową.
  2. Uruchom wszystko z flagą -race. Zobacz na własne oczy wyścig danych, a potem go napraw. Rozumienie tej różnicy odróżnia kandydatów.
  3. Przerób kilka klasycznych zadań. Pula workerów, pipeline z kanałami, anulowanie przez context.
  4. Przećwicz odpowiedzi na głos. Dla każdego pytania z tego artykułu sformułuj w 30 sekund definicję i przykład.

Na czym juniorzy najczęściej tracą punkty?

Nie na trudnych tematach, tylko na trzech drobiazgach, które wychodzą dopiero przy pytaniach uzupełniających:

  1. Slice'y jako „listy”. Odpowiedź „slice to dynamiczna tablica” kończy temat. Wyjaśnij długość, pojemność i współdzielenie danych, bo to właśnie pytanie uzupełniające o append.
  2. Goroutine bez synchronizacji. Uruchamianie goroutine bez WaitGroup i bez ochrony współdzielonych danych. Pokaż, że znasz -race.
  3. Mylenie nil w interfejsie. Wiele osób nie wie, że interfejs z nil wskaźnikiem nie jest równy nil. Wspomnij o tym przy obsłudze błędów.

Jak brzmi dobra odpowiedź, a jak słaba?

Weźmy pytanie o slice'y:

OdpowiedźDlaczego
Słaba„Slice to taka tablica, która może rosnąć.”Brak mechanizmu. Pytanie uzupełniające („co się stanie z drugim slice'em po append?”) od razu to pokaże.
Dobra„Slice to okno na tablicę: ma wskaźnik, długość i pojemność. Dwa slice'y mogą współdzielić dane, więc append w granicach pojemności zmienia obie, a po przekroczeniu pojemności Go tworzy nową tablicę. Dlatego zawsze przypisuję wynik append.”Jest definicja, konsekwencja i reguła postępowania.

Ten sam schemat działa przy każdym pytaniu z tego artykułu: definicja, mechanizm, przykład użycia.

Co zrobić, gdy nie znasz odpowiedzi?

Nie zgaduj i nie milcz. Powiedz, co wiesz na pewno, a potem rozumuj na głos, np.: „Nie pamiętam dokładnej nazwy tej funkcji, ale wiem, że szukam sposobu na bezpieczne zliczanie z wielu goroutine, więc użyłbym mutexa albo kanału i sprawdził w dokumentacji.” Taka odpowiedź pokazuje, że potrafisz sobie poradzić w pracy, a umiejętność znalezienia odpowiedzi jest w codziennej pracy ważniejsza niż pamięć do szczegółów.

Ogólny przebieg całego procesu rekrutacji opisujemy w poradniku jak przygotować się do rekrutacji na juniora.

Masz pytanie do artykułu? Chcesz zgłosić błąd?

Pytaj ogólnie o temat artykułu albo napisz, co poprawić. Jeśli chcesz omówić swój konkretny przypadek, umów się na rozmowę. Ze względów bezpieczeństwa nie przeglądamy ani nie publikujemy żadnych linków. Wiadomości z linkami nie są zapisywane. Limit: 2 pytania na godzinę.

Wyślemy na niego link do potwierdzenia pytania i odpowiedź.

Ostateczną decyzję o opublikowaniu pytania podejmuje administrator.

Konsultacja i warsztaty devmentor.pl

Masz pytania?

Partnerzy

Firmy, z którymi współpracujemy

To partnerzy odpowiedzialni za staże po naszych kursach. Organizują je dla najlepszych absolwentów.

Zobacz wszystkich partnerów →
  • VR Medical
  • Sophona
  • Robot Force