[teamai] Push 87 resource(s) from XingfenD

This commit is contained in:
2026-09-10 16:10:45 +08:00
parent 425c9c078a
commit 65c04def51
1314 changed files with 211681 additions and 0 deletions
@@ -0,0 +1,151 @@
# Architecture Patterns
## Choose the Right Level of Architecture
Architecture complexity MUST match project scope — don't over-architect small projects. When starting a new project, ask the developer what architecture they prefer:
| Project Size | Recommended Approach |
| --- | --- |
| Script / small CLI (<500 lines) | Flat `main.go` + a few files, no layers |
| Medium service (500-5K lines) | Simple layered: `handler/`, `service/`, `repository/` |
| Large service / monolith (5K+ lines) | Clean architecture, hexagonal, or DDD — ask the team |
A 100-line CLI does not need a domain layer, ports and adapters, or dependency injection frameworks. Start simple and refactor when complexity demands it.
## Keep Domain Pure
Domain logic MUST remain pure — no framework or infrastructure dependencies. The domain layer contains business logic and types:
```go
// domain/order.go — pure business logic, no imports from infrastructure
package domain
type Order struct {
ID string
Items []Item
Status OrderStatus
}
func (o *Order) AddItem(item Item) error {
if o.Status != StatusDraft {
return ErrOrderNotEditable
}
o.Items = append(o.Items, item)
return nil
}
```
Infrastructure concerns (database queries, HTTP clients, message queues) live in separate packages that depend on the domain — never the reverse.
## Fail Fast — Validate at Boundaries
Input MUST be validated at system boundaries (HTTP handlers, CLI argument parsing, message consumers). Once data enters your domain layer, trust it:
```go
// Handler layer — validate here
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
var req CreateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
if req.UserID == "" {
http.Error(w, "user_id is required", http.StatusBadRequest)
return
}
if len(req.Items) == 0 {
http.Error(w, "at least one item required", http.StatusBadRequest)
return
}
// Domain layer trusts this data is valid
order, err := h.service.CreateOrder(r.Context(), req.UserID, req.Items)
// ...
}
```
Don't re-validate the same data at every layer — it clutters the code and violates DRY.
## Make Illegal States Unrepresentable
Use Go's type system to prevent invalid states from being expressible in code:
```go
// Bad — status is a raw string, anything goes
type Order struct {
Status string // "pending"? "PENDING"? "active"? anything?
}
// Good — typed enum constrains the values
type OrderStatus int
const (
OrderStatusUnknown OrderStatus = iota // 0 = invalid
OrderStatusDraft // 1
OrderStatusConfirmed // 2
OrderStatusShipped // 3
)
type Order struct {
Status OrderStatus
}
```
```go
// Bad — email is a raw string, could be anything
func SendEmail(to string, body string) error { ... }
// Good — validated type enforces the constraint
type Email struct {
address string // unexported: can only be created via constructor
}
func NewEmail(raw string) (Email, error) {
if !isValidEmail(raw) {
return Email{}, fmt.Errorf("invalid email: %s", raw)
}
return Email{address: raw}, nil
}
```
## Detailed Architecture Guides
For projects that warrant a formal architecture (typically 5K+ lines), see the dedicated guides:
- [Domain-Driven Design (DDD)](./ddd.md) — aggregates, value objects, bounded contexts
- [Clean Architecture](./clean-architecture.md) — use cases, dependency rule, layered adapters
- [Hexagonal Architecture](./hexagonal-architecture.md) — ports, adapters, domain core isolation
## 12-Factor App Principles
→ See `samber/cc-skills-golang@golang-project-layout` for 12-Factor App conventions.
## Explicit Over Implicit
Go favors explicitness. Code should express its intent clearly without requiring the reader to know hidden conventions:
```go
// Bad — implicit behavior hidden in struct tags and reflection
type Config struct {
Port int `default:"8080"`
}
// Good — explicit defaults visible in code
func NewConfig() Config {
return Config{Port: 8080}
}
```
```go
// Bad — implicit dependency via global
func HandleRequest(w http.ResponseWriter, r *http.Request) {
user := globalDB.FindUser(r.Context(), userID) // where does globalDB come from?
}
// Good — explicit dependency via injection
func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
user := h.db.FindUser(r.Context(), userID) // clear: db is a field on Handler
}
```
→ See `samber/cc-skills-golang@golang-project-layout` skill for directory structure and layout patterns.
@@ -0,0 +1,177 @@
# Clean Architecture in Go
## When to Use
Apply clean architecture when you need strong separation between business logic and infrastructure — typically medium-to-large services (2K+ lines) where testability, framework independence, and clear dependency direction matter. Do NOT use for small CLI tools or scripts.
## The Dependency Rule
Dependencies point inward only. Inner layers never import outer layers.
```
Frameworks & Drivers → Interface Adapters → Use Cases → Entities
(HTTP, DB, gRPC) (handlers, repos) (app logic) (domain)
```
Each layer defines interfaces for what it needs. Outer layers implement those interfaces.
## Project Structure
```
order-service/
├── cmd/
│ └── server/
│ └── main.go # Wiring only — builds the dependency graph
├── internal/
│ ├── entity/
│ │ ├── order.go # Order entity + business rules
│ │ ├── item.go # OrderItem
│ │ └── status.go # OrderStatus enum
│ ├── order/
│ │ ├── place.go # PlaceOrderUseCase
│ │ ├── cancel.go # CancelOrderUseCase
│ │ └── port.go # Interfaces this use case depends on
│ ├── adapter/
│ │ ├── handler/
│ │ │ └── order_handler.go # HTTP handler — calls use cases
│ │ ├── repository/
│ │ │ └── order_postgres.go # OrderRepository — implements port
│ │ └── gateway/
│ │ └── payment_client.go # External payment API client
│ └── infrastructure/
│ ├── router.go # HTTP router setup
│ ├── database.go # DB connection
│ └── config.go # Config loading
├── go.mod
└── go.sum
```
## Code Examples
### Entity — pure domain logic, zero dependencies
```go
// internal/entity/order.go
package entity
type Order struct {
ID string
Items []Item
Status OrderStatus
}
func (o *Order) Cancel() error {
if o.Status == StatusShipped {
return ErrCannotCancelShipped
}
o.Status = StatusCancelled
return nil
}
func (o *Order) Total() int64 {
var sum int64
for _, item := range o.Items {
sum += item.Price * int64(item.Quantity)
}
return sum
}
```
### Use Case — orchestrates business operations
```go
// internal/order/port.go
package order
// Ports — interfaces defined by the use case, implemented by adapters
type OrderRepository interface {
Save(ctx context.Context, order *entity.Order) error
FindByID(ctx context.Context, id string) (*entity.Order, error)
}
type PaymentGateway interface {
Charge(ctx context.Context, orderID string, amount int64) error
}
```
```go
// internal/order/place.go
package order
type PlaceOrderUseCase struct {
orders OrderRepository
payments PaymentGateway
}
func NewPlaceOrderUseCase(orders OrderRepository, payments PaymentGateway) *PlaceOrderUseCase {
return &PlaceOrderUseCase{orders: orders, payments: payments}
}
func (uc *PlaceOrderUseCase) Execute(ctx context.Context, orderID string) error {
order, err := uc.orders.FindByID(ctx, orderID)
if err != nil {
return fmt.Errorf("finding order: %w", err)
}
if err := uc.payments.Charge(ctx, order.ID, order.Total()); err != nil {
return fmt.Errorf("charging payment: %w", err)
}
order.Status = entity.StatusPlaced
return uc.orders.Save(ctx, order)
}
```
### Adapter — implements a port
```go
// internal/adapter/repository/order_postgres.go
package repository
type OrderPostgres struct {
db *sql.DB
}
func NewOrderPostgres(db *sql.DB) *OrderPostgres {
return &OrderPostgres{db: db}
}
func (r *OrderPostgres) FindByID(ctx context.Context, id string) (*entity.Order, error) {
// SQL query, scan into entity.Order
}
func (r *OrderPostgres) Save(ctx context.Context, order *entity.Order) error {
// SQL upsert
}
```
### Handler — translates HTTP to use case calls
```go
// internal/adapter/handler/order_handler.go
package handler
type OrderHandler struct {
placeOrder *usecase.PlaceOrderUseCase
}
func (h *OrderHandler) HandlePlaceOrder(w http.ResponseWriter, r *http.Request) {
orderID := chi.URLParam(r, "id")
if err := h.placeOrder.Execute(r.Context(), orderID); err != nil {
// Map domain errors to HTTP status codes
http.Error(w, err.Error(), mapToHTTPStatus(err))
return
}
w.WriteHeader(http.StatusOK)
}
```
## Key Principle
Interfaces live where they are consumed, not where they are implemented. The `usecase/order/port.go` file defines `OrderRepository` — the adapter in `adapter/repository/` implements it. This keeps the use case layer free from infrastructure imports.
## Wiring
All dependency construction happens in `cmd/server/main.go`. → See `samber/cc-skills-golang@golang-dependency-injection` skill for DI library alternatives.
@@ -0,0 +1,63 @@
# Data Handling Patterns
## Iterators for Large Data (Go 1.23+)
Process large datasets without allocating everything into memory:
```go
// Bad — loads all rows into memory
func AllUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT * FROM users")
// ... scan all into slice
}
// Good — iterator yields one at a time
func AllUsers(db *sql.DB) iter.Seq2[User, error] {
return func(yield func(User, error) bool) {
rows, err := db.Query("SELECT * FROM users")
if err != nil {
yield(User{}, err)
return
}
defer rows.Close()
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
yield(User{}, err)
return
}
if !yield(u, nil) {
return
}
}
}
}
```
## Streaming Large Transfers
When transferring large data between services (e.g., 1M rows from DB, 1M rows in HTTP response), use streaming patterns with iterators or `github.com/samber/ro` to prevent OOM:
```go
// Stream JSON array to HTTP response — constant memory
func (h *Handler) ExportUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("["))
first := true
for user, err := range h.repo.AllUsers(r.Context()) {
if err != nil {
slog.Error("streaming user", "error", err)
return
}
if !first {
w.Write([]byte(","))
}
json.NewEncoder(w).Encode(user)
first = false
}
w.Write([]byte("]"))
}
```
@@ -0,0 +1,208 @@
# Domain-Driven Design (DDD) in Go
## When to Use
Apply DDD when the business domain is complex enough that the code structure should mirror the business model — typically services with 5K+ lines, multiple bounded contexts, or rich business rules. Do NOT use for simple CRUD apps or CLI tools.
## Building Blocks
| Concept | Go Mapping | Purpose |
| --- | --- | --- |
| **Entity** | Struct with identity field | Has unique ID, mutable state, lifecycle |
| **Value Object** | Immutable struct, compared by value | No identity — represents a measurement, quantity, or descriptor |
| **Aggregate** | Entity + child entities/value objects | Consistency boundary — all mutations go through the root |
| **Repository** | Interface in domain, impl in infrastructure | Persistence abstraction for aggregates |
| **Domain Service** | Function or struct in domain package | Logic that spans multiple aggregates |
| **Domain Event** | Struct describing a fact that happened | Decouples bounded contexts |
## Project Structure
Organize by **bounded context**, grouping domain, application, and adapters vertically. This scales across multiple contexts and clarifies ownership.
```
order-service/
├── cmd/
│ └── server/
│ └── main.go # Wiring only
├── internal/
│ ├── order/ # Bounded context: Order
│ │ ├── domain/
│ │ │ ├── order.go # Order aggregate root
│ │ │ ├── item.go # OrderItem entity
│ │ │ ├── status.go # OrderStatus enum
│ │ │ ├── repository.go # OrderRepository interface
│ │ │ └── events.go # OrderPlaced, OrderShipped events
│ │ ├── application/
│ │ │ ├── place_order.go # PlaceOrderHandler (command)
│ │ │ └── get_order.go # GetOrderHandler (query)
│ │ └── adapters/
│ │ ├── persistence/
│ │ │ └── postgres.go # OrderRepository implementation
│ │ └── http/
│ │ └── handler.go # HTTP transport
│ ├── billing/ # Bounded context: Billing (another example)
│ │ ├── domain/
│ │ ├── application/
│ │ └── adapters/
│ ├── shared/
│ │ └── money.go # Value object reused across contexts
│ └── events/
│ └── publisher.go # Shared event bus (infrastructure)
├── go.mod
└── go.sum
```
**Key principles:**
- Group each bounded context **vertically** (domain → application → adapters), not by technical role
- Use `adapters/` instead of `infrastructure/` to be explicit about Hexagonal Architecture
- Make cross-context boundaries explicit (see **Bounded Contexts** section below)
- Place shared infrastructure (event bus, logging) at `internal/{shared}/` or `internal/events/`
## Code Examples
### Value Object — Money
```go
// internal/domain/shared/money.go
package shared
type Money struct {
amount int64 // cents — avoids float precision issues
currency string
}
func NewMoney(amount int64, currency string) (Money, error) {
if currency == "" {
return Money{}, errors.New("currency is required")
}
return Money{amount: amount, currency: currency}, nil
}
func (m Money) Add(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, fmt.Errorf("cannot add %s to %s", other.currency, m.currency)
}
return Money{amount: m.amount + other.amount, currency: m.currency}, nil
}
```
### Aggregate Root — Order
```go
// internal/domain/order/order.go
package order
type Order struct {
id string
items []Item
status Status
total shared.Money
}
func NewOrder(id string) *Order {
return &Order{id: id, status: StatusDraft}
}
// All mutations go through the aggregate root
func (o *Order) AddItem(item Item) error {
if o.status != StatusDraft {
return ErrOrderNotEditable
}
o.items = append(o.items, item)
return o.recalculateTotal()
}
func (o *Order) Place() (OrderPlaced, error) {
if len(o.items) == 0 {
return OrderPlaced{}, ErrEmptyOrder
}
o.status = StatusPlaced
return OrderPlaced{OrderID: o.id, Total: o.total}, nil
}
```
### Repository Interface — defined in domain
```go
// internal/order/domain/repository.go
package domain
type Repository interface {
Save(ctx context.Context, order *Order) error
FindByID(ctx context.Context, id string) (*Order, error)
}
```
The implementation lives in `internal/order/adapters/persistence/postgres.go` and depends on the domain — never the reverse.
### Application Service — orchestrates a use case
```go
// internal/order/application/place_order.go
package application
import (
"context"
"fmt"
"myapp/internal/order/domain"
)
type PlaceOrderHandler struct {
orders domain.Repository
events EventPublisher
}
func (h *PlaceOrderHandler) Handle(ctx context.Context, cmd PlaceOrderCommand) error {
order, err := h.orders.FindByID(ctx, cmd.OrderID)
if err != nil {
return fmt.Errorf("finding order: %w", err)
}
evt, err := order.Place()
if err != nil {
return fmt.Errorf("placing order: %w", err)
}
if err := h.orders.Save(ctx, order); err != nil {
return fmt.Errorf("saving order: %w", err)
}
return h.events.Publish(ctx, evt)
}
```
## Bounded Contexts
Each bounded context maps to a top-level package under `internal/` with its own domain, application, and adapters. Contexts communicate through domain events or explicit anti-corruption layers — never by importing each other's internal types directly.
**Anti-corruption layer example:** If `billing/` needs to consume an `order.OrderPlaced` event, translate it to a billing-specific type:
```go
// internal/billing/adapters/events/order_events.go
package events
import (
"myapp/internal/events"
"myapp/internal/billing/domain"
)
type OrderPlacedSubscriber struct {
invoices domain.InvoiceRepository
}
// Receives order.OrderPlaced, translates to billing domain
func (s *OrderPlacedSubscriber) OnOrderPlaced(evt events.OrderPlaced) error {
// Translate and create invoice
return s.invoices.Create(evt.OrderID, evt.Total)
}
```
This prevents billing from depending on order's internal types.
For large systems, each context can be its own Go module in a workspace (`go.work`). See the `samber/cc-skills-golang@golang-project-layout` skill for workspace setup.
## Wiring
Wire dependencies in `cmd/server/main.go` using manual constructor injection. → See `samber/cc-skills-golang@golang-dependency-injection` skill for DI library alternatives.
@@ -0,0 +1,194 @@
# Hexagonal Architecture (Ports & Adapters) in Go
## When to Use
Apply hexagonal architecture when a service interacts with multiple external systems (databases, APIs, message queues, caches) and you want the domain logic fully decoupled from all of them. Particularly effective when the same business logic needs multiple entry points (HTTP, gRPC, CLI, message consumer). Do NOT use for simple CRUD apps or libraries.
## Core Concepts
- **Domain** — Business logic and types. No external dependencies.
- **Ports** — Interfaces that define how the domain interacts with the outside world.
- **Primary (driving) ports**: How the outside world calls into the domain (e.g., `OrderService` interface).
- **Secondary (driven) ports**: How the domain calls out to infrastructure (e.g., `OrderRepository`, `PaymentGateway` interfaces).
- **Adapters** — Concrete implementations of ports.
- **Primary adapters**: HTTP handlers, gRPC servers, CLI commands — they call primary ports.
- **Secondary adapters**: PostgreSQL repository, Stripe client, Redis cache — they implement secondary ports.
## Project Structure
```
order-service/
├── cmd/
│ ├── server/
│ │ └── main.go # HTTP server wiring
│ └── worker/
│ └── main.go # Message consumer wiring
├── internal/
│ ├── domain/
│ │ ├── order.go # Order entity + business rules
│ │ ├── item.go # OrderItem
│ │ └── status.go # OrderStatus enum
│ ├── port/
│ │ ├── incoming.go # Primary ports (OrderService interface)
│ │ └── outgoing.go # Secondary ports (OrderRepository, PaymentGateway)
│ ├── service/
│ │ └── order_service.go # Implements primary ports — orchestrates domain + secondary ports
│ └── adapter/
│ ├── primary/
│ │ ├── http/
│ │ │ ├── router.go
│ │ │ └── order_handler.go # HTTP adapter — calls OrderService
│ │ └── grpc/
│ │ └── order_server.go # gRPC adapter — calls OrderService
│ └── secondary/
│ ├── postgres/
│ │ └── order_repo.go # Implements OrderRepository
│ └── stripe/
│ └── payment.go # Implements PaymentGateway
├── go.mod
└── go.sum
```
## Code Examples
### Domain — pure business logic
```go
// internal/domain/order.go
package domain
type Order struct {
ID string
Items []Item
Status OrderStatus
}
func (o *Order) Ship() error {
if o.Status != StatusPaid {
return ErrOrderNotPaid
}
o.Status = StatusShipped
return nil
}
```
### Ports — interfaces defined separately from implementations
```go
// internal/port/incoming.go
package port
// Primary port — how the outside world drives the application
type OrderService interface {
PlaceOrder(ctx context.Context, items []domain.Item) (string, error)
ShipOrder(ctx context.Context, orderID string) error
GetOrder(ctx context.Context, orderID string) (*domain.Order, error)
}
```
```go
// internal/port/outgoing.go
package port
// Secondary ports — how the application reaches external systems
type OrderRepository interface {
Save(ctx context.Context, order *domain.Order) error
FindByID(ctx context.Context, id string) (*domain.Order, error)
}
type PaymentGateway interface {
Charge(ctx context.Context, orderID string, amount int64) error
}
```
### Service — implements primary port, depends on secondary ports
```go
// internal/service/order_service.go
package service
type orderService struct {
orders port.OrderRepository
payments port.PaymentGateway
}
func NewOrderService(orders port.OrderRepository, payments port.PaymentGateway) port.OrderService {
return &orderService{orders: orders, payments: payments}
}
func (s *orderService) PlaceOrder(ctx context.Context, items []domain.Item) (string, error) {
order := domain.NewOrder(items)
if err := s.payments.Charge(ctx, order.ID, order.Total()); err != nil {
return "", fmt.Errorf("charging payment: %w", err)
}
if err := s.orders.Save(ctx, order); err != nil {
return "", fmt.Errorf("saving order: %w", err)
}
return order.ID, nil
}
```
### Primary Adapter — HTTP handler calls the service port
```go
// internal/adapter/primary/http/order_handler.go
package http
type OrderHandler struct {
svc port.OrderService
}
func NewOrderHandler(svc port.OrderService) *OrderHandler {
return &OrderHandler{svc: svc}
}
func (h *OrderHandler) HandlePlaceOrder(w http.ResponseWriter, r *http.Request) {
var req PlaceOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
id, err := h.svc.PlaceOrder(r.Context(), req.Items)
if err != nil {
http.Error(w, err.Error(), mapToHTTPStatus(err))
return
}
json.NewEncoder(w).Encode(map[string]string{"id": id})
}
```
### Secondary Adapter — implements a driven port
```go
// internal/adapter/secondary/postgres/order_repo.go
package postgres
type OrderRepo struct {
db *sql.DB
}
func NewOrderRepo(db *sql.DB) *OrderRepo {
return &OrderRepo{db: db}
}
func (r *OrderRepo) Save(ctx context.Context, order *domain.Order) error {
// SQL upsert
}
func (r *OrderRepo) FindByID(ctx context.Context, id string) (*domain.Order, error) {
// SQL query
}
```
## Multiple Entry Points
The hexagonal approach shines when the same `OrderService` is called from different primary adapters — HTTP for external clients, gRPC for internal services, a message consumer for async events. Each adapter is wired in its own `cmd/` entry point.
## Wiring
Construct adapters and inject them in `cmd/server/main.go`. → See `samber/cc-skills-golang@golang-dependency-injection` skill for DI library alternatives.
@@ -0,0 +1,154 @@
# Resource Management Patterns
## Defer Close Immediately
`defer Close()` MUST be called immediately after opening — NEVER delay. This prevents leaks when code is modified later and new return paths are added:
```go
// Good — defer is right next to open
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// Bad — Close() is far from Open(), easy to forget when adding early returns
f, err := os.Open(path)
if err != nil {
return err
}
// ... 50 lines of code ...
f.Close() // might never run if a new return is added above
```
This applies to all closeable resources: files, SQL rows, HTTP response bodies, gzip readers, bufio scanners wrapping readers, etc.
```go
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
rows, err := db.QueryContext(ctx, query)
if err != nil {
return err
}
defer rows.Close()
```
## `runtime.AddCleanup` over `runtime.SetFinalizer`
`runtime.AddCleanup` SHOULD be preferred over `runtime.SetFinalizer` (Go 1.24+):
```go
type Resource struct {
handle uintptr
}
func NewResource() *Resource {
r := &Resource{handle: acquireHandle()}
runtime.AddCleanup(r, func(handle uintptr) {
releaseHandle(handle)
}, r.handle)
return r
}
```
`AddCleanup` is preferred because:
- Multiple cleanups can be attached to the same object
- The cleanup function receives a copy of the value, not the object itself — no resurrection risk
- Cleanups run even if the object is part of a cycle
## Resource Pools
Resource pools SHOULD use channels with a fixed capacity for bounded allocation. Use channel-based pools or `sync.Pool` to manage limited resources between consumers. Always set a maximum size:
```go
type ConnPool struct {
conns chan *Conn
}
func NewConnPool(maxSize int, factory func() (*Conn, error)) (*ConnPool, error) {
pool := &ConnPool{
conns: make(chan *Conn, maxSize),
}
// Pre-fill with initial connections
for range maxSize {
conn, err := factory()
if err != nil {
return nil, fmt.Errorf("creating connection: %w", err)
}
pool.conns <- conn
}
return pool, nil
}
func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
select {
case conn := <-p.conns:
return conn, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (p *ConnPool) Put(conn *Conn) {
select {
case p.conns <- conn:
default:
conn.Close() // pool is full, discard
}
}
```
## Graceful Shutdown
Graceful shutdown MUST use `signal.NotifyContext` for clean termination. All resources (connections, files, channels) MUST be drained before process exit. Use `os/signal` and context cancellation:
```go
func main() {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM,
)
defer stop()
srv := &http.Server{Addr: ":8080", Handler: router}
// Start server in background
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("server error", "error", err)
}
}()
slog.Info("server started", "addr", ":8080")
// Wait for interrupt signal
<-ctx.Done()
slog.Info("shutting down...")
// Give outstanding requests time to complete
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
slog.Error("shutdown error", "error", err)
}
// Close other resources: database connections, message queues, etc.
db.Close()
slog.Info("shutdown complete")
}
```
This pattern applies to any long-running service — gRPC servers, message consumers, background workers. The key elements are:
1. Capture OS signals with `signal.NotifyContext`
2. Start the server in a goroutine
3. Block on context cancellation
4. Shut down with a timeout to drain in-flight requests
5. Close all remaining resources in order
For goroutine shutdown patterns, see the `samber/cc-skills-golang@golang-concurrency` skill.