Przejdź do głównej zawartości

Wzorce języka Go

Buduj wysokowydajne aplikacje Go z Cursor i Claude Code. Ten przewodnik obejmuje idiomatyczne wzorce Go, programowanie współbieżne z goroutines i kanałami, rozwój mikroserwisów, strategie testowania i wzorce wdrażania cloud-native.

  1. Inicjalizacja projektu Go

    Okno terminala
    # Standardowy projekt Go
    Ask: "Utwórz nowy moduł Go ze standardową strukturą projektu dla REST API"
    # Lub użyj trybu Agent dla kompletnej konfiguracji
    Agent: "Skonfiguruj mikroserwis Go z frameworkiem Gin, PostgreSQL i Dockerem"
  2. Konfiguracja środowiska deweloperskiego

    // .cursorrules lub CLAUDE.md
    - Używaj funkcji Go 1.22+
    - Przestrzegaj standardowego layoutu projektu Go
    - Używaj wrappowania błędów z fmt.Errorf("%w", err)
    - Preferuj testy table-driven
    - Używaj context dla anulowania
    - Stosuj przysłowia i idiomy Go
  3. Konfiguracja zależności

    Okno terminala
    # go.mod
    module github.com/yourorg/yourproject
    go 1.22
    require (
    github.com/go-chi/chi/v5 v5.0.12
    github.com/jackc/pgx/v5 v5.5.5
    github.com/stretchr/testify v1.9.0
    go.uber.org/zap v1.27.0
    )
// Prompt AI
Agent: "Utwórz produkcyjny serwer HTTP z:
- Routerem Chi z middleware
- Strukturalnym logowaniem JSON
- Sprawdzaniem zdrowia
- Graceful shutdown
- Dokumentacją OpenAPI"
// Wygenerowany serwer
package main
import (
"context"
"net/http"
"os"
"os/signal"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
r := chi.NewRouter()
// Middleware
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(60 * time.Second))
// Trasy
r.Route("/api/v1", func(r chi.Router) {
r.Get("/health", healthHandler)
r.Mount("/users", userRoutes())
})
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
// Graceful shutdown
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
logger.Fatal("server failed", zap.Error(err))
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.Fatal("shutdown failed", zap.Error(err))
}
}
// Prompt AI: "Zaimplementuj warstwę serwisu z dependency injection i obsługą błędów"
type UserService struct {
repo UserRepository
cache Cache
logger *zap.Logger
}
func NewUserService(repo UserRepository, cache Cache, logger *zap.Logger) *UserService {
return &UserService{
repo: repo,
cache: cache,
logger: logger,
}
}
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
// Sprawdź cache najpierw
if user, err := s.cache.Get(ctx, "user:"+id); err == nil {
return user.(*User), nil
}
user, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, fmt.Errorf("użytkownik %s nie znaleziony: %w", id, err)
}
s.logger.Error("nie udało się pobrać użytkownika",
zap.String("id", id),
zap.Error(err))
return nil, fmt.Errorf("pobieranie użytkownika: %w", err)
}
// Cache na następny raz
_ = s.cache.Set(ctx, "user:"+id, user, 5*time.Minute)
return user, nil
}
// Prompt AI
Ask: "Utwórz wzorzec worker pool z:
- Konfigurowalną liczbą workerów
- Kolejką zadań z buforowaniem
- Graceful shutdown
- Obsługą błędów
- Zbieraniem metryk"
type WorkerPool struct {
workers int
jobs chan Job
results chan Result
errors chan error
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
metrics *Metrics
}
func (wp *WorkerPool) Start() {
for i := 0; i < wp.workers; i++ {
wp.wg.Add(1)
go wp.worker(i)
}
}
func (wp *WorkerPool) worker(id int) {
defer wp.wg.Done()
for {
select {
case job, ok := <-wp.jobs:
if !ok {
return
}
start := time.Now()
result, err := wp.processJob(job)
wp.metrics.RecordJobDuration(time.Since(start))
if err != nil {
wp.errors <- fmt.Errorf("worker %d: %w", id, err)
wp.metrics.IncrementErrors()
} else {
wp.results <- result
wp.metrics.IncrementSuccess()
}
case <-wp.ctx.Done():
return
}
}
}
// Prompt AI: "Zaimplementuj propagację context z timeout i anulowaniem"
func (s *Service) ProcessBatch(ctx context.Context, items []Item) error {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
// Przetwarzaj elementy współbieżnie
for _, item := range items {
item := item // przechwytuj zmienną pętli
g.Go(func() error {
return s.processItem(ctx, item)
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf("przetwarzanie wsadowe nie powiodło się: %w", err)
}
return nil
}
func (s *Service) processItem(ctx context.Context, item Item) error {
// Sprawdź context przed kosztowną operacją
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Przetwarzaj z timeout
done := make(chan error, 1)
go func() {
done <- s.doExpensiveWork(item)
}()
select {
case err := <-done:
return err
case <-ctx.Done():
return fmt.Errorf("przetwarzanie anulowane: %w", ctx.Err())
}
}
// Prompt AI
Agent: "Utwórz wzorzec repository z:
- Poolingiem połączeń
- Prepared statements
- Wsparciem transakcji
- Query builders
- Wsparciem migracji"
type PostgresRepository struct {
pool *pgxpool.Pool
}
func NewPostgresRepository(ctx context.Context, dsn string) (*PostgresRepository, error) {
config, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
config.MaxConns = 25
config.MinConns = 5
config.MaxConnLifetime = time.Hour
config.MaxConnIdleTime = time.Minute * 30
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("create pool: %w", err)
}
return &PostgresRepository{pool: pool}, nil
}
func (r *PostgresRepository) CreateUser(ctx context.Context, user *User) error {
query := `
INSERT INTO users (id, email, name, created_at)
VALUES ($1, $2, $3, $4)
ON CONFLICT (email) DO NOTHING
RETURNING id`
err := r.pool.QueryRow(ctx, query,
user.ID, user.Email, user.Name, user.CreatedAt,
).Scan(&user.ID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrDuplicateEmail
}
return fmt.Errorf("insert user: %w", err)
}
return nil
}
// Prompt AI: "Zaimplementuj wrapper transakcji z rollback przy błędzie"
func (r *Repository) WithTransaction(ctx context.Context, fn func(pgx.Tx) error) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
defer func() {
if p := recover(); p != nil {
_ = tx.Rollback(ctx)
panic(p) // ponownie rzuć panic
}
}()
if err := fn(tx); err != nil {
if rbErr := tx.Rollback(ctx); rbErr != nil {
return fmt.Errorf("rollback failed: %v, original error: %w", rbErr, err)
}
return err
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit transaction: %w", err)
}
return nil
}
// Użycie
err := repo.WithTransaction(ctx, func(tx pgx.Tx) error {
if err := createOrder(ctx, tx, order); err != nil {
return err
}
return updateInventory(ctx, tx, order.Items)
})
// Prompt AI
Ask: "Wygeneruj kompleksowe testy table-driven z:
- Przypadkami brzegowymi
- Scenariuszami błędów
- Wykonywaniem równoległym
- Fixtures testowymi
- Mockowaniem"
func TestUserService_GetUser(t *testing.T) {
tests := []struct {
name string
userID string
mockSetup func(*mocks.UserRepository, *mocks.Cache)
want *User
wantErr error
}{
{
name: "sukces z cache",
userID: "123",
mockSetup: func(repo *mocks.UserRepository, cache *mocks.Cache) {
cache.On("Get", mock.Anything, "user:123").
Return(&User{ID: "123", Name: "Jan"}, nil)
},
want: &User{ID: "123", Name: "Jan"},
},
{
name: "sukces z bazy danych",
userID: "456",
mockSetup: func(repo *mocks.UserRepository, cache *mocks.Cache) {
cache.On("Get", mock.Anything, "user:456").
Return(nil, errors.New("nie znaleziono"))
repo.On("FindByID", mock.Anything, "456").
Return(&User{ID: "456", Name: "Anna"}, nil)
cache.On("Set", mock.Anything, "user:456",
mock.Anything, 5*time.Minute).Return(nil)
},
want: &User{ID: "456", Name: "Anna"},
},
{
name: "użytkownik nie znaleziony",
userID: "999",
mockSetup: func(repo *mocks.UserRepository, cache *mocks.Cache) {
cache.On("Get", mock.Anything, "user:999").
Return(nil, errors.New("nie znaleziono"))
repo.On("FindByID", mock.Anything, "999").
Return(nil, ErrNotFound)
},
wantErr: ErrNotFound,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
repo := new(mocks.UserRepository)
cache := new(mocks.Cache)
logger, _ := zap.NewDevelopment()
tt.mockSetup(repo, cache)
svc := NewUserService(repo, cache, logger)
got, err := svc.GetUser(context.Background(), tt.userID)
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
repo.AssertExpectations(t)
cache.AssertExpectations(t)
})
}
}
// Prompt AI: "Utwórz test integracyjny z prawdziwą bazą danych"
func TestIntegration_UserAPI(t *testing.T) {
if testing.Short() {
t.Skip("pomijanie testu integracyjnego")
}
ctx := context.Background()
// Uruchom kontener PostgreSQL
postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "postgres:16-alpine",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_PASSWORD": "test",
"POSTGRES_DB": "testdb",
},
WaitingFor: wait.ForListeningPort("5432/tcp"),
},
Started: true,
})
require.NoError(t, err)
defer postgres.Terminate(ctx)
// Pobierz connection string
host, err := postgres.Host(ctx)
require.NoError(t, err)
port, err := postgres.MappedPort(ctx, "5432")
require.NoError(t, err)
dsn := fmt.Sprintf("postgres://postgres:test@%s:%s/testdb", host, port.Port())
// Uruchom migracje
err = runMigrations(dsn)
require.NoError(t, err)
// Utwórz serwer testowy
srv := setupTestServer(dsn)
// Przypadki testowe
t.Run("utwórz użytkownika", func(t *testing.T) {
req := httptest.NewRequest("POST", "/api/v1/users",
strings.NewReader(`{"name":"Test User","email":"test@example.com"}`))
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
})
}
// Prompt AI: "Zaimplementuj obsługę błędów z wrappowaniem i niestandardowymi typami"
type Error struct {
Code string
Message string
Err error
}
func (e *Error) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func (e *Error) Unwrap() error {
return e.Err
}
// Błędy sentinelle
var (
ErrNotFound = &Error{Code: "NOT_FOUND", Message: "zasób nie znaleziony"}
ErrUnauthorized = &Error{Code: "UNAUTHORIZED", Message: "nieautoryzowany dostęp"}
ErrValidation = &Error{Code: "VALIDATION", Message: "walidacja nie powiodła się"}
)
// Obsługa błędów w handlerze HTTP
func handleError(w http.ResponseWriter, err error) {
var appErr *Error
if errors.As(err, &appErr) {
switch appErr.Code {
case "NOT_FOUND":
http.Error(w, appErr.Message, http.StatusNotFound)
case "UNAUTHORIZED":
http.Error(w, appErr.Message, http.StatusUnauthorized)
case "VALIDATION":
http.Error(w, appErr.Message, http.StatusBadRequest)
default:
http.Error(w, "Błąd wewnętrzny serwera", http.StatusInternalServerError)
}
return
}
http.Error(w, "Błąd wewnętrzny serwera", http.StatusInternalServerError)
}
// Prompt AI
Agent: "Utwórz serwis gRPC z:
- Definicjami Protocol buffer
- Implementacją serwera
- Klientem z logiką retry
- Interceptorami dla logging/auth
- Sprawdzaniem zdrowia"
// Wygenerowany serwis proto
syntax = "proto3";
package user.v1;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
}
// Implementacja serwera
type server struct {
user.UnimplementedUserServiceServer
svc *UserService
}
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, err := s.svc.GetUser(ctx, req.Id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, status.Error(codes.NotFound, "użytkownik nie znaleziony")
}
return nil, status.Error(codes.Internal, "błąd wewnętrzny")
}
return &pb.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
}, nil
}
// Prompt AI: "Utwórz benchmarki ze śledzeniem alokacji pamięci"
func BenchmarkUserService_GetUser(b *testing.B) {
svc := setupBenchmarkService()
ctx := context.Background()
b.ResetTimer()
b.ReportAllocs()
b.Run("z cache", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = svc.GetUser(ctx, "cached-user")
}
})
b.Run("bez cache", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = svc.GetUser(ctx, fmt.Sprintf("user-%d", i))
}
})
}
// Profilowanie CPU
func BenchmarkConcurrentRequests(b *testing.B) {
if *cpuprofile != "" {
f, _ := os.Create(*cpuprofile)
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
processRequest()
}
})
}

Wytyczne rozwoju Go

  1. Prostota - Pisz prosty, czytelny kod
  2. Interfejsy - Akceptuj interfejsy, zwracaj struktury
  3. Obsługa błędów - Opakowuj błędy z kontekstem
  4. Współbieżność - Nie komunikuj się przez dzielenie pamięci; dziel pamięć przez komunikację
  5. Testowanie - Testy table-driven z dobrym pokryciem
  6. Dokumentacja - Czytelne komentarze godoc
// AI: "Zaimplementuj wzorzec options dla konfiguracji"
type Option func(*Server)
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithLogger(logger *zap.Logger) Option {
return func(s *Server) {
s.logger = logger
}
}
func NewServer(opts ...Option) *Server {
s := &Server{
port: 8080,
logger: zap.NewNop(),
}
for _, opt := range opts {
opt(s)
}
return s
}
// Użycie
srv := NewServer(
WithPort(9090),
WithLogger(logger),
)
# Prompt AI: "Utwórz zoptymalizowany build Docker dla aplikacji Go"
# Etap budowania
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache git ca-certificates
WORKDIR /build
# Cache zależności
COPY go.mod go.sum ./
RUN go mod download
# Buduj aplikację
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o app ./cmd/server
# Etap końcowy
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /build/app /app
EXPOSE 8080
ENTRYPOINT ["/app"]