Tech Handbook Null Yard

Go - czytanie kodu

Go jest małym językiem o silnych konwencjach i rozbudowanym toolchainie. Najwygodniej czytać projekt od go.mod i main(), a potem śledzić przepływ danych przez struktury, interfejsy, funkcje i błędy.

Materiał obejmuje Go 1.27.x, w tym generic methods wprowadzone w Go 1.27.

Powiązane tematy: API i integracje systemów, SQL i PostgreSQL dla developera, Testowanie oprogramowania oraz Docker.

Cel tego materiału: po przeczytaniu nie musisz umieć samodzielnie projektować dużych aplikacji w Go. Masz natomiast rozumieć, co robi kod, jak jest zbudowany, gdzie czego szukać i jak go uruchomić, skompilować oraz przetestować.

Materiał jest pisany pod współczesne Go 1.27.x i typowe projekty backendowe, narzędziowe oraz proste gry 2D.


Spis treści

  1. Jak myśleć o Go
  2. Instalacja i narzędzia
  3. Pierwszy program i anatomia pliku
  4. Składnia - najważniejsze reguły
  5. Zmienne, stałe i zero values
  6. Podstawowe typy danych
  7. String, byte i rune
  8. Tablice, slice i mapy
  9. Struct - podstawowy budulec danych
  10. Wskaźniki
  11. Instrukcje sterujące
  12. Funkcje
  13. Metody i receivery
  14. Interfejsy
  15. Generics
  16. Błędy - error
  17. defer, panic, recover
  18. Pakiety, moduły i importy
  19. Widoczność nazw
  20. Typowa struktura projektu
  21. go.mod, go.sum i zależności
  22. Uruchamianie i kompilowanie
  23. Cross-compilation
  24. Formatowanie i analiza kodu
  25. Testy
  26. Benchmarki, fuzzing i race detector
  27. Debugowanie
  28. Pliki, katalogi i system operacyjny
  29. JSON
  30. Czas i daty
  31. Logowanie
  32. context.Context
  33. Współbieżność: goroutines i channels
  34. Mutex, WaitGroup i atomiki
  35. HTTP w standardowej bibliotece
  36. HTML templates i pliki statyczne
  37. Embed - pakowanie plików do binarki
  38. Backend: routery i frameworki
  39. Bazy danych
  40. ORM, sqlc i migracje
  41. Redis i cache
  42. Konfiguracja aplikacji
  43. CLI i narzędzia
  44. Gry w Go
  45. Ebitengine - najpraktyczniejszy start z grami 2D
  46. Raylib-go i inne biblioteki growe
  47. CGO - kiedy Go korzysta z C
  48. Najczęstsze idiomy Go
  49. Rzeczy, które wyglądają dziwnie, ale są normalne
  50. Jak czytać obcy projekt Go
  51. Miniaturowy backend - przykład całości
  52. Miniaturowa gra - przykład struktury
  53. Ściąga poleceń
  54. Ściąga składni
  55. Co warto znać, a czego na razie nie trzeba
  56. Dalsza dokumentacja

1. Jak myśleć o Go

Go jest językiem:

  • kompilowanym,
  • statycznie typowanym,
  • garbage-collected,
  • zaprojektowanym z myślą o prostocie,
  • bardzo mocno związanym z programowaniem sieciowym, serwerowym i współbieżnym,
  • wyposażonym w bardzo dużą standardową bibliotekę.

Kod Go jest zwykle mniej „magiczny” niż kod JavaScriptu, C++ czy rozbudowanych frameworków Javy. W Go często świadomie rezygnuje się z abstrakcji, jeśli zwykła funkcja i kilka struktur wystarczają.

Typowy sposób myślenia w Go:

dane -> funkcja/metoda -> wynik + error

Bardzo często zobaczysz więc kod:

user, err := loadUser(id)
if err != nil {
    return err
}

To nie jest przypadek. Obsługa błędów w Go jest jawna.

Go ma kilka cech, które warto od razu zapamiętać:

  1. Nie ma klas w stylu Javy czy C++.
  2. struct, metody i interfejsy.
  3. Interfejsy implementuje się automatycznie - bez implements.
  4. Funkcje mogą zwracać kilka wartości.
  5. error jest zwykłą wartością.
  6. goroutine to bardzo lekka jednostka współbieżnego wykonania.
  7. channel służy do komunikacji między goroutines.
  8. Kompilator i narzędzia Go narzucają dużo porządku.
  9. gofmt praktycznie eliminuje dyskusje o stylu formatowania.
  10. Duża część backendu może być napisana bez frameworka - samym net/http.

2. Instalacja i narzędzia

Sprawdzenie wersji:

go version

Przykład:

go version go1.27.1 linux/amd64

Najważniejsze polecenie diagnostyczne:

go env

Pokazuje m.in.:

  • system docelowy,
  • architekturę,
  • ścieżki cache,
  • GOPATH,
  • GOROOT,
  • ustawienia modułów,
  • proxy modułów,
  • ustawienia CGO.

Wybrane wartości:

go env GOOS GOARCH GOPATH GOROOT GOPROXY CGO_ENABLED

Typowe GOOS:

linux
windows
darwin
freebsd

Typowe GOARCH:

amd64
arm64
386
arm

Debian

Jeśli zależy Ci na aktualnym Go, pakiet Debiana może być starszy niż oficjalne wydanie. W projekcie warto świadomie ustalić wersję toolchainu.

Po instalacji:

go version
go env

FreeBSD

Najczęściej:

pkg install go

Potem:

go version

Edytor

Do samego Go wystarczy dowolny edytor, ale bardzo pomaga language server:

go install golang.org/x/tools/gopls@latest

gopls odpowiada za:

  • podpowiadanie kodu,
  • przechodzenie do definicji,
  • wykrywanie błędów,
  • refaktoryzację,
  • informacje o typach.

3. Pierwszy program i anatomia pliku

Minimalny program:

package main

import "fmt"

func main() {
    fmt.Println("Hello, world!")
}

Rozbijmy go.

package main

Każdy plik .go należy do jakiegoś pakietu.

package main

oznacza, że ten kod jest częścią programu wykonywalnego.

import "fmt"

Importujemy pakiet standardowej biblioteki:

import "fmt"

fmt odpowiada za formatowanie i wypisywanie tekstu.

func main()

Program wykonywalny zaczyna pracę od:

func main()

Musi ona znajdować się w pakiecie main.

Uruchomienie:

go run .

Kompilacja:

go build

Po go build otrzymujesz binarkę.


4. Składnia - najważniejsze reguły

Bloki kodu

Go używa {}:

if x > 10 {
    fmt.Println("duże")
}

Styl nawiasów nie jest dowolny. To jest prawidłowe:

if x > 10 {
}

A to nie:

if x > 10
{
}

Średniki

Teoretycznie język posiada średniki, ale praktycznie ich nie piszesz:

x := 10
y := 20

Lexer wstawia je automatycznie.

Komentarze

Jedna linia:

// komentarz

Wiele linii:

/*
komentarz
wieloliniowy
*/

Komentarz dokumentujący eksportowaną funkcję zwykle zaczyna się od jej nazwy:

// LoadUser loads a user from storage.
func LoadUser() {}

Nazwy

Typowo:

userName
requestID
httpClient

Nie:

user_name
request_id

Go preferuje camelCase.


5. Zmienne, stałe i zero values

Pełna deklaracja

var age int

age automatycznie dostaje wartość zerową:

0

Można od razu przypisać:

var age int = 46

Kompilator potrafi wywnioskować typ:

var age = 46

Krótka deklaracja :=

Najczęściej spotykana forma wewnątrz funkcji:

age := 46
name := "Anna"

:= oznacza:

utwórz nową zmienną i wywnioskuj jej typ.

Późniejsze przypisanie używa już zwykłego =:

age = 47

Kilka wartości

name, age := "Anna", 30

Stałe

const MaxUsers = 100
const AppName = "Web Monitor"

Możliwe są grupy:

const (
    StatusNew    = "new"
    StatusActive = "active"
    StatusClosed = "closed"
)

Zero values

To bardzo ważna cecha Go.

Zmienne mają sensowną wartość początkową nawet bez jawnego przypisania.

Typ zero value
int 0
float64 0
bool false
string ""
pointer nil
slice nil
map nil
channel nil
function nil
interface nil

Przykład:

var enabled bool
fmt.Println(enabled)

wynik:

false

6. Podstawowe typy danych

Liczby całkowite

Najczęściej:

int
int64
uint
uint64

Istnieją też:

int8
int16
int32
uint8
uint16
uint32

int ma rozmiar zależny od architektury - w praktyce na współczesnych systemach 64-bitowych najczęściej 64 bity.

Liczby zmiennoprzecinkowe

float32
float64

Najczęściej używa się:

float64

Boolean

bool

Wartości:

true
false

Go nie konwertuje automatycznie liczby na bool.

Nie ma czegoś w rodzaju:

if 1 {
}

Aliasy znakowe

byte

to alias uint8.

rune

to alias int32, zwykle oznaczający kod Unicode.

Typy nazwane

Można stworzyć własny typ:

type UserID int64

Teraz UserID nie jest dokładnie tym samym typem co zwykły int64.

To pozwala kompilatorowi wyłapywać pomyłki.


7. String, byte i rune

String:

name := "Anna"

String w Go jest niezmienny.

Nie możesz zrobić:

name[0] = 'M'

UTF-8

Go bardzo mocno opiera się na UTF-8.

String jest ciągiem bajtów.

s := "żółw"

len(s) zwraca liczbę bajtów, a nie znaków Unicode.

Iterowanie:

for i, r := range s {
    fmt.Println(i, r)
}

r jest typu rune.

Konwersja do bajtów:

b := []byte(s)

Konwersja do rune:

r := []rune(s)

To ważne przy:

  • tekstach z polskimi znakami,
  • obcinaniu napisów,
  • liczeniu znaków,
  • analizie tekstu.

8. Tablice, slice i mapy

Tablica

var numbers [3]int

Ma dokładnie trzy elementy.

numbers := [3]int{10, 20, 30}

Rozmiar jest częścią typu.

[3]int i [4]int to dwa różne typy.

W praktyce częściej spotkasz slice.


Slice

numbers := []int{10, 20, 30}

To jeden z najważniejszych typów w Go.

Można myśleć o nim jako o dynamicznym widoku na tablicę.

Dodawanie:

numbers = append(numbers, 40)

Fragment:

part := numbers[1:3]

Element:

x := numbers[0]

Długość:

len(numbers)

Pojemność:

cap(numbers)

Tworzenie:

numbers := make([]int, 0, 100)

Znaczy:

  • długość 0,
  • miejsce zarezerwowane na około 100 elementów.

Ważne

Slice może współdzielić pamięć z innym slice.

a := []int{1, 2, 3, 4}
b := a[1:3]
b[0] = 99

Po zmianie b zmieni się również a.


Map

Mapa to kolekcja klucz → wartość.

ages := map[string]int{
    "Anna": 30,
    "Anna": 40,
}

Odczyt:

age := ages["Anna"]

Zapis:

ages["Jan"] = 30

Usuwanie:

delete(ages, "Jan")

Sprawdzenie, czy klucz istnieje:

age, ok := ages["Anna"]
if ok {
    fmt.Println(age)
}

To bardzo typowy idiom Go.

Tworzenie pustej mapy:

ages := make(map[string]int)

Uwaga:

var ages map[string]int

tworzy mapę nil.

Można z niej czytać, ale zapis spowoduje panic.


9. Struct - podstawowy budulec danych

Go nie ma klas w klasycznym znaczeniu.

Zamiast tego bardzo często używa się struktur:

type User struct {
    ID    int64
    Name  string
    Email string
}

Tworzenie:

u := User{
    ID:    1,
    Name:  "Anna",
    Email: "user@example.com",
}

Dostęp:

fmt.Println(u.Name)

Zmiana:

u.Name = "Anna K."

Zagnieżdżenie

type Address struct {
    City string
}

type User struct {
    Name    string
    Address Address
}

Embedding

Można osadzić typ bez nazwy pola:

type Timestamps struct {
    CreatedAt time.Time
    UpdatedAt time.Time
}

type User struct {
    ID int64
    Timestamps
}

Wtedy:

user.CreatedAt

zamiast:

user.Timestamps.CreatedAt

Embedding jest jednym ze sposobów kompozycji w Go.


10. Wskaźniki

Wskaźnik przechowuje adres wartości.

x := 10
p := &x

p ma typ:

*int

Odczyt wartości spod wskaźnika:

fmt.Println(*p)

Zmiana:

*p = 20

Teraz:

x == 20

Po co wskaźniki?

Najczęściej:

  1. aby funkcja lub metoda mogła zmienić obiekt,
  2. aby nie kopiować dużych struktur,
  3. aby nil mógł oznaczać brak wartości.

Przykład:

func rename(u *User) {
    u.Name = "Nowa nazwa"
}

Go automatycznie upraszcza wiele operacji na pointerach do structów, więc zwykle piszesz:

u.Name

a nie:

(*u).Name

nil

var u *User

u == nil.

Próba:

fmt.Println(u.Name)

spowoduje panic.


11. Instrukcje sterujące

if

if age >= 18 {
    fmt.Println("pełnoletni")
}

Nie ma nawiasów wokół warunku.

Możliwa jest inicjalizacja:

if err := doSomething(); err != nil {
    return err
}

err istnieje tylko w obrębie tego if.


else

if x > 0 {
    fmt.Println("plus")
} else if x < 0 {
    fmt.Println("minus")
} else {
    fmt.Println("zero")
}

switch

switch status {
case "new":
    fmt.Println("nowy")
case "done":
    fmt.Println("gotowy")
default:
    fmt.Println("nieznany")
}

Nie trzeba break.

Go automatycznie kończy dany case.

Istnieje fallthrough, ale jest używany rzadko.


for

Go ma tylko jedną konstrukcję pętli: for.

Klasycznie:

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

Jak while:

for running {
}

Nieskończona:

for {
}

Po kolekcji:

for i, value := range values {
    fmt.Println(i, value)
}

Jeśli indeks niepotrzebny:

for _, value := range values {
    fmt.Println(value)
}

_ oznacza:

wartość świadomie ignorowana.

W nowoczesnym Go możesz też zobaczyć:

for i := range 10 {
    fmt.Println(i)
}

co iteruje po liczbach 0..9.


12. Funkcje

Prosta funkcja:

func add(a int, b int) int {
    return a + b
}

Skrót:

func add(a, b int) int {
    return a + b
}

Kilka wartości zwracanych

Bardzo częste:

func loadUser(id int64) (User, error) {
    // ...
}

Wywołanie:

user, err := loadUser(10)

Nazwane wartości zwracane

Możliwe:

func split() (left int, right int) {
    left = 10
    right = 20
    return
}

Nie należy ich nadużywać, ale w kodzie występują.

Funkcja jako wartość

handler := func(name string) {
    fmt.Println(name)
}

handler("Anna")

Funkcja jako argument

func run(fn func()) {
    fn()
}

Funkcja jako wynik

func makeGreeter(prefix string) func(string) string {
    return func(name string) string {
        return prefix + name
    }
}

Variadic

func sum(values ...int) int {
    total := 0
    for _, v := range values {
        total += v
    }
    return total
}

Wywołanie:

sum(1, 2, 3, 4)

Slice można rozwinąć:

values := []int{1, 2, 3}
sum(values...)

13. Metody i receivery

Metoda to funkcja przypięta do typu.

type User struct {
    Name string
}

func (u User) Greeting() string {
    return "Cześć " + u.Name
}

(u User) to receiver.

Wywołanie:

user.Greeting()

Value receiver

func (u User) NameUpper() string

Metoda dostaje kopię wartości.

Pointer receiver

func (u *User) Rename(name string) {
    u.Name = name
}

Może zmodyfikować oryginalny obiekt.

W praktyce często typ ma większość metod z receiverem pointerowym:

func (s *Server) Start() error
func (s *Server) Stop() error

Czytając kod:

func (p *Promo) Validate() error

czytaj to jako:

metoda Validate działająca na obiekcie Promo.


14. Interfejsy

Interfejs opisuje zachowanie.

type Writer interface {
    Write([]byte) (int, error)
}

Każdy typ posiadający metodę:

Write([]byte) (int, error)

automatycznie implementuje Writer.

Nie ma:

implements Writer

To bardzo ważne dla czytania Go.

Przykład

type Storage interface {
    Save(User) error
    Load(int64) (User, error)
}

Potem:

type Service struct {
    storage Storage
}

Service nie musi wiedzieć, czy Storage jest:

  • PostgreSQL,
  • SQLite,
  • pamięcią RAM,
  • mockiem testowym.

Pusty interfejs

Dawniej często:

interface{}

Współcześnie preferowany alias:

any

Przykład:

var value any

any może przechowywać wartość dowolnego typu.

Type assertion

s, ok := value.(string)

Sprawdza, czy value zawiera string.

Type switch

switch v := value.(type) {
case string:
    fmt.Println("string", v)
case int:
    fmt.Println("int", v)
default:
    fmt.Println("coś innego")
}

15. Generics

Generics pozwalają pisać kod działający z wieloma typami.

Najprostszy przykład:

func First[T any](values []T) T {
    return values[0]
}

T jest parametrem typu.

Wywołanie:

x := First([]int{10, 20})
y := First([]string{"a", "b"})

Kompilator zwykle sam wywnioskuje typ.

Constraints

type Number interface {
    ~int | ~int64 | ~float64
}

func Add[T Number](a, b T) T {
    return a + b
}

Znak ~ oznacza w przybliżeniu:

również typy zdefiniowane na bazie danego typu.

Przykład:

type UserID int64

pasuje do ~int64.

Typ generyczny

type Box[T any] struct {
    Value T
}

Użycie:

b := Box[string]{Value: "hello"}

Generyczne metody

W Go 1.27 możesz spotkać również metody deklarujące własne parametry typu.

Nie musisz od razu biegle ich pisać. Ważne, żeby rozpoznać składnię:

N[Int intType](...)

Kwadratowe nawiasy przy nazwie funkcji/metody oznaczają parametry typów.


16. Błędy - error

To absolutnie centralny element Go.

Typowa funkcja:

func loadConfig() (Config, error)

Wywołanie:

cfg, err := loadConfig()
if err != nil {
    return err
}

Tworzenie błędu

errors.New("user not found")

lub:

fmt.Errorf("cannot load user %d", id)

Opakowanie błędu

Bardzo ważny idiom:

return fmt.Errorf("load config: %w", err)

%w zachowuje oryginalny błąd wewnątrz nowego.

Potem:

errors.Is(err, os.ErrNotExist)

albo:

var pathErr *os.PathError
if errors.As(err, &pathErr) {
}

Sentinel errors

Możesz zobaczyć:

var ErrNotFound = errors.New("not found")

Potem:

if errors.Is(err, ErrNotFound) {
}

Dlaczego wszędzie jest if err != nil?

Bo Go celowo nie opiera zwykłego sterowania błędami na wyjątkach.

Błąd jest jawny.

Dzięki temu patrząc na funkcję od razu widzisz miejsca, w których coś może pójść nie tak.


17. defer, panic, recover

defer

Odkłada wykonanie funkcji do momentu opuszczenia aktualnej funkcji.

Najczęściej do sprzątania zasobów:

f, err := os.Open("config.json")
if err != nil {
    return err
}
defer f.Close()

Typowy wzorzec:

  1. otwórz zasób,
  2. sprawdź błąd,
  3. natychmiast ustaw defer Close().

Inny przykład:

mu.Lock()
defer mu.Unlock()

panic

panic("fatal problem")

Przerywa normalny tok wykonywania.

panic nie jest normalnym sposobem obsługi błędów biznesowych.

Nie robimy:

if user == nil {
    panic("user missing")
}

jeśli brak usera jest przewidywalnym przypadkiem.

recover

Może przechwycić panic.

Najczęściej spotkasz go we frameworkach HTTP lub kodzie infrastrukturalnym.

defer func() {
    if r := recover(); r != nil {
        log.Println("panic:", r)
    }
}()

18. Pakiety, moduły i importy

To trzy pojęcia, których nie należy mieszać.

Plik

user.go

jest po prostu plikiem źródłowym.

Package

Wszystkie pliki .go w jednym katalogu zazwyczaj należą do jednego pakietu:

package users

Mogą odwoływać się do swoich funkcji i typów bez importowania siebie nawzajem.

Module

Moduł definiuje go.mod.

Przykład:

module github.com/user/web-monitor

go 1.27

Moduł może zawierać wiele pakietów.

Przykład

web-monitor/
├── go.mod
├── cmd/
│   └── web-monitor/
│       └── main.go
├── internal/
│   ├── scanner/
│   └── validator/
└── web/

Moduł:

github.com/user/web-monitor

Pakiety:

github.com/user/web-monitor/internal/scanner
github.com/user/web-monitor/internal/validator

19. Widoczność nazw

Go ma wyjątkowo prostą zasadę.

Nazwa zaczynająca się wielką literą jest eksportowana:

func LoadUser()
type User struct{}
const MaxUsers = 100

Nazwa małą literą jest prywatna dla pakietu:

func loadUser()
type cacheEntry struct{}

Czyli:

http.Server

Server jest publiczny.

Natomiast jakiś:

http.someInternalThing

nie byłby dostępny poza pakietem.

Ta reguła zastępuje słowa typu:

public
private
protected

20. Typowa struktura projektu

Go nie wymusza jednej struktury projektu, ale istnieją popularne wzorce.

Mały projekt:

myapp/
├── go.mod
├── go.sum
├── main.go
├── server.go
├── storage.go
└── server_test.go

Większy projekt:

myapp/
├── cmd/
│   └── myapp/
│       └── main.go
├── internal/
│   ├── config/
│   ├── httpserver/
│   ├── service/
│   └── storage/
├── migrations/
├── web/
│   ├── static/
│   └── templates/
├── go.mod
└── go.sum

cmd/

Często zawiera programy wykonywalne.

cmd/server/main.go
cmd/worker/main.go

Jeden moduł może budować kilka binarek.

internal/

Specjalny mechanizm Go.

Pakiety wewnątrz internal mają ograniczoną możliwość importowania spoza odpowiedniego drzewa projektu.

To prawdziwa ochrona na poziomie toolchainu, a nie tylko konwencja.

pkg/

Spotykany w wielu repozytoriach, ale nie jest wymagany przez Go.

Zwykle oznacza kod przeznaczony do użycia przez inne projekty.

Nie należy tworzyć pkg/ automatycznie tylko dlatego, że ktoś tak robi.


21. go.mod, go.sum i zależności

Nowy moduł:

go mod init github.com/example/myapp

Powstaje:

go.mod

Przykład:

module github.com/example/myapp

go 1.27

require github.com/go-chi/chi/v5 v5.x.x

Dodanie biblioteki

go get github.com/go-chi/chi/v5

Porządkowanie zależności

go mod tidy

To bardzo ważne polecenie.

Usuwa niepotrzebne zależności i dopisuje brakujące.

go.sum

Zawiera sumy kryptograficzne modułów.

Nie jest listą zależności w sensie package-lock.json, choć pełni pokrewną rolę w zapewnianiu powtarzalności i weryfikacji pobranych modułów.

Normalnie commitujesz zarówno:

go.mod
go.sum

Lista modułów

go list -m all

Dlaczego dana zależność istnieje?

go mod why github.com/jackc/pgx/v5

Graf zależności

go mod graph

22. Uruchamianie i kompilowanie

Uruchomienie projektu

go run .

Go kompiluje program tymczasowo i go uruchamia.

Konkretny plik

go run main.go

Ale przy większych projektach lepiej uruchamiać pakiet:

go run .

lub:

go run ./cmd/server

Kompilacja

go build

Nazwa binarki

go build -o web-monitor

Budowa konkretnego programu

go build -o bin/web-monitor ./cmd/web-monitor

Wszystkie pakiety

go build ./...

./... oznacza:

ten pakiet i wszystkie podpakiety.

Instalacja programu

go install ./cmd/web-monitor

Binarka trafia zwykle do katalogu binariów Go.

Sprawdź:

go env GOPATH

Typowo:

~/go/bin

23. Cross-compilation

Jedna z mocnych stron Go.

Na Linuksie możesz często zbudować binarkę dla innego systemu bez specjalnego toolchainu.

Windows amd64:

GOOS=windows GOARCH=amd64 go build -o app.exe

Linux ARM64:

GOOS=linux GOARCH=arm64 go build -o app-arm64

FreeBSD amd64:

GOOS=freebsd GOARCH=amd64 go build -o app-freebsd

Lista wspieranych kombinacji:

go tool dist list

Ważne: CGO

Cross-compilation jest najprostsze dla czystego Go.

Jeżeli projekt korzysta z bibliotek C przez CGO, sytuacja staje się trudniejsza i może wymagać odpowiedniego kompilatora C dla systemu docelowego.


24. Formatowanie i analiza kodu

gofmt

gofmt -w .

W praktyce częściej:

go fmt ./...

Go ma jeden dominujący styl formatowania.

go vet

go vet ./...

Wyszukuje podejrzane konstrukcje, których sam kompilator nie musi uznać za błąd.

staticcheck

Popularne dodatkowe narzędzie:

go install honnef.co/go/tools/cmd/staticcheck@latest

Potem:

staticcheck ./...

Praktyczny zestaw

Przed commitem:

go fmt ./...
go vet ./...
go test ./...

W większym projekcie dodatkowo:

staticcheck ./...

25. Testy

Go ma testy w standardowej bibliotece.

Plik testowy kończy się:

_test.go

Przykład:

package calc

import "testing"

func TestAdd(t *testing.T) {
    got := Add(2, 3)
    want := 5

    if got != want {
        t.Fatalf("got %d, want %d", got, want)
    }
}

Uruchomienie:

go test

Cały moduł:

go test ./...

Szczegółowo:

go test -v ./...

Jeden test

go test -run TestAdd

Table-driven tests

Bardzo charakterystyczny styl Go:

func TestAdd(t *testing.T) {
    tests := []struct {
        name string
        a    int
        b    int
        want int
    }{
        {"positive", 2, 3, 5},
        {"zero", 0, 0, 0},
        {"negative", -2, 1, -1},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Fatalf("got %d, want %d", got, tt.want)
            }
        })
    }
}

Jeśli zobaczysz tt, to często oznacza po prostu:

test table entry

Test HTTP

Standardowa biblioteka ma:

net/http/httptest

Przykład:

req := httptest.NewRequest(http.MethodGet, "/health", nil)
w := httptest.NewRecorder()

handler(w, req)

if w.Code != http.StatusOK {
    t.Fatalf("unexpected status: %d", w.Code)
}

26. Benchmarki, fuzzing i race detector

Benchmark

func BenchmarkParser(b *testing.B) {
    for b.Loop() {
        Parse(data)
    }
}

Uruchomienie:

go test -bench=. ./...

Starszy kod może używać:

for i := 0; i < b.N; i++ {
}

Coverage

go test -cover ./...

Raport do pliku:

go test -coverprofile=coverage.out ./...

HTML:

go tool cover -html=coverage.out

Race detector

Bardzo ważny przy goroutines:

go test -race ./...

Wykrywa wyścigi dostępu do pamięci.

Fuzzing

Go ma fuzzing wbudowany w testing.

Przykład konstrukcji:

func FuzzParse(f *testing.F) {
    f.Add("hello")

    f.Fuzz(func(t *testing.T, input string) {
        _ = Parse(input)
    })
}

Uruchomienie:

go test -fuzz=FuzzParse

27. Debugowanie

Najpopularniejszy debugger dla Go:

Delve

Instalacja:

go install github.com/go-delve/delve/cmd/dlv@latest

Uruchomienie:

dlv debug

Test:

dlv test ./internal/service

W VS Code debugger Go zwykle korzysta właśnie z Delve.

Najprostszy debug

Go bardzo często debugguje się również przez:

fmt.Printf("user=%+v\n", user)

%+v pokazuje strukturę razem z nazwami pól.

Jeszcze czytelniej:

fmt.Printf("user=%#v\n", user)

28. Pliki, katalogi i system operacyjny

Najważniejsze pakiety:

os
io
io/fs
path/filepath
bufio

Odczyt całego pliku

data, err := os.ReadFile("config.json")
if err != nil {
    return err
}

data ma typ:

[]byte

Zapis

err := os.WriteFile("output.txt", []byte("hello"), 0644)

Otwieranie pliku

f, err := os.Open("data.txt")
if err != nil {
    return err
}
defer f.Close()

Tworzenie katalogu

os.MkdirAll("data/cache", 0755)

Ścieżki

path := filepath.Join("data", "cache", "file.json")

Lepiej niż ręczne:

"data/cache/file.json"

jeśli kod ma działać na wielu systemach.

Zmienne środowiskowe

value := os.Getenv("DATABASE_URL")

Bezpieczniejsza kontrola istnienia:

value, ok := os.LookupEnv("DATABASE_URL")

29. JSON

Pakiet:

encoding/json

Struktura:

type User struct {
    ID   int64  `json:"id"`
    Name string `json:"name"`
}

Fragmenty:

`json:"id"`

to struct tags.

Mówią bibliotece JSON, jak ma nazywać pole.

Kodowanie

u := User{ID: 1, Name: "Anna"}

data, err := json.Marshal(u)

Wynik:

{"id":1,"name":"Anna"}

Dekodowanie

var u User
err := json.Unmarshal(data, &u)

Zwróć uwagę na:

&u

Biblioteka musi dostać pointer, bo ma zmodyfikować strukturę.

HTTP

Często:

json.NewEncoder(w).Encode(data)

oraz:

json.NewDecoder(r.Body).Decode(&input)

omitempty

Email string `json:"email,omitempty"`

Jeśli pole jest puste, może zostać pominięte w JSON.

Ignorowanie pola

Password string `json:"-"`

30. Czas i daty

Pakiet:

time

Aktualny czas:

now := time.Now()

Dodawanie:

later := now.Add(10 * time.Minute)

Timeout:

5 * time.Second

Sen:

time.Sleep(time.Second)

Parsowanie:

t, err := time.Parse("2006-01-02", "2026-09-19")

Dziwny layout:

2006-01-02 15:04:05

jest charakterystyczny dla Go.

To wzorcowa data referencyjna używana do formatowania czasu.

Formatowanie:

s := t.Format("2006-01-02")

Ticker:

ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop()

Potem często:

for range ticker.C {
    check()
}

To konstrukcja bardzo przydatna np. w Promoguardzie.


31. Logowanie

Najprostsze:

log.Println("server started")

Współczesne Go posiada standardowy logger strukturalny:

log/slog

Przykład:

slog.Info("promotion checked",
    "url", promo.URL,
    "status", promo.Status,
)

Błąd:

slog.Error("cannot connect to database", "error", err)

Log strukturalny jest łatwiejszy do późniejszego przetwarzania niż ręczne składanie stringów.

Możliwe formaty:

  • tekst,
  • JSON.

W większych aplikacjach spotkasz też:

  • zerolog,
  • zap.

Dla większości nowych projektów warto najpierw sprawdzić, czy slog nie wystarcza.


32. context.Context

To jedna z rzeczy, która na początku wygląda dziwnie, a jest wszędzie w backendzie.

Przykład:

func LoadUser(ctx context.Context, id int64) (User, error)

context.Context służy przede wszystkim do przenoszenia:

  • anulowania operacji,
  • deadline,
  • timeoutu,
  • niewielkich danych związanych z requestem.

Request HTTP

Każdy request ma context:

ctx := r.Context()

Jeśli klient zerwie połączenie, context może zostać anulowany.

Baza danych może wtedy również przerwać zapytanie.

Timeout

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

Potem:

result, err := service.Load(ctx)

Ważne zasady

Context zazwyczaj:

  • jest pierwszym argumentem,
  • nazywa się ctx,
  • nie przechowuje się go w structach bez dobrego powodu,
  • przekazuje się w dół stosu wywołań.

Jeśli widzisz:

func (r *Repository) Find(ctx context.Context, id int64)

to jest całkowicie typowy kod Go.


33. Współbieżność: goroutines i channels

Goroutine

Zwykłe wywołanie:

work()

Uruchomienie współbieżne:

go work()

To wszystko.

Przykład:

go checkPromotions()
go sendReports()

Program może mieć tysiące goroutines.

Funkcja anonimowa

Bardzo częste:

go func() {
    doSomething()
}()

Końcowe () oznacza natychmiastowe wywołanie funkcji anonimowej.

Channel

Tworzenie:

ch := make(chan string)

Wysłanie:

ch <- "hello"

Odbiór:

msg := <-ch

Buffered channel

ch := make(chan string, 10)

Może tymczasowo przechować 10 wartości bez blokowania nadawcy.

Zamknięcie

close(ch)

Odbiorca może zrobić:

for msg := range ch {
    fmt.Println(msg)
}

Pętla skończy się po zamknięciu kanału i opróżnieniu bufora.

select

Działa podobnie do switch, ale dla operacji kanałowych.

select {
case msg := <-messages:
    fmt.Println(msg)
case <-ctx.Done():
    return ctx.Err()
}

To jeden z najważniejszych wzorców współbieżności w Go.

Timeout kanałowy

select {
case result := <-ch:
    fmt.Println(result)
case <-time.After(2 * time.Second):
    fmt.Println("timeout")
}

34. Mutex, WaitGroup i atomiki

Nie każdą współbieżność robi się kanałami.

Mutex

var mu sync.Mutex

Sekcja krytyczna:

mu.Lock()
defer mu.Unlock()

counter++

Chroni współdzieloną pamięć przed jednoczesną modyfikacją.

RWMutex

sync.RWMutex

Ma osobne blokady do:

  • odczytu,
  • zapisu.
mu.RLock()
mu.RUnlock()

oraz:

mu.Lock()
mu.Unlock()

WaitGroup

Czeka na zakończenie grupy goroutines.

Klasyczny kod:

var wg sync.WaitGroup

for _, job := range jobs {
    wg.Add(1)

    go func(job Job) {
        defer wg.Done()
        process(job)
    }(job)
}

wg.Wait()

W nowszym Go możesz też spotkać wygodniejsze API WaitGroup.Go, zależnie od wersji kodu/toolchainu.

Atomiki

Pakiet:

sync/atomic

Do prostych liczników i flag bez pełnego mutexa.

Nie używaj atomików jako pierwszego wyboru, jeśli nie rozumiesz dobrze modelu pamięci. Przy czytaniu kodu wystarczy wiedzieć, że służą do bardzo lekkiej synchronizacji pojedynczych wartości.


35. HTTP w standardowej bibliotece

Pakiet:

net/http

Go potrafi postawić pełnoprawny serwer HTTP bez żadnego frameworka.

Minimalny serwer:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello")
    })

    http.ListenAndServe(":8080", nil)
}

Najważniejsze typy:

http.Request
http.ResponseWriter
http.Handler
http.HandlerFunc
http.Server
http.ServeMux

Handler

Kluczowy interfejs:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Jeśli coś ma metodę ServeHTTP, może być handlerem HTTP.

HandlerFunc

Funkcja:

func hello(w http.ResponseWriter, r *http.Request) {
}

może działać jako handler.

Współczesny ServeMux

Nowoczesne Go potrafi routować także po metodzie i parametrach ścieżki.

Przykładowy styl:

mux := http.NewServeMux()

mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)

Odczyt parametru:

id := r.PathValue("id")

Dla prostych aplikacji może to całkowicie wystarczyć bez zewnętrznego routera.

Status

w.WriteHeader(http.StatusNotFound)

Lepiej używać nazw:

http.StatusOK
http.StatusCreated
http.StatusBadRequest
http.StatusNotFound
http.StatusInternalServerError

niż magicznych liczb:

200
201
400
404
500
w.Header().Set("Content-Type", "application/json")

Odpowiedź JSON

func health(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{
        "status": "ok",
    })
}

Serwer z konfiguracją

Zamiast:

http.ListenAndServe(":8080", mux)

w poważniejszym kodzie zobaczysz:

server := &http.Server{
    Addr:              ":8080",
    Handler:           mux,
    ReadHeaderTimeout: 5 * time.Second,
}

err := server.ListenAndServe()

To daje większą kontrolę.


36. HTML templates i pliki statyczne

Pakiet:

html/template

jest przeznaczony do generowania HTML i automatycznie stosuje odpowiednie escaping kontekstowy.

Szablon

<h1>{{.Title}}</h1>
<p>{{.Text}}</p>

Kod:

tmpl, err := template.ParseFiles("templates/index.html")
if err != nil {
    return err
}

Render:

data := struct {
    Title string
    Text  string
}{
    Title: "Example Site",
    Text:  "Nowe opowiadanie",
}

err = tmpl.Execute(w, data)

Pliki statyczne

fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))

Typowa struktura:

web/
├── static/
│   ├── css/
│   ├── js/
│   └── img/
└── templates/
    ├── layout.html
    └── index.html

Dla strony generowanej po stronie serwera Go + html/template może całkowicie zastąpić duży frontendowy framework.


37. Embed - pakowanie plików do binarki

Pakiet:

embed

pozwala wbudować pliki do programu podczas kompilacji.

Przykład:

import "embed"

//go:embed web/templates/*.html
var templatesFS embed.FS

Po kompilacji pliki znajdują się w binarce.

Zaleta:

app

może być pojedynczym plikiem zawierającym:

  • backend,
  • HTML,
  • CSS,
  • JavaScript,
  • inne assety.

To świetnie pasuje do małych projektów wdrażanych jako jedna binarka lub jeden kontener.

Można osadzić katalog:

//go:embed web/static/*
var staticFS embed.FS

38. Backend: routery i frameworki

W Go warto najpierw znać net/http. Dopiero później framework.

1. Standardowe net/http

Dobre dla:

  • małych stron,
  • API,
  • prostych usług,
  • narzędzi wewnętrznych,
  • projektów, gdzie chcesz minimalnych zależności.

Zalety:

  • zero zewnętrznych zależności,
  • bardzo stabilne API,
  • świetna kompatybilność z ekosystemem.

2. Chi

Import:

github.com/go-chi/chi/v5

Chi to lekki router bardzo blisko standardowego net/http.

Przykład:

r := chi.NewRouter()

r.Get("/users/{id}", getUser)
r.Post("/users", createUser)

Dobre, gdy:

  • net/http jest prawie wystarczające,
  • potrzebujesz wygodnego routingu,
  • chcesz zostać blisko idiomatycznego Go.

Dla wielu zwykłych backendów to bardzo rozsądny wybór.

3. Gin

Import:

github.com/gin-gonic/gin

Framework mocniej opakowujący HTTP.

Przykładowy styl:

r := gin.Default()

r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id")
    c.JSON(200, gin.H{"id": id})
})

Daje dużo wygody i dużą społeczność.

4. Echo

Import:

github.com/labstack/echo/v4

Przykład:

e := echo.New()

e.GET("/", func(c echo.Context) error {
    return c.String(http.StatusOK, "Hello")
})

Echo oferuje router, middleware, binding danych i wygodne API do odpowiedzi.

5. Fiber

Aktualna główna linia to Fiber v3:

github.com/gofiber/fiber/v3

Fiber jest inspirowany Express.js i działa na fasthttp, a nie na standardowym net/http.

Kod wygląda bardziej frameworkowo:

app := fiber.New()

app.Get("/", func(c fiber.Ctx) error {
    return c.SendString("Hello")
})

To może być przyjemne dla osób przychodzących z JavaScript/Node, ale brak bezpośredniej zgodności ze standardowym net/http jest ważną różnicą architektoniczną.

Jak to ustawić mentalnie

Od najmniejszej warstwy do największej:

net/http
   ↓
chi
   ↓
Gin / Echo
   ↓
Fiber - osobniejszy ekosystem oparty o fasthttp

Nie oznacza to, że „niżej = gorzej” albo „wyżej = lepiej”. To po prostu inna ilość abstrakcji.


39. Bazy danych

Standardowa biblioteka ma:

database/sql

To ogólny interfejs do relacyjnych baz danych.

Potrzebujesz jeszcze sterownika.

Dla PostgreSQL bardzo popularny jest:

pgx

czyli:

github.com/jackc/pgx/v5

Natywne pgx

Przykładowy styl:

conn, err := pgx.Connect(ctx, databaseURL)
if err != nil {
    return err
}
defer conn.Close(ctx)

Query:

var name string
err := conn.QueryRow(ctx,
    "SELECT name FROM users WHERE id=$1",
    id,
).Scan(&name)

Pool

W serwerze częściej spotkasz pulę połączeń:

pgxpool.Pool

niż pojedynczy pgx.Conn.

database/sql

Typowy kod:

db, err := sql.Open("driver", dsn)

Potem:

row := db.QueryRowContext(ctx,
    "SELECT name FROM users WHERE id = ?",
    id,
)

Konkretny placeholder zależy od bazy/drivera. PostgreSQL zwykle używa:

$1
$2
$3

Transakcja

tx, err := db.BeginTx(ctx, nil)
if err != nil {
    return err
}
defer tx.Rollback()

// operacje

return tx.Commit()

Rollback() po udanym Commit() nic złego nie zrobi, a defer chroni przed zapomnieniem rollbacku po błędzie.


40. ORM, sqlc i migracje

GORM

Popularny ORM:

gorm.io/gorm

Pozwala pracować na strukturach zamiast pisać cały SQL ręcznie.

Przykładowy styl:

db.First(&user, id)

Zaleta:

  • szybkie CRUD.

Wada:

  • większa warstwa abstrakcji,
  • trudniej czasem zobaczyć faktyczny SQL.

sqlc

Inne podejście:

Ty piszesz SQL, a sqlc generuje typowany kod Go.

Przykład SQL:

-- name: GetUser :one
SELECT id, name, email
FROM users
WHERE id = $1;

sqlc może wygenerować metodę podobną do:

func (q *Queries) GetUser(ctx context.Context, id int64) (User, error)

To bardzo ciekawy środek między:

  • ręcznym SQL,
  • pełnym ORM.

Migracje

Popularne narzędzia:

  • golang-migrate/migrate,
  • goose,
  • migracje konkretnego ORM.

Pliki często wyglądają tak:

0001_create_users.up.sql
0001_create_users.down.sql

41. Redis i cache

Popularny klient:

github.com/redis/go-redis/v9

Przykładowy styl:

rdb := redis.NewClient(&redis.Options{
    Addr: "localhost:6379",
})

Zapis:

err := rdb.Set(ctx, "key", "value", time.Hour).Err()

Odczyt:

value, err := rdb.Get(ctx, "key").Result()

Redis spotkasz jako:

  • cache,
  • session storage,
  • lock,
  • kolejkę lub element systemu kolejkowego,
  • licznik,
  • magazyn krótkotrwałych danych.

Do małego projektu nie dodawaj Redis tylko dlatego, że „backend powinien mieć Redis”. Jeśli pamięć procesu lub PostgreSQL wystarczają, dodatkowa usługa tylko komplikuje system.


42. Konfiguracja aplikacji

Najprostszy sposób:

os.Getenv("DATABASE_URL")

Struktura:

type Config struct {
    ListenAddr  string
    DatabaseURL string
    LogLevel    string
}

Funkcja:

func LoadConfig() (Config, error) {
    // ...
}

.env

Biblioteka:

github.com/joho/godotenv

pomaga ładować lokalny .env.

Na produkcji często lepiej używać normalnych zmiennych środowiskowych dostarczanych przez:

  • systemd,
  • Docker,
  • Compose,
  • Kubernetes,
  • platformę hostingową.

Viper

Popularna większa biblioteka konfiguracyjna:

github.com/spf13/viper

Potrafi obsługiwać m.in.:

  • env,
  • YAML,
  • JSON,
  • TOML,
  • wartości domyślne.

Do małego projektu może być przesadą.


43. CLI i narzędzia

Prosty CLI można napisać standardowym:

flag

Przykład:

port := flag.Int("port", 8080, "listen port")
flag.Parse()

Uruchomienie:

./app -port 9000

Cobra

Duże i popularne narzędzie:

github.com/spf13/cobra

Nadaje się do programów typu:

web-monitor scan
web-monitor report
web-monitor config show

Daje:

  • komendy,
  • podkomendy,
  • flagi,
  • help,
  • completion.

Często występuje razem z Viperem.

Do prostego narzędzia jedna komenda + kilka flag → flag może być całkowicie wystarczający.


44. Gry w Go

Go nie jest pierwszym językiem kojarzonym z AAA, ale świetnie nadaje się do:

  • prostych gier 2D,
  • symulacji,
  • roguelike,
  • gier strategicznych,
  • prototypów,
  • serwerów gier,
  • narzędzi do generowania danych,
  • gier webowych przez WebAssembly.

Najbardziej praktyczne biblioteki do poznania:

  1. Ebitengine - czysty Go, bardzo dobry do 2D.
  2. raylib-go - binding Go do raylib, prosty i przyjemny API.
  3. SDL bindings - niższy poziom, więcej kontroli, więcej konfiguracji.
  4. Pixel - historycznie popularna biblioteka 2D; warto rozpoznawać w starszym kodzie, ale do nowego projektu lepiej najpierw sprawdzić aktywniej rozwijane opcje.

Jeśli celem jest „chcę zrobić małą grę i zrozumieć kod”, Ebitengine jest bardzo dobrym punktem startowym.


45. Ebitengine - najpraktyczniejszy start z grami 2D

Pakiet:

github.com/hajimehoshi/ebiten/v2

Rdzeń gry wygląda zwykle mniej więcej tak:

type Game struct {
    playerX float64
    playerY float64
}

Następnie implementuje się metody wymagane przez engine.

Update

Logika gry:

func (g *Game) Update() error {
    if ebiten.IsKeyPressed(ebiten.KeyArrowRight) {
        g.playerX++
    }
    return nil
}

Draw

Renderowanie:

func (g *Game) Draw(screen *ebiten.Image) {
    // rysowanie
}

Layout

Rozmiar logicznego ekranu:

func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
    return 1280, 720
}

Uruchomienie

func main() {
    ebiten.SetWindowSize(1280, 720)
    ebiten.SetWindowTitle("My Game")

    game := &Game{}

    if err := ebiten.RunGame(game); err != nil {
        log.Fatal(err)
    }
}

Mentalny model:

RunGame
   ↓
Update
   ↓
Draw
   ↓
Update
   ↓
Draw
   ↓
...

Co trzyma się w Game?

Na przykład:

type Game struct {
    player Player
    enemies []Enemy
    score int
    state GameState
}

Typowa organizacja:

game/
├── main.go
├── game.go
├── player.go
├── enemy.go
├── world.go
└── assets/

Ebitengine obsługuje m.in.:

  • rysowanie 2D,
  • input,
  • audio,
  • obrazy,
  • shadery,
  • WebAssembly,
  • wiele platform desktopowych i mobilnych.

46. Raylib-go i inne biblioteki growe

raylib-go

Raylib jest biblioteką C zaprojektowaną do prostego programowania gier. raylib-go daje binding dla Go.

Kod ma zwykle bardzo klasyczny game loop:

rl.InitWindow(800, 450, "game")
defer rl.CloseWindow()

for !rl.WindowShouldClose() {
    rl.BeginDrawing()

    rl.ClearBackground(rl.RayWhite)
    rl.DrawText("Hello", 20, 20, 20, rl.Black)

    rl.EndDrawing()
}

To może być bardzo czytelne dla kogoś, kto chce zobaczyć „co komputer robi w każdej klatce”.

W porównaniu z Ebitengine raylib-go może wprowadzać zależność od natywnej biblioteki/C toolchainu, zależnie od sposobu budowy i platformy.

SDL

SDL daje niższy poziom:

  • okna,
  • input,
  • audio,
  • rendering,
  • urządzenia.

W Go korzysta się z bindingów.

Dobre, jeśli chcesz więcej kontroli, ale do zwykłej zabawy 2D Ebitengine/raylib są wygodniejsze.

Pixel

W starszych tutorialach Go możesz trafić na:

github.com/faiface/pixel

Biblioteka jest ciekawa historycznie i ma czytelne API 2D, ale aktywność projektu jest znacznie mniejsza niż w nowszych, żywiej rozwijanych alternatywach. Rozpoznawaj ją, ale nie musi być pierwszym wyborem do nowego projektu.


47. CGO - kiedy Go korzysta z C

Go może korzystać z kodu C przez mechanizm:

CGO

Sprawdzenie:

go env CGO_ENABLED

Czysty Go jest prosty do budowania i cross-compilowania.

CGO może wprowadzać:

  • zależność od kompilatora C,
  • natywne biblioteki,
  • trudniejszy deployment,
  • trudniejszą cross-kompilację.

Przy czytaniu zależności warto wiedzieć, czy biblioteka jest:

pure Go

czy opiera się o binding do biblioteki C.

Ebitengine we współczesnych wydaniach ma bardzo mocne wsparcie pure-Go na desktopie. Raylib-go jest bindingiem do raylib.


48. Najczęstsze idiomy Go

if err != nil

value, err := load()
if err != nil {
    return err
}

Czytaj:

spróbuj operacji; jeśli nie wyszła, przerwij i zwróć błąd.


Ignorowanie wartości

value, _ := strconv.Atoi(input)

_ = nie interesuje mnie ta wartość.

Uwaga: ignorowanie błędów często jest podejrzane.


Compile-time interface check

Możesz zobaczyć:

var _ io.Writer = (*MyWriter)(nil)

To nie jest sensowna wartość biznesowa.

To sztuczka kompilacyjna:

sprawdź podczas kompilacji, czy *MyWriter implementuje io.Writer.


Constructor-like function

Go nie ma konstruktorów językowych.

Często stosuje się:

func NewServer(cfg Config) *Server {
    return &Server{
        cfg: cfg,
    }
}

Wywołanie:

server := NewServer(cfg)

Nazwy:

New
NewServer
NewClient
NewRepository

są tylko konwencją.


Functional options

W większych bibliotekach zobaczysz:

client := NewClient(
    WithTimeout(5*time.Second),
    WithRetries(3),
)

W środku opcja często jest funkcją:

type Option func(*Client)

To popularny sposób konfiguracji API bez konstruktora z piętnastoma argumentami.


Must...

Funkcja o nazwie:

MustLoad
MustParse
MustCompile

zwykle oznacza:

jeśli operacja się nie uda, funkcja zrobi panic.

Przykład ze standardowej biblioteki:

template.Must(...)

Nadaje się zwykle tam, gdzie błąd jest błędem programisty lub start aplikacji bez danego zasobu nie ma sensu.


New...

http.NewServeMux()
bytes.NewBuffer(...)
json.NewEncoder(...)

zwykle tworzy i zwraca obiekt.


With...

context.WithTimeout(...)
context.WithCancel(...)

często oznacza utworzenie zmodyfikowanej wersji istniejącego obiektu/kontekstu.


49. Rzeczy, które wyglądają dziwnie, ale są normalne

:=

x := 10

Deklaracja + przypisanie.

&User{}

u := &User{}

Utwórz User i zwróć pointer do niego.

*User

func Save(u *User)

Pointer do User.

[]User

Slice struktur User.

[]*User

Slice pointerów do User.

map[string]User

Mapa:

string → User

map[string][]User

Mapa:

string → slice User

chan Result

Kanał przenoszący Result.

<-chan Result

Kanał tylko do odbierania.

chan<- Result

Kanał tylko do wysyłania.

func() error

Typ funkcji, która:

  • nie przyjmuje argumentów,
  • zwraca error.

func(context.Context, string) (*User, error)

Typ funkcji:

  • argument context.Context,
  • argument string,
  • zwraca *User,
  • zwraca error.

...string

Variadic:

func Log(tags ...string)

przyjmuje dowolną liczbę stringów.

struct{}

Pusta struktura.

struct{}

zajmuje zero bajtów danych użytkowych i bywa używana do sygnalizacji.

Przykład:

chan struct{}

czyli kanał służący nie do przesyłania danych, tylko sygnału.

map[string]any

Dynamiczny zestaw danych:

string → dowolny typ

Częsty przy luźnym JSON.


50. Jak czytać obcy projekt Go

To najważniejsza część całego kompendium, jeśli Twoim celem jest rozumienie kodu.

Krok 1 - znajdź go.mod

cat go.mod

Dowiesz się:

  • jak nazywa się moduł,
  • jakiej wersji Go wymaga,
  • jakich bibliotek używa.

Sprawdź przede wszystkim, czy widzisz:

chi
gin
echo
fiber
pgx
gorm
redis
ebiten
cobra
viper

To od razu mówi dużo o projekcie.


Krok 2 - znajdź package main

rg 'package main'

lub:

grep -R '^package main' .

Potem znajdź:

func main()

To punkt startowy programu.


Krok 3 - zobacz, co main() tworzy

Na przykład:

func main() {
    cfg := loadConfig()
    db := openDatabase(cfg)
    repo := NewRepository(db)
    service := NewService(repo)
    server := NewServer(service)
    server.Run()
}

Już masz mapę programu:

config
  ↓
database
  ↓
repository
  ↓
service
  ↓
HTTP server

Krok 4 - rozpoznaj warstwy

Typowy backend:

HTTP handler
    ↓
service
    ↓
repository
    ↓
database

Handler

Odpowiada za HTTP:

func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request)

Service

Logika aplikacji:

func (s *Service) GetUser(ctx context.Context, id int64)

Repository

Dostęp do danych:

func (r *Repository) GetUser(ctx context.Context, id int64)

Krok 5 - czytaj struktury

Jeśli widzisz:

type Server struct {
    router  http.Handler
    service *Service
    logger  *slog.Logger
}

wiesz, od czego Server zależy.

Struct jest często najlepszą mapą architektury.


Krok 6 - czytaj interfejsy

type UserStore interface {
    Get(context.Context, int64) (User, error)
    Save(context.Context, User) error
}

Interfejs pokazuje kontrakt między warstwami.


Krok 7 - znajdź trasy HTTP

Szukaj:

HandleFunc
Handle
GET
POST
PUT
DELETE
Route
Mount

Dostajesz listę wejść do aplikacji.


Krok 8 - znajdź goroutines

rg '\bgo\s+'

Sprawdź:

  • co działa w tle,
  • jak proces się zamyka,
  • gdzie używany jest context,
  • gdzie są kanały.

Krok 9 - znajdź I/O

Szukaj:

os.ReadFile
os.WriteFile
http.Client
pgx
sql.DB
redis
json.NewDecoder
json.NewEncoder

To pokaże, gdzie program komunikuje się ze światem.


Krok 10 - uruchom testy

go test ./...

To często najlepszy szybki test zdrowia projektu.

Potem:

go vet ./...

51. Miniaturowy backend - przykład całości

Poniższy przykład pokazuje wszystkie najważniejsze klocki naraz.

package main

import (
    "encoding/json"
    "log/slog"
    "net/http"
    "os"
)

type App struct {
    logger *slog.Logger
}

type HealthResponse struct {
    Status string `json:"status"`
}

func (a *App) health(w http.ResponseWriter, r *http.Request) {
    response := HealthResponse{
        Status: "ok",
    }

    w.Header().Set("Content-Type", "application/json")

    if err := json.NewEncoder(w).Encode(response); err != nil {
        a.logger.Error("cannot encode response", "error", err)
    }
}

func main() {
    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

    app := &App{
        logger: logger,
    }

    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", app.health)

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    logger.Info("server starting", "addr", server.Addr)

    if err := server.ListenAndServe(); err != nil {
        logger.Error("server stopped", "error", err)
    }
}

Rozbiór:

type App struct {
    logger *slog.Logger
}

Aplikacja przechowuje logger.

type HealthResponse struct {
    Status string `json:"status"`
}

Struktura odpowiedzi JSON.

func (a *App) health(...)

Metoda App, działająca jako handler.

json.NewEncoder(w).Encode(response)

Serializuje strukturę do JSON i zapisuje do odpowiedzi HTTP.

mux.HandleFunc("GET /health", app.health)

Rejestruje trasę.

server.ListenAndServe()

Uruchamia serwer.

To jest już prawdziwy, działający backend HTTP.


52. Miniaturowa gra - przykład struktury

Przykładowa koncepcja Ebitengine:

type Player struct {
    X float64
    Y float64
}

type Game struct {
    Player Player
}

func (g *Game) Update() error {
    if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) {
        g.Player.X -= 2
    }

    if ebiten.IsKeyPressed(ebiten.KeyArrowRight) {
        g.Player.X += 2
    }

    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    // tutaj renderer
}

func (g *Game) Layout(w, h int) (int, int) {
    return 800, 600
}

Mentalnie:

Game
├── dane świata
├── dane gracza
├── Update()   -> logika
├── Draw()     -> grafika
└── Layout()   -> ekran

Większa gra może wyglądać:

cmd/game/main.go
internal/game/game.go
internal/game/player.go
internal/game/enemy.go
internal/game/world.go
internal/game/input.go
internal/game/render.go
assets/

Nie musisz jednak od razu dzielić wszystkiego na kilkanaście pakietów. W małej grze kilka plików w jednym pakiecie main może być znacznie czytelniejsze.


53. Ściąga poleceń

Projekt

Nowy katalog:

mkdir myapp
cd myapp

Nowy moduł:

go mod init github.com/example/myapp

Uruchom:

go run .

Kompiluj:

go build

Kompiluj do konkretnej nazwy:

go build -o myapp

Wszystkie pakiety:

go build ./...

Zależności

Dodaj:

go get github.com/go-chi/chi/v5

Porządkuj:

go mod tidy

Lista:

go list -m all

Dlaczego zależność istnieje:

go mod why MODULE

Graf:

go mod graph

Jakość

Format:

go fmt ./...

Analiza:

go vet ./...

Testy:

go test ./...

Testy szczegółowo:

go test -v ./...

Race detector:

go test -race ./...

Coverage:

go test -cover ./...

Benchmark:

go test -bench=. ./...

Informacje

Wersja:

go version

Środowisko:

go env

Dokumentacja symbolu:

go doc fmt.Println

Dokumentacja pakietu:

go doc net/http

Lista pakietów:

go list ./...

54. Ściąga składni

Zmienna

var x int
x := 10

Stała

const Max = 100

Slice

items := []string{"a", "b"}
items = append(items, "c")

Map

m := map[string]int{}
m["a"] = 1

Struct

type User struct {
    ID   int64
    Name string
}

Pointer

p := &user

Funkcja

func Add(a, b int) int {
    return a + b
}

Funkcja z błędem

func Load() (Data, error)

Metoda

func (u *User) Rename(name string)

Interfejs

type Store interface {
    Save(User) error
}

if

if x > 10 {
}

switch

switch x {
case 1:
case 2:
default:
}

for

for i := 0; i < 10; i++ {
}

range

for i, v := range values {
}

Error handling

value, err := load()
if err != nil {
    return err
}

Goroutine

go work()

Channel

ch := make(chan Result)
ch <- result
result := <-ch

Context

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

JSON tag

Name string `json:"name"`

Import grupowy

import (
    "context"
    "fmt"
    "net/http"
)

55. Co warto znać, a czego na razie nie trzeba

Jeżeli Twoim celem jest głównie rozumienie projektu, poprawianie go z pomocą AI, przeglądanie PR-ów i orientowanie się, co się dzieje, skoncentruj się na tych rzeczach.

Musisz dobrze rozpoznawać

package
import
func
var
const
:=
struct
slice
map
pointer
if
switch
for
range
error
method
interface
context
goroutine
channel
go.mod
go test
go build

Powinieneś rozumieć na poziomie ogólnym

generics
mutex
WaitGroup
HTTP handler
middleware
database/sql
pgx
JSON
html/template
embed

Możesz na razie traktować jako „wiem, że istnieje”

reflection
unsafe
zaawansowany model pamięci
compiler internals
assembly
custom allocators
skomplikowane generic constraints
plugin package
linker flags
profilowanie runtime na głębokim poziomie

Dla backendu stron

Najbardziej praktyczna ścieżka rozumienia kodu:

Go syntax
   ↓
struct / method / interface
   ↓
error
   ↓
context
   ↓
net/http
   ↓
JSON / templates
   ↓
PostgreSQL + pgx
   ↓
chi lub inny router
   ↓
testy

Dla gier

Go syntax
   ↓
struct
   ↓
methods
   ↓
slices / maps
   ↓
input
   ↓
game loop
   ↓
Ebitengine lub raylib-go

56. Dalsza dokumentacja

Najbardziej wartościowe źródła:

Oficjalne Go

  • Dokumentacja: https://go.dev/doc/
  • A Tour of Go: https://go.dev/tour/
  • Standardowa biblioteka: https://pkg.go.dev/std
  • Wyszukiwarka pakietów: https://pkg.go.dev/
  • Moduły: https://go.dev/ref/mod
  • Organizacja modułu: https://go.dev/doc/modules/layout
  • Effective Go: https://go.dev/doc/effective_go

Backend

  • net/http: https://pkg.go.dev/net/http
  • html/template: https://pkg.go.dev/html/template
  • Chi: https://github.com/go-chi/chi
  • Gin: https://gin-gonic.com/
  • Echo: https://echo.labstack.com/
  • Fiber: https://docs.gofiber.io/
  • pgx: https://github.com/jackc/pgx
  • sqlc: https://sqlc.dev/
  • go-redis: https://github.com/redis/go-redis

Gry

  • Ebitengine: https://ebitengine.org/
  • raylib: https://www.raylib.com/
  • raylib-go: https://github.com/gen2brain/raylib-go

Podsumowanie mentalne

Jeżeli otwierasz plik Go i widzisz:

func (s *Service) Check(ctx context.Context, promo *Promotion) ([]Issue, error)

powinieneś umieć przeczytać go niemal po polsku:

Jest metoda Check należąca do Service. Działa na pointerze do Service. Dostaje context i pointer do promocji. Zwraca slice problemów oraz błąd.

Jeżeli potem widzisz:

issues, err := s.validator.Validate(ctx, promo)
if err != nil {
    return nil, fmt.Errorf("validate promotion: %w", err)
}

czytasz:

Wywołaj walidator. Dostanę listę problemów i ewentualny błąd. Jeśli jest błąd, kończę działanie i opakowuję go dodatkowym kontekstem.

Jeśli zobaczysz:

go s.sendReport(ctx, issues)

czytasz:

Uruchom wysyłkę raportu współbieżnie w osobnej goroutine.

A jeśli:

mux.HandleFunc("GET /api/promotions/{id}", app.getPromotion)

czytasz:

Dla requestu GET pod tym URL-em uruchom metodę getPromotion.

I właśnie do tego poziomu rozumienia ten materiał ma Cię doprowadzić.


Jedna praktyczna rada na koniec

Nie próbuj czytać dużego projektu Go od pierwszego pliku do ostatniego.

Czytaj go jako graf przepływu:

main()
  ↓
co jest tworzone?
  ↓
co uruchamia serwer / CLI / grę?
  ↓
jakie są wejścia?
  ↓
jakie struktury przenoszą dane?
  ↓
jakie metody są wywoływane?
  ↓
gdzie pojawia się I/O?
  ↓
gdzie wraca error?

Jeżeli umiesz przejść tę ścieżkę, potrafisz zrozumieć zaskakująco dużą część realnego projektu Go nawet wtedy, gdy sam jeszcze nie napisałbyś go od zera.

Oficjalne źródła

  • Go documentation: https://go.dev/doc/
  • Go 1.27 release notes: https://go.dev/doc/go1.27
  • Go language specification: https://go.dev/ref/spec
  • Standard library and packages: https://pkg.go.dev/