pkg

module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 21, 2026 License: MIT

README

Go Package Collection

Go Reference Test Go Report Card

A collection of production-ready Go packages for building web services: database, Redis (standard + cluster), JWT, crypto, GCS, structured logging, HTTP middleware, Prometheus metrics, graceful shutdown, and utilities. Follows clean architecture boundaries — domain, use-case, and infrastructure are separate.

Contents


Installation

go get github.com/turahe/pkg

Requires Go 1.21+.


Packages

config

Loads all configuration from environment variables (or a .env file via godotenv). All packages read from the global Configuration singleton.

API:

config.Setup(configPath string) error   // load .env; "" uses env vars only
config.GetConfig() *Configuration       // get global config (builds from env if needed)
config.SetConfig(cfg *Configuration)    // override config (testing / manual wiring)

Configuration struct (abbreviated):

type Configuration struct {
    Server      ServerConfiguration
    Cors        CorsConfiguration
    Database    DatabaseConfiguration
    DatabaseSite DatabaseConfiguration  // optional second DB
    Redis       RedisConfiguration
    GCS         GCSConfiguration
    RateLimiter RateLimiterConfiguration
    Timezone    TimezoneConfiguration
}

database

Enterprise-grade GORM database layer. Supports MySQL, Postgres, SQLite, SQL Server, and Google Cloud SQL (Postgres/MySQL) with IAM auth and Private IP. Includes SQL redaction in logs (passwords, tokens, card numbers).

Drivers: mysql · postgres · sqlite · sqlserver · cloudsql-mysql · cloudsql-postgres

New API (recommended — dependency injection):

db, err := database.New(&cfg.Database, database.Options{
    LogLevel: logger.Warn,
})
// or with Cloud SQL options:
db, err := database.New(&cfg.Database, database.Options{
    UseIAM:       true,
    UsePrivateIP: true,
    LogLevel:     logger.Warn,
})

gormDB  := db.DB()                    // *gorm.DB
err     = db.Health(ctx)             // ping with PingTimeout-bounded context
err     = db.Close()                  // close connections + Cloud SQL connector

Options:

Option Default Description
MaxOpenConns 30 Max open DB connections
MaxIdleConns 10 Max idle DB connections
ConnMaxLife 30 min Max connection lifetime
ConnMaxIdle 10 min Max connection idle time
SlowThreshold 500 ms GORM slow query log threshold
PingTimeout 5 s Health check ping timeout
LogLevel Warn GORM log level
UseIAM false Cloud SQL IAM auth
UsePrivateIP false Cloud SQL Private IP

High-RPS pool preset:

db, err := database.New(&cfg.Database, database.Options{}, database.WithProductionPoolDefaults)
// sets MaxOpenConns=150, MaxIdleConns=50

Legacy API (global, backward-compatible):

database.Setup()          // init from config env vars
database.GetDB()          // *gorm.DB (panics if not initialized)
database.GetDBSite()      // secondary *gorm.DB (falls back to primary)
database.HealthCheck(ctx) // check both primary and site DB
database.IsAlive()        // bool
database.Cleanup()        // close all connections

redis

Redis client wrapper for both standalone Redis and Redis Cluster (Google Cloud Memorystore). Connection pooling, timeouts, and all common commands included.

Setup:

redis.Setup() error          // reads config; no-op if REDIS_ENABLED=false
redis.Close() error          // close client; call on shutdown
redis.IsAlive() bool         // ping check
redis.GetUniversalClient() redis.Cmdable  // works with both modes
redis.GetRedis() *redis.Client            // standard mode only
redis.GetRedisCluster() *redis.ClusterClient  // cluster mode only

String operations:

redis.Get(key string) (string, error)
redis.Set(key string, value interface{}, expiration time.Duration) error
redis.Delete(keys ...string) error
redis.MGet(keys ...string) ([]interface{}, error)
redis.MSet(pairs ...interface{}) error

Hash operations:

redis.HGet(key, field string) (string, error)
redis.HGetAll(key string) (map[string]string, error)
redis.HSet(key, field string, value interface{}) error
redis.HSetMap(key string, fields map[string]interface{}) error

List operations:

redis.LPush(key string, values ...interface{}) error
redis.RPop(key string) (string, error)
redis.LRange(key string, start, stop int64) ([]string, error)

Set operations:

redis.SAdd(key string, members ...interface{}) error
redis.SMembers(key string) ([]string, error)
redis.SRem(key string, members ...interface{}) error

Distributed lock:

redis.AcquireLock(key string, expiration time.Duration) (bool, error)
redis.ExtendLock(key string, expiration time.Duration) (bool, error)
redis.ReleaseLock(key string) error

Pipeline, Pub/Sub, Scan:

redis.Pipeline(fn func(redis.Pipeliner) error) error
redis.PipelineSet(pairs map[string]interface{}, expiration time.Duration) error
redis.PublishMessage(channel string, message interface{}) error
redis.SubscribeToChannel(channel string) *redis.PubSub
redis.ScanKeys(pattern string, count int64) ([]string, error)  // cluster-aware

logger

Structured logging built on log/slog. Outputs Google Cloud Logging-compatible JSON with severity, time, message, trace_id, correlation_id, sourceLocation, and optional fields. The underlying writer is lazy-initialized on first log write (no I/O at startup).

Plain functions:

logger.Debugf(format string, args ...interface{})
logger.Infof(format string, args ...interface{})
logger.Warnf(format string, args ...interface{})
logger.Errorf(format string, args ...interface{})
logger.Fatalf(format string, args ...interface{})  // calls os.Exit(1)

Structured fields:

logger.Debug(msg string, fields logger.Fields)
logger.Info(msg string, fields logger.Fields)
logger.Warn(msg string, fields logger.Fields)
logger.Error(msg string, fields logger.Fields)

Context-bound (includes trace_id / correlation_id automatically):

// Store IDs in context (done by RequestID / TraceMiddleware):
ctx = logger.WithTraceID(ctx, traceID)
ctx = logger.WithCorrelationID(ctx, correlationID)

// Read IDs from context:
logger.GetTraceID(ctx)
logger.GetCorrelationID(ctx)

// Context-bound logger — all log calls carry the IDs from ctx:
log := logger.WithContext(ctx)
log.Infof("user %s logged in", userID)
log.Errorf("operation failed: %v", err)

// Or use context-aware free functions:
logger.InfofContext(ctx, "processed %d records", n)
logger.ErrorfContext(ctx, "failed: %v", err)

Configuration:

logger.SetLogLevel(slog.LevelDebug)  // default: Info
logger.GetLogger() *slog.Logger
logger.GetWriter() io.Writer         // file + stderr multi-writer

middlewares

Gin middleware collection. Designed to be composed in a specific order for correct behaviour.

Recommended middleware order:

router := gin.New()
router.Use(
    middlewares.RecoveryHandler,              // 1. catch panics first
    middlewares.RequestID(),                  // 2. inject trace/correlation IDs
    middlewares.LoggerMiddleware(),           // 3. log with IDs in context
    middlewares.Metrics(),                    // 4. Prometheus instrumentation
    middlewares.RequestTimeout(10*time.Second), // 5. bound all downstream handlers
    middlewares.CORS(),                       // 6. CORS headers
    middlewares.AuthMiddleware(),             // 7. JWT auth (protected routes)
    middlewares.RateLimiter(),               // 8. rate limit (requires Redis)
)
router.NoMethod(middlewares.NoMethodHandler())
router.NoRoute(middlewares.NoRouteHandler())

Reference:

Middleware Signature Description
RecoveryHandler gin.HandlerFunc Catches panics; returns structured JSON 500 with stack trace in logs
RequestID() gin.HandlerFunc Reads X-Request-ID / X-Trace-ID or generates UUID; injects into context and response headers
TraceMiddleware() gin.HandlerFunc Reads X-Trace-Id, X-Correlation-Id, X-Request-Id separately; use when upstream sends distinct trace and correlation IDs
LoggerMiddleware() gin.HandlerFunc Structured request log (method, path, status, latency, IP, user-agent, trace IDs)
Metrics() gin.HandlerFunc Prometheus counters, histogram, and in-flight gauge; uses route pattern to avoid high-cardinality labels
RequestTimeout(d) gin.HandlerFunc Adds context.WithTimeout to every request; no-op when d <= 0
CORS() gin.HandlerFunc CORS headers; global or per-origin from config
AuthMiddleware() gin.HandlerFunc Validates Bearer JWT; sets user_id in Gin context
RateLimiter() gin.HandlerFunc Redis Lua single-round-trip rate limiter; sets X-RateLimit-* headers; supports IP or user keying and skip-paths
NoMethodHandler() gin.HandlerFunc 405 JSON response
NoRouteHandler() gin.HandlerFunc 404 JSON response

Prometheus metrics exposed by Metrics():

Metric Type Labels
http_requests_total Counter method, path, status
http_request_duration_seconds Histogram method, path, status
http_requests_in_flight Gauge

Register the scrape endpoint separately:

import "github.com/prometheus/client_golang/prometheus/promhttp"
router.GET("/metrics", gin.WrapH(promhttp.Handler()))

handler

Base handler for Gin with binding, pagination, error routing, and role checks.

type BaseHandler struct{}

// Bind request body / query / URI by content-type and method.
func (c *BaseHandler) ValidateReqParams(ctx *gin.Context, params interface{}) error

// 422 validation error response (Laravel-style field map).
func (c *BaseHandler) HandleValidationError(ctx *gin.Context, serviceCode string, err error)

// Clamp page/size to defaults (1, 10) and max (100).
func (c *BaseHandler) NormalizePagination(pageNumber, pageSize int) (int, int)

// Extract and validate URL param; writes 422 and returns false if missing.
func (c *BaseHandler) GetIDFromParam(ctx *gin.Context, paramName, serviceCode string) (string, bool)

// Prefer reqID; fall back to URL param.
func (c *BaseHandler) GetIDFromRequestOrParam(ctx *gin.Context, reqID, paramName, serviceCode string) (string, bool)

// Route domain errors to correct HTTP status. Returns true if handled.
// Checks errors.Is(ErrNotFound) → 404, errors.Is(ErrUnauthorized) → 401,
// then notFoundMessages list, then falls back to 500.
func (c *BaseHandler) HandleServiceError(ctx *gin.Context, serviceCode string, err error, notFoundMessages ...string) bool

// Build SimplePaginationResponse from a slice, page info, and total.
func (c *BaseHandler) BuildPaginationResponse(data []interface{}, pageNumber, pageSize int, total int64) response.SimplePaginationResponse

// Read "user_id" string from Gin context (set by AuthMiddleware).
func (c *BaseHandler) GetCurrentUserID(ctx *gin.Context) (string, bool)

// Return true if any userRole matches any requiredRole.
func (c *BaseHandler) CheckUserHasRole(userRoles, requiredRoles []string) bool

response

Standardised JSON responses with a 7-digit composite code (HTTP(3) + Service(2) + Case(2)).

Success responses:

response.Ok(ctx)
response.OkWithMessage(ctx, message)
response.OkWithData(ctx, data)
response.Created(ctx, data)
response.Updated(ctx, data)
response.Deleted(ctx)

Error responses:

response.FailWithMessage(ctx, message)
response.FailWithDetailed(ctx, httpStatus, serviceCode, caseCode, data, message)
response.ValidationError(ctx, serviceCode, err)          // 422 with field map
response.ValidationErrorSimple(ctx, serviceCode, field, message)
response.NotFoundError(ctx, serviceCode, caseCode, message)   // 404
response.UnauthorizedError(ctx, message)                       // 401
response.ForbiddenError(ctx, message)                          // 403
response.ConflictError(ctx, serviceCode, caseCode, message)    // 409

Pagination responses:

response.SimplePaginated(ctx, data, pageNumber, pageSize, hasNext, hasPrev)
response.CursorPaginated(ctx, data, nextCursor, hasNext)

Service codes: ServiceCodeCommon, ServiceCodeAuth, ServiceCodeTransaction, ServiceCodeWallet, ServiceCodeUser, ServiceCodeAdmin, ServiceCodeMerchant, ServiceCodeRole, ServiceCodePermission, ServiceCodeNotification, ServiceCodeApiKey, ServiceCodeDeposit, and more.


jwt

HS256 JWT tokens with configurable expiry.

jwt.Init()                                              // reads SERVER_SECRET from config
jwt.GenerateToken(id uuid.UUID, email, name string) (string, error)
jwt.GenerateTokenWithExpiry(id uuid.UUID, email, name string, expiry time.Duration) (string, error)
jwt.GenerateRefreshToken(id uuid.UUID, email, name string) (string, error)
jwt.ValidateToken(tokenString string) (*Claims, error)  // returns Claims{UUID string}
jwt.ComparePassword(hashed, plain string) bool          // bcrypt comparison

crypto

bcrypt password hashing.

crypto.HashAndSalt(plainPassword []byte) string
crypto.ComparePassword(hashedPassword string, plainPassword []byte) bool

gcs

Google Cloud Storage wrapper. Uses Application Default Credentials (ADC) or an explicit service account JSON file.

gcs.Setup() error
gcs.GetClient() *storage.Client
gcs.GetBucket() *storage.BucketHandle
gcs.ReadObject(objectName string) ([]byte, error)
gcs.ReadObjectAsReader(objectName string) (io.ReadCloser, error)
gcs.WriteObject(objectName string, data []byte, contentType string) error
gcs.DeleteObject(objectName string) error
gcs.ObjectExists(objectName string) (bool, error)
gcs.ListObjects(prefix string) ([]string, error)
gcs.Close() error

types
// Conditions is the WHERE clause map passed to repository methods.
type Conditions map[string]interface{}

// TimeRange holds a start/end pair (IANA or RFC3339 strings).
type TimeRange struct {
    Start string
    End   string
}

util
util.IsEmpty(value interface{}) bool
util.InAnySlice[T comparable](haystack []T, needle T) bool
util.RemoveDuplicates[T comparable](haystack []T) []T
util.FormatPhoneNumber(number, defaultRegion string) (string, error)  // E.164

Usage

Minimal server
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "sync/atomic"
    "syscall"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "github.com/turahe/pkg/config"
    "github.com/turahe/pkg/database"
    "github.com/turahe/pkg/middlewares"
    pkgredis "github.com/turahe/pkg/redis"
    "gorm.io/gorm/logger"
)

func main() {
    cfg := config.GetConfig()

    // Database
    db, err := database.New(&cfg.Database, database.Options{LogLevel: logger.Warn})
    if err != nil {
        log.Fatal(err)
    }

    // Redis (optional)
    if cfg.Redis.Enabled {
        if err := pkgredis.Setup(); err != nil {
            log.Printf("redis: %v", err)
        }
    }

    // Readiness gate
    var ready atomic.Bool
    ready.Store(true)

    // Router
    router := gin.New()
    router.Use(
        middlewares.RecoveryHandler,
        middlewares.RequestID(),
        middlewares.LoggerMiddleware(),
        middlewares.Metrics(),
        middlewares.RequestTimeout(10*time.Second),
    )
    router.GET("/metrics", gin.WrapH(promhttp.Handler()))
    router.GET("/live", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })
    router.GET("/ready", func(c *gin.Context) {
        if !ready.Load() {
            c.JSON(http.StatusServiceUnavailable, gin.H{"status": "shutting_down"})
            return
        }
        if err := db.Health(c.Request.Context()); err != nil {
            c.JSON(http.StatusServiceUnavailable, gin.H{"status": "degraded", "error": err.Error()})
            return
        }
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

    // Application routes ...
    router.NoMethod(middlewares.NoMethodHandler())
    router.NoRoute(middlewares.NoRouteHandler())

    srv := &http.Server{
        Addr:              ":" + cfg.Server.Port,
        Handler:           router,
        ReadHeaderTimeout: 5 * time.Second,
        ReadTimeout:       10 * time.Second,
        WriteTimeout:      10 * time.Second,
        IdleTimeout:       60 * time.Second,
    }
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Printf("server: %v", err)
        }
    }()

    // Graceful shutdown (25 s matches terminationGracePeriodSeconds: 30)
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
    <-sig
    ready.Store(false) // signal readiness probe → 503 immediately

    shutCtx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
    defer cancel()
    srv.Shutdown(shutCtx)

    if cfg.Redis.Enabled {
        pkgredis.Close()
    }
    db.Close()
}
Repository with dependency injection
import (
    "github.com/turahe/pkg/database"
    "github.com/turahe/pkg/repositories"
    "github.com/turahe/pkg/types"
)

db, _ := database.New(&cfg.Database, database.Options{})
repo := repositories.NewBaseRepositoryWithDB(db.DB())

// Create
err := repo.Create(ctx, &myModel)

// Find with conditions
var results []MyModel
err = repo.Find(ctx, &results, types.Conditions{
    "status = ?": "active",
    "tenant_id = ?": tenantID,
})

// Pagination (pass preload names to avoid N+1)
total, err := repo.SimplePagination(ctx, &MyModel{}, &results, page, size,
    types.Conditions{"status = ?": "active"},
    []string{"created_at DESC"},
    "User", "Items",  // preloads
)
Clean architecture wiring
// domain/port/repository.go
type ItemRepository interface {
    GetByID(ctx context.Context, id string) (*Item, bool, error)
}

// infrastructure/repository/item.go
type itemRepo struct{ base repositories.IBaseRepository }

func (r *itemRepo) GetByID(ctx context.Context, id string) (*Item, bool, error) {
    var item Item
    notFound, err := r.base.First(ctx, &item, types.Conditions{"id = ?": id})
    return &item, !notFound, err
}

// usecase/get_item.go
func GetItem(ctx context.Context, repo port.ItemRepository, id string) (*Item, error) {
    item, found, err := repo.GetByID(ctx, id)
    if err != nil { return nil, err }
    if !found { return nil, domain.ErrNotFound }
    return item, nil
}

Environment Variables

Copy env.example to .env and call config.Setup(""):

cp env.example .env
Server
Variable Default Description
SERVER_PORT 8080 HTTP listen port
SERVER_SECRET JWT signing secret (required)
SERVER_MODE debug Gin mode: debug, release, test
SERVER_ACCESS_TOKEN_EXPIRY 1 Access token lifetime (hours)
SERVER_REFRESH_TOKEN_EXPIRY 7 Refresh token lifetime (days)
SERVER_SESSION_EXPIRY 24 Session lifetime (hours)
CORS_GLOBAL true Allow all origins
Database
Variable Default Description
DATABASE_DRIVER mysql mysql · postgres · sqlite · sqlserver · cloudsql-mysql · cloudsql-postgres
DATABASE_HOST 127.0.0.1
DATABASE_PORT 3306
DATABASE_USERNAME
DATABASE_PASSWORD
DATABASE_DBNAME
DATABASE_SSLMODE false
DATABASE_LOGMODE false Enable GORM query logging
DATABASE_MAX_IDLE_CONNS 0 (→ 10) Connection pool idle size
DATABASE_MAX_OPEN_CONNS 0 (→ 30) Connection pool max open
DATABASE_CONN_MAX_LIFETIME 0 (→ 30 min) Connection lifetime (minutes)
DATABASE_CLOUD_SQL_INSTANCE project:region:instance for Cloud SQL
DATABASE_*_SITE Same keys with _SITE suffix for secondary DB
Redis
Variable Default Description
REDIS_ENABLED false
REDIS_HOST 127.0.0.1
REDIS_PORT 6379
REDIS_PASSWORD
REDIS_DB 0 Database index (ignored in cluster mode)
REDIS_CLUSTER_MODE false Enable cluster client
REDIS_CLUSTER_NODES Comma-separated host:port list
REDIS_POOL_SIZE 0 (client default) Max connections per node
REDIS_MIN_IDLE_CONNS 0 Min idle connections
REDIS_READ_TIMEOUT_SEC 0 (no timeout) Read timeout
REDIS_WRITE_TIMEOUT_SEC 0 (no timeout) Write timeout
Rate Limiter
Variable Default Description
RATE_LIMITER_ENABLED false Requires Redis
RATE_LIMITER_REQUESTS 100 Requests allowed per window
RATE_LIMITER_WINDOW 60 Window size (seconds)
RATE_LIMITER_KEY_BY ip ip or user
RATE_LIMITER_SKIP_PATHS Comma-separated paths (e.g. /health,/metrics)
GCS
Variable Default Description
GCS_ENABLED false
GCS_BUCKET_NAME
GCS_CREDENTIALS_FILE Path to service account JSON; omit to use ADC

Production Wiring Example

See cmd/example/main.go for a complete wiring of:

  • Dependency-injected database with health check
  • Optional Redis setup and graceful Close()
  • gin.New() with explicit middleware stack (recovery → trace → logging → metrics → timeout)
  • /live (liveness), /ready (readiness with component checks), /metrics (Prometheus)
  • HTTP server with all timeouts set
  • Graceful shutdown: readiness gate → srv.Shutdown(25s) → Redis close → DB close
Kubernetes probes
livenessProbe:
  httpGet:
    path: /live
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

terminationGracePeriodSeconds: 30
Docker
docker build -t myapp .
docker run --env-file .env -p 8080:8080 myapp

The multi-stage Dockerfile uses golang:1.25-alpine to build and gcr.io/distroless/base-debian12:nonroot as the runtime image. Runs as UID 65532 (non-root).


Testing

Run all tests:

go test ./...

With race detector (requires CGO):

CGO_ENABLED=1 go test -race ./...

With coverage:

go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
Integration tests (Redis · MySQL · Postgres)

Start services with Docker Compose:

docker compose up -d

Run with environment variables:

REDIS_ENABLED=true REDIS_HOST=127.0.0.1 REDIS_PORT=6379 \
DATABASE_DRIVER=mysql DATABASE_HOST=127.0.0.1 DATABASE_PORT=3306 \
DATABASE_USERNAME=root DATABASE_PASSWORD=root DATABASE_DBNAME=testdb \
go test ./...

Integration tests skip automatically when services are unavailable. CI runs the full matrix (Go 1.21–1.25.4) with these services via GitHub Actions.

Packages with tests: config, crypto, database, gcs, handler, jwt, logger, middlewares, redis, repositories, response, types, util.


License

MIT

Directories

Path Synopsis
Package domain holds domain errors and ports.
Package domain holds domain errors and ports.
port
Package port defines repository and other ports (interfaces) used by use cases.
Package port defines repository and other ports (interfaces) used by use cases.
Package usecase provides a minimal pattern for application use cases (application service layer).
Package usecase provides a minimal pattern for application use cases (application service layer).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL