Przejdź do treści
← wszystkie przedmioty

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.

TEMATY EGZAMINACYJNE W TYM PRZEDMIOCIE
  • 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

★ egzamin

Go 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.

Termin kluczowy
Gorutyna - Funkcja uruchomiona słowem kluczowym `go`, wykonywana współbieżnie względem wywołującego; runtime Go planuje tysiące gorutyn na małą pulę wątków systemowych (model M:N, sterowany przez GOMAXPROCS).
Termin kluczowy
Kanał (channel) - Typowany potok komunikacyjny (`chan T`) do przekazywania wartości między gorutynami, tworzony przez `make(chan T)` (niebuforowany) lub `make(chan T, n)` (buforowany, pojemność n).
Filozofia CSP
Zasada Go: „Do not communicate by sharing memory; share memory by communicating.” Synchronizacja przez przekazywanie wartości kanałem, nie przez mutex chroniący współdzieloną zmienną, wszędzie tam, gdzie to naturalne.
Worker pool z kanałami i sync.WaitGroup go
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 z timeoutem zamiast blokowania na zawsze go
select {
case res := <-resultCh:
	fmt.Println("wynik:", res)
case <-time.After(2 * time.Second):
	fmt.Println("timeout: brak odpowiedzi w 2s")
}
Pułapka
Wysłanie wartości do zamkniętego kanału powoduje panic w runtime; zamykanie kanału, do którego wciąż ktoś wysyła, to błąd projektowy, nie kosmetyczne niedopatrzenie.
  • 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)
Mnemonik
Kanał jak taśma produkcyjna: nadawca pakuje towar, odbiorca zdejmuje z taśmy, close(ch) gasi taśmę - nic nowego już na nią nie kładziesz, ale to, co leży, można jeszcze zdjąć.
CechaGorutynaWątek systemowy (OS thread)
Rozmiar startowy stosu~2 KB, rośnie dynamiczniezwykle 1-8 MB, stały
Kto planujeScheduler Go (M:N na GOMAXPROCS wątków OS)Scheduler systemu operacyjnego
Koszt utworzeniaBardzo niski, miliony możliweWysoki, praktyczny limit - tysiące
  1. 1 Generator Gorutyna emitująca wartości do kanału wyjściowego i zamykająca go po zakończeniu pracy.
  2. 2 Etap przetwarzania Gorutyna czytająca z kanału wejściowego przez range, przetwarzająca wartość, wysyłająca dalej.
  3. 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

★ egzamin

Go 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.

Termin kluczowy
error - Wbudowany interfejs `type error interface { Error() string }`; każdy typ z metodą Error() string spełnia go niejawnie i może pełnić rolę błędu.
Sentinel error i własny typ błędu go
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
}
Odczytanie łańcucha błędów przez errors.Is/As go
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)
	}
}
Pułapka
fmt.Errorf("...: %v", err) gubi możliwość odnalezienia err przez errors.Is/As, bo nie tworzy metody Unwrap(); tylko %w buduje przeszukiwane drzewo błędów.

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
Konwencja komunikatu
Komunikat błędu w Go pisze się małą literą, bez kropki na końcu, bez zbędnego kontekstu, który doda wyżej wywołujący: fmt.Errorf("reading config: %w", err), nie "Error: Could not read config file."
Mnemonik
err != nil to jedyny „try/catch” w Go: sprawdzasz od razu po wywołaniu, nie na końcu bloku.
errors.Join scala wyniki wielu walidacji go
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

★ egzamin

Interfejs w Go opisuje zachowanie, nie pochodzenie: typ implementuje interfejs automatycznie, gdy tylko posiada wymagany zestaw metod, bez deklaracji implements.

Termin kluczowy
Structural typing - Kompilator sprawdza zgodność metod w czasie kompilacji, nie w runtime przez reflection; jeśli typ „chodzi jak kaczka”, jest kaczką dla danego interfejsu.
Niejawna implementacja io.Writer go
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"))
}
any i interface{}
Od Go 1.18 `any` to alias dla interface{}; oba oznaczają zbiór wszystkich typów, ale any jest preferowaną, czytelniejszą formą w nowym kodzie.
Pułapka
Zmienna typu error/interface może być != nil, mimo że przechowuje typowany nil wskaźnik: interfejs to para (typ, wartość), a para (*MyError, nil) różni się od (nil, nil).
Pułapka: typowany nil w interfejsie go
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
AspektGo (implicit)Java/C# (explicit)
Deklaracja zgodnościAutomatyczna, przez zestaw metodJawna przez implements/:
Sprzężenie modułówNiskie - konsument definiuje potrzebny interfejsWyższe - typ musi znać interfejs przy definicji
Nowy interfejs dla istniejącego typuNie wymaga zmian w typieWymaga edycji deklaracji typu
Mnemonik
Nie pytaj „czym jesteś”, pytaj „co potrafisz zrobić” - to sedno interfejsów w Go.
  1. 1 Zdefiniuj interfejs po stronie konsumenta Np. w pakiecie, który wywołuje funkcję, nie w pakiecie z implementacją.
  2. 2 Ogranicz liczbę metod do minimum Mniejszy interfejs = łatwiejszy do zaimplementowania i przetestowania (mock).
  3. 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

★ egzamin

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

Termin kluczowy
defer - Odracza wywołanie funkcji do momentu zwrotu z otaczającej funkcji; argumenty są ewaluowane natychmiast, samo wywołanie następuje później, w kolejności LIFO.
Kolejność LIFO i bezpieczne domykanie zasobów go
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)
}
Termin kluczowy
panic / recover - panic przerywa normalny przepływ i zaczyna odwijać stos, wykonując po drodze odroczone defer; recover, wywołany bezpośrednio w takim defer, zatrzymuje odwijanie i pozwala funkcji zwrócić się normalnie.
recover() jako granica pakietu: panic → error go
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
}
Pułapka
recover() poza funkcją wywołaną bezpośrednio przez defer nie działa; recover() w innej gorutynie niż ta, w której wystąpił panic, również nic nie przechwyci - każda gorutyna potrzebuje własnego recover.
  • 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
Nieobsłużony panic
Panic, którego żadna funkcja w stosie danej gorutyny nie przechwyci przez recover, kończy CAŁY proces (nie tylko gorutynę) z niezerowym kodem wyjścia i wypisuje stack trace.
Mnemonik
defer sprząta, panic krzyczy „coś jest fundamentalnie nie tak”, recover zatyka uszy tylko temu, kto stoi bezpośrednio obok.
Middleware HTTP odzyskujący się po panice w handlerze go
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

★ egzamin

context.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.

Termin kluczowy
context.Context - Interfejs z metodami Deadline(), Done() <-chan struct{}, Err() error, Value(key any) any; Done() zwraca kanał zamykany w momencie anulowania lub upłynięcia deadline'u.
Żądanie HTTP z timeoutem przez WithTimeout go
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)
}
Gorutyna reagująca na ctx.Done() go
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)
		}
	}
}
Konwencja API
ctx context.Context zawsze jako pierwszy parametr funkcji, nazwany dosłownie ctx; nigdy jako pole struktury długożyjącego obiektu.
Pułapka
Zapomnienie wywołania cancel() zwróconego przez WithCancel/WithTimeout/WithDeadline (nawet po sukcesie) zostawia zasoby kontekstu żywe do czasu GC; zawsze defer cancel() zaraz po utworzeniu.
KonstruktorKiedy 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
Mnemonik
Context to smycz: rodzic anuluje, dziecko czuje to natychmiast - ale nie odwrotnie.

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

★ egzamin

Kompilator 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.

Flaga -race
go test -race ./... (oraz go run -race, go build -race) instrumentuje dostęp do pamięci i wykrywa brak synchronizacji happens-before między gorutynami; koszt to zwykle 2-20x wolniejsze wykonanie i 5-10x większe zużycie pamięci, więc stosuje się ją w CI/testach, nie na produkcji.
Wyścig danych: niezsynchronizowany inkrement go
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
}
Naprawa przez sync/atomic go
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
}
Pułapka
go build bez -race skompiluje kod z wyścigiem danych bez ostrzeżenia (build tylko tworzy binarkę, nie uruchamia jej); dopiero uruchomiony później plik wykonywalny ściga się po cichu - brak widocznego objawu przy niskim obciążeniu usypia czujność aż do produkcji pod dużym ruchem.
Wyciek gorutyny na niebuforowanym kanale go
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
}
Naprawa: bufor lub wyjście przez ctx.Done() go
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)
MechanizmKiedy użyć
sync.MutexOchrona złożonego stanu (kilka pól, mapa, struktura)
sync/atomicPojedyncza liczba/wskaźnik, wysoka częstotliwość aktualizacji
channelPrzekazanie własności danych lub sygnalizacja zdarzenia między gorutynami
Mnemonik
Jeśli nie potrafisz odpowiedzieć „kto i kiedy odbierze z tego kanału”, właśnie napisałeś wyciek gorutyny.

7 · Generyki: parametry typu i constraints

★ egzamin

Od 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.

Termin kluczowy
Parametr typu - Deklarowany w nawiasach kwadratowych po nazwie funkcji/typu, np. func Sum[T cmp.Ordered](nums []T) T; T zostaje ustalony przy wywołaniu, najczęściej przez inferencję typu z argumentów.
Termin kluczowy
Constraint - Interfejs opisujący dozwolony zbiór typów konkretnych dla parametru typu; ~int dopuszcza int oraz każdy typ nazwany o bazowym typie int (underlying type).
Generyczna suma z użyciem stdlib cmp.Ordered go
import "cmp"

func Sum[T cmp.Ordered](nums []T) T {
	var total T
	for _, n := range nums {
		total += n
	}
	return total
}
Własny constraint dla typów liczbowych go
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))
}
Generyczna struktura: Stack[T] go
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
}
comparable
Predeklarowany constraint comparable dopuszcza typy wspierające ==/!=; wycinki (slices), mapy i funkcje NIE implementują comparable i nie mogą być parametrem typu ograniczonym przez comparable.
Pułapka
Nadużywanie generyków tam, gdzie wystarczy interfejs (jedna implementacja, brak potrzeby wielu konkretnych typów) dodaje złożoność czytania bez korzyści w wydajności czy bezpieczeństwie typów.
  • 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
Mnemonik
Interfejs pyta „co ten obiekt potrafi teraz”, generyk pyta „dla jakiego typu ten kod ma zadziałać, ustalone przy 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.

Termin kluczowy
go.mod / go.sum - go.mod deklaruje nazwę modułu, wymaganą wersję Go i zależności; go.sum przechowuje kryptograficzne sumy kontrolne pobranych wersji dla weryfikowalnej odtwarzalności buildu.
Przykładowy go.mod text
module github.com/tenzanlogic/example

go 1.23

require (
	github.com/google/uuid v1.6.0
)
Semantic import versioning
Od wersji major 2 wzwyż ścieżka modułu i importu musi zawierać sufiks /vN (np. github.com/tenzanlogic/example/v2), aby v1 i v2 mogły współistnieć w jednym drzewie zależności jako odrębne, niekolidujące pakiety.
Import modułu w wersji v2 go
// go.mod: module github.com/tenzanlogic/example/v2
import "github.com/tenzanlogic/example/v2/pkg/data"
Statyczny binary
CGO_ENABLED=0 trzeba ustawić jawnie, by zagwarantować w pełni statyczny, wolny od libc binarny plik - przy natywnym buildzie z obecnym w systemie kompilatorem C domyślne jest CGO_ENABLED=1, a pakiety stdlib takie jak net (rozwiązywanie DNS) czy os/user potrafią po cichu włączyć cgo i dynamicznie zlinkować libc, mimo że kod użytkownika nigdy nie pisze import "C"; CGO_ENABLED=0 jest domyślne tylko przy cross-compilation na inny GOOS/GOARCH niż host. Z jawnie wymuszonym CGO_ENABLED=0 binarka Go działa nawet w kontenerze FROM scratch, bez systemu operacyjnego.
Cross-compilation i minimalny obraz kontenera bash
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o app-linux-arm64 ./cmd/app
Pułapka
Włączenie cgo (CGO_ENABLED=1, np. przez rozwiązywanie DNS przez glibc lub sterowniki SQLite w C) przywraca dynamiczne zależności od libc i psuje przenośność binarki między dystrybucjami/kontenerami scratch.
  • 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
Mnemonik
Go nie pyta serwera „czy masz zainstalowany mój runtime” - przynosi go ze sobą w jednym pliku.
Fiszki - aktywne przypominanie

Sprawdź się - testowanie to nauka

24 pytań w losowej kolejności. Twoje wyniki zapisują się lokalnie.

Rozpocznij quiz
Zbudowane przez Tenzan Logic