Go (Golang)
Głęboki kurs Go: gorutyny i kanały, błędy jako wartości, interfejsy strukturalne, context, generyki i pułapki współbieżności w praktyce produkcyjnej.
- Model współbieżności CSP: gorutyny, kanały (buforowane/niebuforowane), select
- Jawna obsługa błędów: errors.Is/As, opakowywanie przez %w, sentinel vs custom error types
- Niejawna, strukturalna implementacja interfejsów i pułapka typowanego nil w interfejsie
- context.Context: anulowanie, deadline'y, propagacja przez łańcuch wywołań
- Wykrywanie wyścigów danych (-race) i diagnozowanie wycieków gorutyn
- Generyki (1.18+): parametry typu, constraints, comparable
1 · Gorutyny i kanały: model współbieżności CSP
★ egzaminGo nie oferuje wątków jako podstawowej abstrakcji, tylko gorutyny: lekkie jednostki wykonania zarządzane przez runtime, komunikujące się przez kanały zamiast dzielenia pamięci pod zamkiem.
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
results <- j * j
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
var wg sync.WaitGroup
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
wg.Wait()
close(results)
for r := range results {
fmt.Println(r)
}
} select {
case res := <-resultCh:
fmt.Println("wynik:", res)
case <-time.After(2 * time.Second):
fmt.Println("timeout: brak odpowiedzi w 2s")
} - Kanał niebuforowany: wysyłka blokuje do momentu odbioru (rendezvous)
- Kanał buforowany: wysyłka blokuje dopiero po zapełnieniu bufora
- `v, ok := <-ch`: ok=false oznacza kanał zamknięty i wyczerpany
- `for v := range ch`: pętla kończy się automatycznie po close(ch)
| Cecha | Gorutyna | Wątek systemowy (OS thread) |
|---|---|---|
| Rozmiar startowy stosu | ~2 KB, rośnie dynamicznie | zwykle 1-8 MB, stały |
| Kto planuje | Scheduler Go (M:N na GOMAXPROCS wątków OS) | Scheduler systemu operacyjnego |
| Koszt utworzenia | Bardzo niski, miliony możliwe | Wysoki, praktyczny limit - tysiące |
- 1 Generator Gorutyna emitująca wartości do kanału wyjściowego i zamykająca go po zakończeniu pracy.
- 2 Etap przetwarzania Gorutyna czytająca z kanału wejściowego przez range, przetwarzająca wartość, wysyłająca dalej.
- 3 Konsument Ostatnia gorutyna (lub main) odbierająca finalne wyniki przez range aż do zamknięcia kanału.
2 · Wartości błędów, errors.Is/As i %w
★ egzaminGo nie ma wyjątków: błąd to zwykła wartość zwracana jako ostatni wynik funkcji, sprawdzana jawnie przez if err != nil, nigdy niewidocznym rzutem sterowania.
var ErrNotFound = errors.New("resource not found")
type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("invalid field: %s", e.Field)
}
func fetch(id int) error {
if id < 0 {
return &ValidationError{Field: "id"}
}
if id == 0 {
return fmt.Errorf("fetch failed: %w", ErrNotFound)
}
return nil
} func main() {
err := fetch(0)
if errors.Is(err, ErrNotFound) {
fmt.Println("nie znaleziono zasobu")
}
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Println("błąd walidacji pola:", valErr.Field)
}
} errors.Is
- Porównuje z konkretną wartością sentinel (np. io.EOF, sql.ErrNoRows)
- Przechodzi drzewo Unwrap w poszukiwaniu dopasowania (== lub metoda Is())
- Pytanie: „czy to JEST ten błąd?”
errors.As
- Szuka błędu określonego TYPU w drzewie i przypisuje go do wskaźnika
- Daje dostęp do pól/metod konkretnego typu błędu (np. *ValidationError)
- Pytanie: „czy w drzewie JEST błąd tego typu?”
- Sentinel errors (var ErrNotFound = errors.New(...)): stała, porównywalna tożsamość błędu
- Custom error types: niosą dodatkowe dane (pola struktury) o kontekście awarii
- errors.Join (od Go 1.20): łączy wiele błędów w jedno drzewo bez utraty żadnego z nich
func validate(name string, age int) error {
var errs error
if name == "" {
errs = errors.Join(errs, errors.New("name is required"))
}
if age < 0 {
errs = errors.Join(errs, errors.New("age must not be negative"))
}
return errs // nil, jeśli obie walidacje przeszły
} 3 · Interfejsy i niejawna implementacja strukturalna
★ egzaminInterfejs w Go opisuje zachowanie, nie pochodzenie: typ implementuje interfejs automatycznie, gdy tylko posiada wymagany zestaw metod, bez deklaracji implements.
type Writer interface {
Write(p []byte) (n int, err error)
}
type UpperWriter struct {
w io.Writer
}
func (u UpperWriter) Write(p []byte) (int, error) {
return u.w.Write(bytes.ToUpper(p))
}
func main() {
var w Writer = UpperWriter{w: os.Stdout}
w.Write([]byte("hello\n"))
} type MyError struct{}
func (e *MyError) Error() string { return "boom" }
func mayFail(fail bool) *MyError {
if fail {
return &MyError{}
}
return nil
}
func do() error {
var err *MyError = mayFail(false)
return err // interfejs niosący (typ=*MyError, wartość=nil) - to NIE jest nil interfejsu
}
func main() {
if err := do(); err != nil {
fmt.Println("err != nil, mimo że logicznie nic się nie stało:", err)
}
} - Interfejsy w Go trzyma się małe: 1-3 metody (np. io.Reader, io.Writer, sort.Interface)
- Zasada „accept interfaces, return structs”: parametry przyjmują interfejsy, funkcje zwracają konkretne typy
- Interfejs deklaruje się tam, gdzie jest konsumowany, nie tam, gdzie typ jest zdefiniowany
| Aspekt | Go (implicit) | Java/C# (explicit) |
|---|---|---|
| Deklaracja zgodności | Automatyczna, przez zestaw metod | Jawna przez implements/: |
| Sprzężenie modułów | Niskie - konsument definiuje potrzebny interfejs | Wyższe - typ musi znać interfejs przy definicji |
| Nowy interfejs dla istniejącego typu | Nie wymaga zmian w typie | Wymaga edycji deklaracji typu |
- 1 Zdefiniuj interfejs po stronie konsumenta Np. w pakiecie, który wywołuje funkcję, nie w pakiecie z implementacją.
- 2 Ogranicz liczbę metod do minimum Mniejszy interfejs = łatwiejszy do zaimplementowania i przetestowania (mock).
- 3 Zwracaj konkretny typ z konstruktora Wywołujący dostaje pełny dostęp do API, interfejs stosuje się dopiero tam, gdzie potrzebny jest polimorfizm.
4 · defer, panic i recover
★ egzamindefer, panic i recover to mechanizm Go do zarządzania zasobami i nietypowymi, katastrofalnymi błędami - nie zamiennik zwykłej obsługi błędów przez wartości error.
func closeAll() error {
f1, err := os.Open("a.txt")
if err != nil {
return fmt.Errorf("opening a.txt: %w", err)
}
defer f1.Close()
f2, err := os.Open("b.txt")
if err != nil {
return fmt.Errorf("opening b.txt: %w", err)
}
defer f2.Close()
fmt.Println("praca z plikami")
return nil
// kolejność zamykania: najpierw f2.Close(), potem f1.Close() (LIFO)
} func process(items []int) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("process: recovered from panic: %v", r)
}
}()
for _, i := range items {
fmt.Println(100 / i) // panikuje przy i == 0 (dzielenie int przez zero)
}
return nil
} - panic: naruszenie niezmiennika, błąd inicjalizacji pakietu, index out of range wykryty przez runtime, dzielenie int przez zero
- error: każdy oczekiwany, przewidywalny scenariusz awarii (brak pliku, timeout sieci, nieprawidłowe dane wejściowe)
- recover ma sens głównie na granicy systemu (np. middleware HTTP), by nie ubić całego procesu przez błąd w jednym żądaniu
func recoverMiddleware(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 w handlerze: %v", rec)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
} 5 · Pakiet context: anulowanie i deadline'y
★ egzamincontext.Context niesie przez łańcuch wywołań sygnał anulowania, deadline i opcjonalnie wartości powiązane z pojedynczym żądaniem - to standardowy sposób na zatrzymanie pracy, gdy nikt już nie czeka na wynik.
func fetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("building request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
} func worker(ctx context.Context, jobs <-chan int) {
for {
select {
case <-ctx.Done():
fmt.Println("worker: zatrzymano,", ctx.Err())
return
case j, ok := <-jobs:
if !ok {
return
}
fmt.Println("przetwarzam:", j)
}
}
} | Konstruktor | Kiedy zamyka Done() |
|---|---|
| context.Background() | Nigdy - to korzeń drzewa kontekstów |
| context.WithCancel(parent) | Ręcznie, przy wywołaniu zwróconej funkcji cancel() |
| context.WithTimeout(parent, d) | Po czasie d albo ręcznie przez cancel() |
| context.WithDeadline(parent, t) | O konkretnym czasie t albo ręcznie przez cancel() |
- context.Background(): korzeń w main, testach, inicjalizacji
- context.TODO(): tymczasowy placeholder, gdy docelowy kontekst jeszcze nie jest przekazywany - sygnał do refaktoryzacji
- context.WithValue: tylko dla danych per-żądanie (trace ID, uwierzytelniony użytkownik), nigdy dla wymaganych argumentów funkcji
Dobre użycie WithValue
- ID żądania do logowania/tracingu
- Tożsamość uwierzytelnionego użytkownika z middleware
Złe użycie WithValue
- Przekazywanie wymaganych parametrów biznesowych (jawny argument jest lepszy)
- Przechowywanie połączenia z bazą danych lub loggera zamiast wstrzyknięcia zależności
6 · Pułapki współbieżności: wyścigi danych i wycieki gorutyn
★ egzaminKompilator Go nie chroni przed wyścigami danych ani wyciekami gorutyn - oba błędy są w pełni poprawne składniowo i ujawniają się dopiero w runtime, często niedeterministycznie.
var counter int
func incBroken() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // odczyt-modyfikacja-zapis bez ochrony: wyścig danych
}()
}
wg.Wait()
fmt.Println(counter) // niedeterministyczny wynik, zwykle < 1000
} func incSafe() {
var counter int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
fmt.Println(atomic.LoadInt64(&counter)) // zawsze dokładnie 1000
} func leaky() {
ch := make(chan int) // niebuforowany
go func() {
ch <- computeExpensiveValue() // blokuje się w nieskończoność, gdy nikt nie odbiera
}()
// funkcja wraca bez odbioru z ch -> gorutyna wisi w pamięci na zawsze
} func notLeaky(ctx context.Context) {
ch := make(chan int, 1) // buforowany: wysyłka się nie zablokuje
go func() {
select {
case ch <- computeExpensiveValue():
case <-ctx.Done():
}
}()
} - Zablokowana wysyłka/odbiór na kanale bez partnera po drugiej stronie
- Zapomniany wg.Done() w defer przy panice w gorutynie roboczej
- Nieskończona pętla bez ścieżki wyjścia (brak select na ctx.Done() lub kanale stop)
| Mechanizm | Kiedy użyć |
|---|---|
| sync.Mutex | Ochrona złożonego stanu (kilka pól, mapa, struktura) |
| sync/atomic | Pojedyncza liczba/wskaźnik, wysoka częstotliwość aktualizacji |
| channel | Przekazanie własności danych lub sygnalizacja zdarzenia między gorutynami |
7 · Generyki: parametry typu i constraints
★ egzaminOd Go 1.18 funkcje i typy mogą przyjmować parametry typu ograniczone przez constraints, eliminując powielanie kodu dla int, float64, string bez ucieczki do interface{} i rzutowań w runtime.
import "cmp"
func Sum[T cmp.Ordered](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
} type Numeric interface {
~int | ~int64 | ~float32 | ~float64
}
func Average[T Numeric](nums []T) float64 {
var sum T
for _, n := range nums {
sum += n
}
return float64(sum) / float64(len(nums))
} type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
n := len(s.items) - 1
v := s.items[n]
s.items = s.items[:n]
return v, true
} - slices.Sort, slices.Contains, maps.Keys - generyczne funkcje stdlib zamiast ręcznych pętli po typach
- Inferencja typu zwykle eliminuje potrzebę jawnego podania [T] przy wywołaniu
- Generyki nie zastępują interfejsów: interfejsy dają polimorfizm w runtime, generyki - reużywalność kodu w czasie kompilacji
8 · Moduły, semantic import versioning i statyczna kompilacja
Go modules (od 1.11, domyślne i jedyne wspierane od 1.16) zarządzają zależnościami i wersjonowaniem; kompilator generuje pojedynczy statyczny plik wykonywalny bez zależności od zewnętrznego runtime czy maszyny wirtualnej.
module github.com/tenzanlogic/example
go 1.23
require (
github.com/google/uuid v1.6.0
) // go.mod: module github.com/tenzanlogic/example/v2
import "github.com/tenzanlogic/example/v2/pkg/data" GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o app-linux-arm64 ./cmd/app - GOOS/GOARCH: docelowy system operacyjny/architektura bez potrzeby fizycznego dostępu do tej platformy
- CGO_ENABLED=0 trzeba ustawić jawnie dla gwarancji statycznego builda - nie jest to domyślne przy natywnym buildzie z dostępnym kompilatorem C
- Jeden binarny plik = prostszy deployment: brak instalacji runtime, brak zarządzania wersją interpretera na serwerze
Sprawdź się - testowanie to nauka
24 pytań w losowej kolejności. Twoje wyniki zapisują się lokalnie.