Wzorce języka Go
Twoja usługa Go obsługuje 200 req/s na deweloperce bez problemu, a potem wywraca się przy pierwszym kontakcie z prawdziwym ruchem. Goroutines, które uruchamiasz per żądanie, nigdy nie kończą działania, pula pgx głoduje, bo nic nie zwalnia połączeń, a brak podpiętego timeoutu kontekstu sprawia, że wolne wywołania do usług downstream piętrzą się, aż maszynie zabraknie pamięci. Kod wygenerowany przez agenta AI wyglądał idiomatycznie — po prostu nie był utwardzony pod produkcję.
Ta receptura jest o tym, jak zrobić Go dobrze za pierwszym razem z Cursor, Claude Code i Codex: o promptach, które od początku proszą o graceful shutdown, ograniczoną współbieżność i limity puli połączeń, plus o promptach uzupełniających, które wyłapują to, o czym pierwsze podejście zapomina.
Co z tego wyniesiesz
Dział zatytułowany „Co z tego wyniesiesz”- Gotowy do wklejenia prompt, który scaffolduje serwer HTTP na Chi ze strukturalnym logowaniem i graceful shutdown
- Prompt na worker pool, który ogranicza współbieżność i nigdy nie wycieka goroutine
- Prompt na repozytorium pgxpool z rozsądnymi limitami puli i wrapperem transakcji wycofującym zmiany przy panice
- Prompt na testy table-driven (testify + równoległe podtesty) oraz prompt na test integracyjny z testcontainers, korzystający z aktualnego API
postgres.Run - Prompt na usługę gRPC z interceptorami i health checkami
- Checklista „Kiedy to się sypie” dla trybów awarii, które kod Go generowany przez AI po cichu wdraża
Przygotuj kontekst projektu
Dział zatytułowany „Przygotuj kontekst projektu”Zanim poprosisz o kod, daj agentowi plik z regułami, żeby każda sugestia trzymała się Twoich konwencji. Mechanika różni się w zależności od narzędzia, ale treść jest identyczna.
Utwórz .cursor/rules/go.mdc (pojedynczy plik .cursorrules jest przestarzały — Cursor czyta teraz katalog .cursor/rules/*.mdc):
---description: Go conventions for this serviceglobs: ["**/*.go"]alwaysApply: true---
- Target Go 1.25+; use range-over-func and the standard library where it fits- Standard layout: cmd/, internal/, no global state- Wrap errors with fmt.Errorf("context: %w", err); define sentinel errors- Accept interfaces, return concrete structs- Every blocking call takes a context.Context as its first arg- Table-driven tests with t.Parallel(); use testify require/assertUtwórz CLAUDE.md w katalogu głównym repozytorium — Claude Code wczytuje go automatycznie:
## Go conventions- Target Go 1.25+; use range-over-func and the standard library where it fits- Standard layout: cmd/, internal/, no global state- Wrap errors with fmt.Errorf("context: %w", err); define sentinel errors- Accept interfaces, return concrete structs- Every blocking call takes a context.Context as its first arg- Table-driven tests with t.Parallel(); use testify require/assertCodex czyta ten sam CLAUDE.md, jeśli istnieje, albo własny AGENTS.md. Codex działa na GPT-5.5 w powierzchniach CLI, IDE i Cloud, więc jeden plik reguł obejmuje każdy punkt wejścia:
## Go conventions- Target Go 1.25+; use range-over-func and the standard library where it fits- Standard layout: cmd/, internal/, no global state- Wrap errors with fmt.Errorf("context: %w", err); define sentinel errors- Accept interfaces, return concrete structs- Every blocking call takes a context.Context as its first arg- Table-driven tests with t.Parallel(); use testify require/assertPrzypnij aktualny toolchain w go.mod, żeby agent nie sięgał po przestarzałe API:
module github.com/yourorg/yourservice
go 1.25
require ( github.com/go-chi/chi/v5 v5.2.5 github.com/jackc/pgx/v5 v5.8.0 github.com/stretchr/testify v1.10.0 go.uber.org/zap v1.27.1 golang.org/x/sync v0.11.0)Przepływ pracy
Dział zatytułowany „Przepływ pracy”1. Scaffolduj serwer HTTP
Dział zatytułowany „1. Scaffolduj serwer HTTP”Największą rzeczą, którą AI psuje za pierwszym razem, jest shutdown: uruchamia serwer w goroutine i zapomina opróżnić żądania w locie. Wymień graceful shutdown w prompcie, a dostaniesz go za darmo.
Model zwraca coś takiego — zwróć uwagę na signal.NotifyContext zamiast ręcznie sklejanego kanału os.Signal, co jest nowoczesnym idiomem:
func main() { logger, _ := zap.NewProduction() defer func() { _ = logger.Sync() }()
r := chi.NewRouter() r.Use(middleware.RequestID, middleware.RealIP, middleware.Recoverer) r.Use(middleware.Timeout(60 * time.Second)) r.Route("/api/v1", func(r chi.Router) { r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) })
srv := &http.Server{Addr: ":8080", Handler: r}
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("server failed", zap.Error(err)) } }()
<-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { logger.Fatal("shutdown failed", zap.Error(err)) }}Jak to ocenić: sprawdź, czy błąd nasłuchiwania jest porównywany przez errors.Is(err, http.ErrServerClosed) (zwykłe != błędnie zaklasyfikuje opakowany błąd) oraz czy shutdown używa świeżego kontekstu — użycie anulowanego ctx natychmiast przerwałoby opróżnianie.
2. Ogranicz współbieżność
Dział zatytułowany „2. Ogranicz współbieżność”Naiwny prompt „przetwórz te elementy współbieżnie” produkuje nieograniczony fan-out go func(), który pod obciążeniem wyczerpie deskryptory plików albo połączenia do bazy. Poproś wprost o ograniczony worker pool lub errgroup.SetLimit.
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("batch processing failed: %w", err) } return nil}Od Go 1.22 zmienna pętli for jest per-iterację, więc stara linia przechwytująca item := item to martwy kod — jeśli agent ją dodaje, to sygnał, że czerpie z danych treningowych sprzed 1.22. Odpowiedz: „Remove the item := item line; we’re on Go 1.25 where loop vars are per-iteration.”
3. Podłącz repozytorium i pulę
Dział zatytułowany „3. Podłącz repozytorium i pulę”Wyczerpanie puli połączeń to klasyczny bug „działa na deweloperce, umiera na produkcji”. Lekarstwem jest ustawienie limitów puli w prompcie, a nie odkrywanie ich w trakcie incydentu.
func (r *Repository) WithTx(ctx context.Context, fn func(pgx.Tx) error) (err error) { tx, err := r.pool.Begin(ctx) if err != nil { return fmt.Errorf("begin tx: %w", err) } defer func() { if p := recover(); p != nil { _ = tx.Rollback(ctx) panic(p) // re-throw after cleanup } if err != nil { _ = tx.Rollback(ctx) } }()
if err = fn(tx); err != nil { return err } return tx.Commit(ctx)}Jak to ocenić: nazwany zwracany wynik (err error) pozwala odroczonemu rollbackowi zobaczyć błąd funkcji, więc nie potrzebujesz wywołania rollbacku w każdej gałęzi. Jeśli model wpisuje rollback wprost w każdej ścieżce błędu, poproś o skonsolidowanie do formy odroczonej — łatwo inaczej zapomnieć o którejś gałęzi.
4. Wygeneruj testy
Dział zatytułowany „4. Wygeneruj testy”Poproś o testy table-driven z równoległymi podtestami i testify oraz wymień przypadki brzegowe, na których Ci zależy — inaczej dostaniesz pokrycie tylko ścieżki sukcesu.
W testach integracyjnych poproś wprost o aktualny punkt wejścia testcontainers postgres.Run — starsze API GenericContainer/RunContainer jest przestarzałe, a agent po nie sięgnie, jeśli nie powiesz inaczej.
5. Postaw usługę gRPC
Dział zatytułowany „5. Postaw usługę gRPC”Mapowanie błędów domenowych na kody statusu to część, którą trzeba przejrzeć — wygenerowany handler zwracający surowy err ujawnia klientom wewnętrzne szczegóły:
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) { user, err := s.svc.GetUser(ctx, req.GetId()) if err != nil { if errors.Is(err, ErrNotFound) { return nil, status.Error(codes.NotFound, "user not found") } return nil, status.Error(codes.Internal, "internal error") } return &pb.User{Id: user.ID, Name: user.Name, Email: user.Email}, nil}Różnice między narzędziami warte poznania
Dział zatytułowany „Różnice między narzędziami warte poznania”Prompty powyżej są identyczne we wszystkich trzech narzędziach. Tam, gdzie narzędzia się różnią, to sposób ich uruchamiania:
Używaj trybu Agent do wieloplikowych scaffoldów (serwer + trasy + repozytorium w jednym przebiegu) oraz edycji inline (Cmd/Ctrl+K) do chirurgicznych poprawek, takich jak zamiana GenericContainer na postgres.Run. Checkpointy pozwalają cofnąć złą wieloplikową generację jednym kliknięciem. Dla promptów mocno opartych na architekturze — pełny scaffolding usługi, złożone wieloplikowe refaktoryzacje — używaj Claude Fable 5 (/model fable), gdy szybkość i jakość są ważniejsze niż koszt; Opus 4.8 pozostaje dobrym domyślnym wyborem. Zejdź do Sonnet 4.6 dla mechanicznych przebiegów generowania testów. Szczegóły znajdziesz w porównaniu modeli.
Uruchamiaj prompty scaffoldujące prosto z terminala:
claude "Create a Go HTTP server in cmd/server/main.go using go-chi/chi v5 with RequestID, RealIP, Recoverer, a 60s Timeout, zap logging, GET /healthz, and graceful shutdown via signal.NotifyContext + srv.Shutdown(10s)."Dla powtarzalnych kontroli podepnij hook, żeby go vet ./... i go test -short ./... uruchamiały się po każdej edycji, a w CI używaj claude -p w trybie headless do przeglądu diffów. Sub-agenci przydają się do rozdzielenia „napisz repozytorium” i „napisz jego testy” na równoległe przebiegi.
Codex działa na GPT-5.5 w powierzchniach CLI, IDE i Cloud. Z poziomu CLI przekaż prompt pozycyjnie i pozwól mu pracować w worktree, żeby główna gałąź pozostała czysta:
codex "Create a Go HTTP server in cmd/server/main.go using go-chi/chi v5 with RequestID, RealIP, Recoverer, a 60s Timeout, zap logging, GET /healthz, and graceful shutdown via signal.NotifyContext + srv.Shutdown(10s)."Użyj --ask-for-approval on-request (a --full-auto, gdy już ufasz pętli), żeby mógł samodzielnie uruchamiać go build i go test. Powierzchnia Cloud może podjąć to samo zadanie z issue w GitHubie lub Linearze i otworzyć PR z wygenerowaną usługą.
Kiedy to się sypie
Dział zatytułowany „Kiedy to się sypie”Nawet przy dobrych promptach kod Go generowany przez AI wdraża te tryby awarii. Wypatruj ich w przeglądzie:
- Wycieki goroutine.
go func(), które blokuje się na wysyłce do kanału, którego nikt nie odbiera, albo goroutine per żądanie bez ścieżki wyjścia poctx, wycieka jedną goroutine na wywołanie. Uruchamiaj zgo test -racei dodajgoleak.VerifyTestMain(go.uber.org/goleak), żeby wyłapać wycieki w CI. Prompt uzupełniający: „Audit everygo func()in this file for a guaranteed exit path under context cancellation.” - Nieprzekazany kontekst. Jeśli handler wywołuje
context.Background()głęboko w stosie zamiast przekazaćctxżądania, anulowanie i deadline’y po cichu przestają działać. Przeszukaj kod (grep) pod kątemcontext.Background()pozamaini testami. - Wyczerpanie puli. Pominięcie
defer rows.Close()albo uruchamianie zapytań napoolwewnątrz transakcji, która trzyma osobne połączenie, zagładza pulę. Objaw: żądania wieszają się napool.Acquirepod obciążeniem. UstawMaxConnsświadomie i przetestuj obciążeniowo przed wdrożeniem. - Artefakty przechwytywania pętli sprzed 1.22. Jeśli model wypluwa
item := itemalbofor i := range xs { go f(i) }z ręczną kopią, pracuje na przestarzałych danych treningowych. Zmienne pętli per-iterację są domyślne od Go 1.22 — usuń obejście. - Błędy porównywane przez
==zamiasterrors.Is. Opakowane błędy (%w) psująerr == ErrNotFound. Poproś agenta, żeby używałerrors.Is/errors.Aswszędzie i definiował sentinele jako zmienne na poziomie pakietu. - Przesłonięte nazwy pakietów. Parametr o nazwie
urlprzesłania pakietnet/url, więcurl.Parse(url)się nie skompiluje. Jeśli build pada z „type string has no field or method Parse”, sprawdź, czy parametr nie przesłania importu, i zmień jego nazwę (np.rawURL).
Co dalej
Dział zatytułowany „Co dalej”Zastosuj to samo podejście prompt-first do swojej warstwy danych we Wzorcach baz danych albo wdróż tę usługę za pomocą Wzorców Kubernetes.
Podepnij kontrole go test -race i go vet do hooka pre-commit, a potem zajrzyj do Wzorców Kubernetes po przepływy wdrożeniowe w trybie headless.
Przekaż wdrożenie tej usługi zadaniu Codex Cloud — zacznij od Wzorców Kubernetes albo utwardź dostęp do danych za pomocą Wzorców baz danych.