golang — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited golang (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill provides the knowledge and conventions for building production-grade Go REST APIs. It covers project structure, layered architecture (Handler -> Service -> Repository), database access with PostgreSQL, routing with Chi, and all supporting concerns (config, logging, error handling, validation, graceful shutdown). The go-dev agent uses this skill to implement backend API code that integrates with a React frontend.
project-root/
cmd/
server/
main.go # Entry point: config load, DI wiring, server start, graceful shutdown
internal/
config/
config.go # Struct with env tags, Load() function
handler/
health.go # Health check handler
{resource}.go # HTTP handlers per resource (one file per domain entity)
response.go # Shared JSON response helpers (Success, Error, Paginated)
service/
{resource}.go # Business logic per resource
repository/
{resource}.go # Database queries per resource
model/
{resource}.go # Domain structs, DB models
errors.go # Custom error types
middleware/
auth.go # JWT/session auth middleware
cors.go # CORS configuration
logging.go # Request logging middleware
recovery.go # Panic recovery middleware
request_id.go # Request ID injection
router/
router.go # Chi router setup, route registration, middleware chain
migrations/
000001_initial.up.sql # golang-migrate format: {sequence}_{name}.{direction}.sql
000001_initial.down.sql
sqlc/ # Optional: sqlc configuration and generated code
sqlc.yaml
query/
{resource}.sql
db/ # Generated code (do not edit)
go.mod
go.sum
Dockerfile
.env.exampleThe codebase follows strict layering. Dependencies flow inward only:
HTTP Request
-> Middleware (auth, logging, CORS, recovery)
-> Handler (parse request, validate, call service, write response)
-> Service (business logic, orchestrate repos, enforce rules)
-> Repository (database queries, no business logic)
-> PostgreSQLRules:
Every layer uses constructor injection. No global state, no init() for wiring.
// repository/user.go
type UserRepository struct {
db *pgxpool.Pool
}
func NewUserRepository(db *pgxpool.Pool) *UserRepository {
return &UserRepository{db: db}
}
// service/user.go
type UserService struct {
repo *repository.UserRepository
}
func NewUserService(repo *repository.UserRepository) *UserService {
return &UserService{repo: repo}
}
// handler/user.go
type UserHandler struct {
svc *service.UserService
}
func NewUserHandler(svc *service.UserService) *UserHandler {
return &UserHandler{svc: svc}
}Wiring happens in cmd/server/main.go:
db := mustConnectDB(cfg)
userRepo := repository.NewUserRepository(db)
userSvc := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userSvc)
r := router.New(userHandler, ...)Chi is the preferred HTTP router. It is stdlib-compatible (net/http handlers).
package router
import (
"github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware"
)
func New(userH *handler.UserHandler) chi.Router {
r := chi.NewRouter()
// Global middleware stack (order matters)
r.Use(chimw.RequestID)
r.Use(chimw.RealIP)
r.Use(middleware.Logger) // custom structured logging
r.Use(middleware.Recovery) // panic recovery
r.Use(middleware.CORS) // CORS for React frontend
r.Use(chimw.Timeout(30 * time.Second))
// Health check (no auth)
r.Get("/healthz", handler.HealthCheck)
// API routes
r.Route("/api/v1", func(r chi.Router) {
// Public routes
r.Post("/auth/login", authH.Login)
r.Post("/auth/register", authH.Register)
// Protected routes
r.Group(func(r chi.Router) {
r.Use(middleware.Auth)
r.Route("/users", func(r chi.Router) {
r.Get("/", userH.List)
r.Post("/", userH.Create)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", userH.GetByID)
r.Put("/", userH.Update)
r.Delete("/", userH.Delete)
})
})
})
})
return r
}Use pgxpool for connection pooling. Never use database/sql directly for PostgreSQL.
import "github.com/jackc/pgx/v5/pgxpool"
func ConnectDB(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) {
config, err := pgxpool.ParseConfig(databaseURL)
if err != nil {
return nil, fmt.Errorf("parsing database URL: %w", err)
}
config.MaxConns = 25
config.MinConns = 5
config.MaxConnLifetime = time.Hour
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("creating connection pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
return nil, fmt.Errorf("pinging database: %w", err)
}
return pool, nil
}Query option A: sqlc (preferred for type safety)
Define SQL in .sql files, generate Go code:
-- sqlc/query/user.sql
-- name: GetUserByID :one
SELECT id, email, name, created_at FROM users WHERE id = $1;
-- name: ListUsers :many
SELECT id, email, name, created_at FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2;
-- name: CreateUser :one
INSERT INTO users (email, name, password_hash) VALUES ($1, $2, $3) RETURNING id, email, name, created_at;Query option B: GORM
Use only when project already has GORM or PRD specifies it. Prefer sqlc/pgx otherwise.
Use golang-migrate with file-based migrations.
migrate create -ext sql -dir migrations -seq {name}
migrate -path migrations -database "$DATABASE_URL" up
migrate -path migrations -database "$DATABASE_URL" down 1Migration files are pure SQL. One up file, one down file per migration. Down migrations must be reversible.
Define domain error types in internal/model/errors.go:
package model
import "errors"
var (
ErrNotFound = errors.New("resource not found")
ErrConflict = errors.New("resource already exists")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
ErrBadRequest = errors.New("bad request")
ErrInternal = errors.New("internal server error")
)
// ValidationError carries field-level validation details.
type ValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
}
type ValidationErrors []ValidationError
func (ve ValidationErrors) Error() string {
return fmt.Sprintf("%d validation error(s)", len(ve))
}Always wrap errors with context using fmt.Errorf("doing thing: %w", err). Handlers map domain errors to HTTP status codes:
func mapErrorToStatus(err error) int {
switch {
case errors.Is(err, model.ErrNotFound):
return http.StatusNotFound
case errors.Is(err, model.ErrConflict):
return http.StatusConflict
case errors.Is(err, model.ErrUnauthorized):
return http.StatusUnauthorized
case errors.Is(err, model.ErrForbidden):
return http.StatusForbidden
case errors.Is(err, model.ErrBadRequest):
return http.StatusBadRequest
default:
return http.StatusInternalServerError
}
}Use environment variables. Load with envconfig or viper.
package config
import "github.com/kelseyhightower/envconfig"
type Config struct {
Port int `envconfig:"PORT" default:"8080"`
DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
JWTSecret string `envconfig:"JWT_SECRET" required:"true"`
CORSOrigins string `envconfig:"CORS_ORIGINS" default:"http://localhost:3000"`
LogLevel string `envconfig:"LOG_LEVEL" default:"info"`
Environment string `envconfig:"ENVIRONMENT" default:"development"`
}
func Load() (*Config, error) {
var cfg Config
if err := envconfig.Process("", &cfg); err != nil {
return nil, fmt.Errorf("loading config: %w", err)
}
return &cfg, nil
}Use dedicated request and response structs. Never bind directly to domain models.
// handler/user.go
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email"`
Name string `json:"name" validate:"required,min=2,max=100"`
Password string `json:"password" validate:"required,min=8"`
}
type UserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
}Validate with go-playground/validator:
import "github.com/go-playground/validator/v10"
var validate = validator.New()
func decodeAndValidate[T any](r *http.Request) (T, error) {
var req T
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return req, fmt.Errorf("%w: %s", model.ErrBadRequest, err.Error())
}
if err := validate.Struct(req); err != nil {
return req, toValidationErrors(err)
}
return req, nil
}Use log/slog (stdlib, Go 1.21+). No external logging libraries needed.
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
slog.Info("server starting", "port", cfg.Port, "env", cfg.Environment)
slog.Error("query failed", "err", err, "user_id", userID)func main() {
// ... setup ...
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Port),
Handler: r,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
slog.Info("server listening", "addr", srv.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("server error", "err", err)
os.Exit(1)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("shutting down server")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("server shutdown error", "err", err)
}
pool.Close()
slog.Info("server stopped")
}FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /server .
COPY migrations/ ./migrations/
EXPOSE 8080
CMD ["./server"]handler not handlers.user_repository.go.UserReader, Authenticator.NewXxx pattern.ErrXxx pattern.ctx.github.com/{org}/{repo}.go mod tidy after adding dependencies.go get package@latest for updates.Run golangci-lint run ./... before reporting. Key linters:
errcheck: all errors must be handled.govet: catches common mistakes.staticcheck: advanced static analysis.unused: no dead code.{name}_test.go in same package.testify/assert or stdlib testing only.pgxmock.httptest.NewRecorder() and httptest.NewRequest().// FunctionName does X. (starts with the name).doc.go or the primary file).references/. Changes to SKILL.md or scripts/ require user approval.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.