Przejdź do głównej zawartości

Wzorce języka Go

Wzorce Go dla agentów AI to prompty i bramki weryfikacyjne, dzięki którym Claude Code, Codex i Cursor tworzą produkcyjne usługi Go: graceful shutdown, współbieżność ograniczoną przez errgroup, dobrane pule pgx, testy table-driven i Testcontainers oraz mapowanie statusów gRPC. Kod agenta przyjmuje się na podstawie wyników detektora wyścigów (-race), goleak, golangci-lint, govulncheck i testu obciążeniowego, a nie czytania linijka po linijce.

Twoja usługa Go obsługuje 200 żądań na sekundę na stagingu, a potem pada przy pierwszym kontakcie z prawdziwym ruchem. Goroutines uruchamiane per żądanie nigdy się nie kończą, w puli pgx kończą się wolne połączenia, bo brakuje rows.Close(), a wywołanie usługi zależnej bez deadline’u piętrzy się, aż OOM killer zabije pod. Kod agenta wyglądał idiomatycznie i przechodził jego własne testy happy path. Niczego, co wyprodukował, nie sprawdzono pod kątem awarii, które ujawniają się dopiero przy współbieżności.

Ta receptura jest dla programistów, którzy budują backendy w Go z agentem i chcą przestać przeglądać każdą goroutine wzrokiem. Szczegóły produkcyjne trafiają od razu do promptów, a dowód ich poprawności dostarcza maszyna.

  • Jeden blok reguł Go — w AGENTS.md dla Claude Code i Codeksa oraz skopiowany do reguły projektu w Cursorze — oraz bramkę make check, którą agent uruchamia, zanim powie „gotowe”
  • Pięć gotowych do wklejenia promptów: serwer HTTP na chi z graceful shutdown, ograniczony batch na errgroup, repozytorium pgx z dobraną pulą i bezpiecznymi transakcjami, testy table-driven i Testcontainers oraz usługę gRPC z mapowaniem statusów
  • Tabelę weryfikacji, która mówi, co łapie każda bramka, więc czytasz wynik bramki zamiast diffu
  • Prompty naprawcze na sześć sposobów, w jakie Go pisane przez agenta psuje się na produkcji

Przygotuj konwencje Go i bramkę, którą agent musi przejść

Dział zatytułowany „Przygotuj konwencje Go i bramkę, którą agent musi przejść”

Większość pracy wykonują dwa pliki: plik reguł, który agent czyta przy każdym zadaniu, i jedno polecenie, które kończy się błędem, gdy kod nie jest gotowy na produkcję. Napisz je raz na repozytorium, zanim poprosisz o jakikolwiek kod.

  1. Przypnij wspierany toolchain w go.mod. 26 września 2026 najnowszym wydaniem jest Go 1.27.1 (według proxy.golang.org), więc wspierane linie to Go 1.26 i 1.27. Jawna dyrektywa go powstrzymuje agenta przed idiomami sprzed wersji 1.22.

    module github.com/yourorg/orders
    go 1.26
    require (
    github.com/go-chi/chi/v5 v5.3.2
    github.com/jackc/pgx/v5 v5.11.0
    github.com/stretchr/testify v1.12.1
    go.uber.org/goleak v1.3.0
    go.uber.org/zap v1.28.0
    golang.org/x/sync v0.23.0
    )

    To najnowsze wersje z proxy.golang.org na dzień 2026-09-26. Później podnoś je przez go get -u ./...; chodzi o to, żeby agent widział aktualne wersje główne.

  2. Dodaj bramkę jako cel make check. To definicja „gotowe” dla każdego promptu na tej stronie.

    .PHONY: check
    check:
    test -z "$$(gofmt -l .)"
    go vet ./...
    golangci-lint run ./...
    go test -race -shuffle=on -count=1 ./...
    govulncheck ./...

    Zainstaluj raz oba zewnętrzne narzędzia (terminal):

    Okno terminala
    go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.14.0
    go install golang.org/x/vuln/cmd/govulncheck@latest
  3. Umieść reguły Go w AGENTS.md w katalogu głównym repozytorium. Codex czyta AGENTS.md natywnie; zakładki poniżej pokazują, jak Claude Code ładuje ten sam plik i jak skopiować go do reguły Cursora.

    ## Go conventions
    - Go 1.26+. Loop variables are per-iteration: never write `x := x` copies.
    - Layout: cmd/, internal/. No package-level mutable state.
    - Every blocking function takes ctx context.Context first. Never call
    context.Background() outside main() and tests.
    - Every `go` statement has a documented exit path on ctx cancellation.
    Prefer errgroup with SetLimit over raw goroutines.
    - Wrap errors with fmt.Errorf("op: %w", err). Compare with errors.Is/As, never ==.
    - Transactions use pgx.BeginFunc; do not hand-roll commit/rollback.
    - Tests are table-driven with t.Parallel(); use testify require/assert.
    - Never edit an existing *_test.go assertion or add t.Skip to make a test pass.
    If a test looks wrong, stop and say why.
    - Done means `make check` passes. Paste its last 20 lines in your final message.

Najważniejsze są dwie ostatnie reguły. Pierwsza nie pozwala agentowi osłabiać testów, żeby dojść do zielonego wyniku; dlaczego to najczęstszy sposób, w jaki praca agenta udaje sukces, wyjaśnia strona ochrona wyroczni testów. Druga zamienia „chyba działa” w dowód, który przeczytasz w dziesięć sekund.

Plik reguł i prompty są identyczne we wszystkich narzędziach. Różni się to, jak każde narzędzie ładuje reguły, uruchamia bramkę i przegląda wynik.

Na kanale latest (v2.1.277+) Claude Code czyta AGENTS.md, gdy w projekcie nie ma CLAUDE.md. Jeśli masz już CLAUDE.md albo korzystasz z kanału stable, dodaj import. Składnia importu @path wczytuje wskazany plik do kontekstu przy starcie:

@AGENTS.md

Formatuj każdy plik Go edytowany przez agenta hookiem PostToolUse w .claude/settings.json:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | grep '\\.go$' | xargs -r gofmt -w"
}
]
}
]
}
}

Nawigację świadomą typów da agentowi plugin gopls-lsp z oficjalnego marketplace’u Anthropic. Plugin tylko rejestruje serwer, więc najpierw zainstaluj gopls:

Okno terminala
go install golang.org/x/tools/gopls@latest
claude plugin install gopls-lsp@claude-plugins-official

Zanim otworzysz pull request, uruchom w sesji /code-review, żeby uzyskać drugą opinię o diffie.

Zacznij w każdym narzędziu od jego domyślnego modelu i podnieś poziom effort, zanim zmienisz model; aktualne domyślne modele znajdziesz w przeglądzie modeli.

Zbuduj produkcyjną usługę Go z agentem krok po kroku

Dział zatytułowany „Zbuduj produkcyjną usługę Go z agentem krok po kroku”

Każdy krok to jeden prompt, fragment dobrej odpowiedzi i bramka, która ją potwierdza. Wykonuj je po kolei: późniejsze prompty zakładają, że serwer i repozytorium już istnieją.

Jak wygenerować serwer HTTP w Go, który czysto się zamyka?

Dział zatytułowany „Jak wygenerować serwer HTTP w Go, który czysto się zamyka?”

Najczęstsze przeoczenie w pierwszym podejściu to zamykanie. Agent uruchamia ListenAndServe w goroutine i kończy proces bez dokończenia trwających żądań, więc każde wdrożenie gubi ruch.

Dobra odpowiedź ma taki rdzeń:

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Fatal("listen", zap.Error(err))
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
logger.Error("shutdown", zap.Error(err))
}

Nie musisz tego sprawdzać wzrokiem. Test zamykania nie przejdzie, jeśli Shutdown użyje anulowanego ctx, bo Shutdown zwróci wtedy context.Canceled, zanim trwające żądanie się zakończy. Timeouty serwera są ważne, bo http.Server domyślnie nie ma deadline’u odczytu ani zapisu, a go vet ci o tym nie powie.

Jak ograniczyć goroutines, żeby batch nie wyczerpał bazy danych?

Dział zatytułowany „Jak ograniczyć goroutines, żeby batch nie wyczerpał bazy danych?”

„Przetwórz te elementy współbieżnie” daje nieograniczony fan-out go func(). Pod obciążeniem otwiera jedno połączenie z bazą na element i wyczerpuje pulę albo limit deskryptorów plików.

func (s *Service) ProcessBatch(ctx context.Context, items []Item) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8)
for _, item := range items {
g.Go(func() error {
if err := ctx.Err(); err != nil {
return err
}
return s.processItem(ctx, item)
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf("process batch: %w", err)
}
return nil
}

Dowodem jest tu goleak.VerifyTestMain: pakiet nie przejdzie testów, jeśli po ich zakończeniu nadal działa jakakolwiek goroutine. Razem z -race w make check łapie dwa błędy współbieżności, których nie widać w diffie.

Jak skłonić agenta do napisania repozytorium pgx, które wytrzyma obciążenie?

Dział zatytułowany „Jak skłonić agenta do napisania repozytorium pgx, które wytrzyma obciążenie?”

Wyczerpanie puli to klasyczny błąd „działa na dev, pada na produkcji”. Jeden szczegół, którego agenci zwykle nie znają: domyślne MaxConns w pgxpool to większa z wartości 4 i runtime.NumCPU() (źródła pgx v5.11.0), więc kontener z dwoma vCPU dostaje cztery połączenia, jeśli nie ustawisz rozmiaru puli.

Prompt celowo wskazuje pgx.BeginFunc. Pozostawiony sam sobie agent pisze własny helper WithTx z odroczonym recover() i nazwanym zwracanym błędem, który jest poprawny tylko wtedy, gdy każda gałąź jest poprawna. BeginFunc zatwierdza transakcję, gdy callback zwraca nil, a w przeciwnym razie ją wycofuje, także gdy callback panikuje, więc jest mniej kodu agenta do przeglądania.

Prompt prosi też o kod 23505, a nie pgx.ErrNoRows, dla zduplikowanego adresu e-mail. INSERT, który narusza ograniczenie unikalności, zwraca *pgconn.PgError, nigdy ErrNoRows, a pomylenie jednego z drugim daje 500 tam, gdzie klient oczekiwał 409.

Żeby agent czytał prawdziwy schemat zamiast zgadywać nazwy kolumn, podłącz serwer MCP bazy danych wyłącznie do bazy deweloperskiej. Przewodnik po serwerach MCP dla baz danych porównuje opcje i ich przełączniki tylko do odczytu. Nie używaj @modelcontextprotocol/server-postgres: pakiet jest oznaczony w npm jako przestarzały i zarchiwizowany.

Jak generować testy Go, które łapią prawdziwe błędy?

Dział zatytułowany „Jak generować testy Go, które łapią prawdziwe błędy?”

Nazwij przypadki, inaczej dostaniesz pokrycie wyłącznie happy path. Potem dodaj jeden test integracyjny na prawdziwym Postgresie, bo zamockowane repozytorium nie wyłapie błędnej kolumny w SQL ani brakującego indeksu unikalnego.

Nazwy funkcji w drugim prompcie pochodzą z testcontainers-go v0.44.0 (sprawdzone 2026-09-26). Starszy punkt wejścia RunContainer nadal się kompiluje, więc bez jawnych nazw agent często po niego sięga.

Prośba o wypisanie przypadków przed kodem daje ci jedyną rzecz, którą warto przeczytać: listę testów. Jeśli lista pokrywa zachowanie, na którym ci zależy, a make check jest zielone, czytanie ciał testów nie było potrzebne. Więcej o projektowaniu testów z agentem znajdziesz na stronach o testach jednostkowych z agentem i testach integracyjnych.

Jak postawić usługę gRPC, która nie ujawnia błędów wewnętrznych?

Dział zatytułowany „Jak postawić usługę gRPC, która nie ujawnia błędów wewnętrznych?”

Wygenerowany handler, który zwraca surowy err, potrafi wysłać klientowi treść nieudanego zapytania SQL, więc poproś o jedną funkcję, która odpowiada za mapowanie błędów domenowych na kody statusu gRPC.

Dobra odpowiedź ma taki rdzeń:

func toStatus(err error) error {
switch {
case errors.Is(err, users.ErrNotFound):
return status.Error(codes.NotFound, "user not found")
case errors.Is(err, users.ErrDuplicateEmail):
return status.Error(codes.AlreadyExists, "email already registered")
case errors.Is(err, context.DeadlineExceeded):
return status.Error(codes.DeadlineExceeded, "deadline exceeded")
default:
slog.Error("unmapped error", "err", err) // szczegóły zostają w logach
return status.Error(codes.Internal, "internal error")
}
}

Kluczowa jest funkcja mapująca: każdy błąd opuszcza usługę przez nią, a gałąź default zostawia wewnętrzne szczegóły w logach zamiast w odpowiedzi dla klienta. Testy na bufconn potwierdzają każde mapowanie bez sieci i bez człowieka czytającego handlery.

Jak zweryfikować Go napisane przez agenta bez czytania każdej linijki?

Dział zatytułowany „Jak zweryfikować Go napisane przez agenta bez czytania każdej linijki?”

Czytaj wynik bramek i listę testów, nie diff. Każda bramka poniżej łapie klasę błędów, które w przeglądzie wyglądają dobrze, i dlatego czytanie jest najsłabszą kontrolą kodu współbieżnego w Go.

BramkaPolecenieCo łapie
Formatowanie i vetgofmt -l ., go vet ./...Niesformatowane pliki, błędne czasowniki Printf, kopiowane blokady, nieosiągalny kod
Lintgolangci-lint run ./...Niesprawdzone błędy (errcheck), nieskuteczne przypisania, uwagi staticcheck; == na opakowanych błędach po włączeniu errorlint
Race detectorgo test -race -shuffle=on -count=1 ./...Wyścigi danych i testy zależne od kolejności
Wycieki goroutinesgoleak.VerifyTestMain(m) w każdym pakiecie współbieżnymGoroutines bez ścieżki wyjścia
Podatnościgovulncheck ./...Znane CVE w ścieżkach kodu, które faktycznie wywołujesz
IntegracjaTesty Testcontainers bez -shortBłędny SQL, brakujące ograniczenia, złe migracje
ObciążenieDwukrotność oczekiwanego szczytu na stagingu, np. echo "GET https://staging.example.com/api/v1/items" | vegeta attack -rate=400 -duration=60s | vegeta reportWyczerpanie puli, brakujące timeouty, rosnące zużycie pamięci
Agent przeglądający/code-review (Claude Code), codex review --base main (Codex), Bugbot (Cursor)Błędy logiki i brakujące przypadki, których bramki nie wyrażą

Pierwszych sześć uruchamiaj w CI przy każdym pull requeście; plik workflow znajdziesz we wzorcach potoków CI/CD. Inżynier odpowiedzialny za usługę zatwierdza trzy rzeczy: listę przypadków testowych, zieloną bramkę i wykres z testu obciążeniowego. Człowiek czyta tylko fragmenty oznaczone przez agenta przeglądającego oraz kod dotykający transakcji lub goroutines. Jak podzielić tę pracę w zespole, opisuje strona o przeglądzie PR z agentem.

Każda awaria poniżej przechodzi zestaw testów happy path. Każda ma objaw, który możesz wykryć, i sposób naprawy, który możesz wkleić.

  • Wyciek goroutines. Objaw: go_goroutines w metrykach rośnie i nigdy nie spada. Przyczyna: go func() zablokowane na wysyłce do kanału bez odbiorcy albo bez gałęzi ctx.Done(). Naprawa: dodaj goleak.VerifyTestMain do pakietu, a potem wklej prompt: „Audit every go statement in this package. For each one, name its exit path under context cancellation, and fix any that have none. Done means make check passes.”

  • Kontekst zgubiony w połowie stosu. Objaw: żądania działają dalej po rozłączeniu klienta, a deadline’y nic nie robią. Przyczyna: context.Background() lub context.TODO() głęboko w handlerze. Naprawa: grep -rn "context.Background()\|context.TODO()" --include=*.go internal/ i poproś agenta, żeby przeprowadził ctx przez każdą znalezioną funkcję.

  • Wyczerpanie puli. Objaw: skoki opóźnień, a potem żądania wiszą w oczekiwaniu na połączenie. Przyczyna: brakujące rows.Close(), zapytanie na pool wewnątrz transakcji (dwa połączenia na żądanie) albo domyślny rozmiar puli. Naprawa: ustaw MaxConns świadomie, wystaw pool.Stat() w endpoincie diagnostycznym i wklej prompt: „Find every Query call whose rows are not closed on every path, and every pool query made while a pgx.Tx is open.”

  • Przestarzałe idiomy z dawnych danych treningowych. Objaw: kopie item := item, importy ioutil albo wszędzie interface{}. Przyczyna: model czerpie z kodu sprzed Go 1.22. Naprawa: dyrektywa go i reguła w AGENTS.md zapobiegają większości przypadków; na resztę wklej prompt: „Modernise this package for Go 1.26: remove loop-variable copies, replace ioutil, and use any. Change no behaviour.”

  • Agent osłabia test, żeby dojść do zielonego wyniku. Objaw: poluzowana asercja, usunięty przypadek albo dodany t.Skip w tym samym diffie co poprawka. Naprawa: przywróć plik testu, przypomnij regułę z AGENTS.md i skonfiguruj CI tak, żeby odrzucało pull request zmieniający, przenoszący lub usuwający istniejący *_test.go bez etykiety test-change. Krok jest bramką tylko wtedy, gdy pull request nie może go usunąć: GitHub uruchamia plik workflow z samego pull requesta, więc obejmij /.github/ regułą CODEOWNERS i wymuś tę recenzję przez ochronę gałęzi lub ruleset. Ten krok zakłada actions/checkout z fetch-depth: 0; szerszą politykę opisuje strona ochrona wyroczni testów.

    - name: Block edits to existing tests without a test-change label
    if: ${{ !contains(github.event.pull_request.labels.*.name, 'test-change') }}
    # Needs actions/checkout with fetch-depth: 0: origin/main... diffs from the merge base.
    run: |
    # --name-status lists a rename as R<score> OLD NEW; field 2 is the path that existed before
    if git diff --name-status --diff-filter=MDR origin/main... | awk -F'\t' '{print $2}' | grep '_test\.go$'; then
    echo "Existing tests changed without the test-change label"; exit 1
    fi
  • golangci-lint wywraca się, zanim cokolwiek sprawdzi. Objaw: błąd, że wersja Go użyta do zbudowania golangci-lint jest niższa niż docelowa, albo błąd konfiguracji w .golangci.yml napisanym przez agenta w formacie v1. Naprawa: zainstaluj golangci-lint v2 zbudowany wersją Go co najmniej tak nową jak twoja dyrektywa go i uruchom golangci-lint migrate na starej konfiguracji.