Performance
Overview
Performance optimization is crucial for delivering fast, responsive applications. This guide covers techniques for optimizing database queries, implementing efficient caching strategies, and monitoring application performance.
Database Optimization
Query Optimization
// ✅ Efficient - Use specific columns
func (f *GardenFetcher) FindGardenSummaries(ctx context.Context) ([]*GardenSummary, error) {
query := `
SELECT id, name, created_at, user_id,
(SELECT COUNT(*) FROM plants WHERE garden_id = gardens.id) as plant_count
FROM gardens
WHERE active = TRUE
ORDER BY created_at DESC
`
rows, err := f.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var summaries []*GardenSummary
for rows.Next() {
summary := &GardenSummary{}
err := rows.Scan(&summary.Id, &summary.Name, &summary.CreatedAt,
&summary.UserId, &summary.PlantCount)
if err != nil {
return nil, err
}
summaries = append(summaries, summary)
}
return summaries, nil
}
// ❌ Inefficient - Loading full entities and counting in Go
func (f *GardenFetcher) FindGardenSummariesSlow(ctx context.Context) ([]*GardenSummary, error) {
gardens, err := f.FindAll(ctx, nil)
if err != nil {
return nil, err
}
var summaries []*GardenSummary
for _, garden := range gardens {
plants, err := f.plantFetcher.FindByGardenId(ctx, garden.Id)
if err != nil {
return nil, err
}
summary := &GardenSummary{
Id: garden.Id,
Name: garden.Name,
CreatedAt: garden.CreatedAt,
PlantCount: len(plants),
}
summaries = append(summaries, summary)
}
return summaries, nil
}
Batch Operations
// Efficient batch insert
func (h *PlantHandler) CreateMany(ctx context.Context, plants []*mdl.Plant, mod *handler.HandlerMod) error {
if len(plants) == 0 {
return nil
}
// Build batch insert query
query := "INSERT INTO plants (id, name, species, garden_id, created_at, updated_at) VALUES "
args := make([]interface{}, 0, len(plants)*6)
placeholders := make([]string, 0, len(plants))
for _, plant := range plants {
plant.Id = generateID()
plant.CreatedAt = time.Now()
plant.UpdatedAt = time.Now()
placeholders = append(placeholders, "(?, ?, ?, ?, ?, ?)")
args = append(args, plant.Id, plant.Name, plant.Species,
plant.GardenId, plant.CreatedAt, plant.UpdatedAt)
}
query += strings.Join(placeholders, ", ")
_, err := h.db.ExecContext(ctx, query, args...)
return err
}
// Efficient batch update
func (h *PlantHandler) UpdateManyHeights(ctx context.Context, updates map[string]float64) error {
if len(updates) == 0 {
return nil
}
query := "UPDATE plants SET height = ?, updated_at = ? WHERE id = ?"
stmt, err := h.db.PrepareContext(ctx, query)
if err != nil {
return err
}
defer stmt.Close()
now := time.Now()
for id, height := range updates {
_, err = stmt.ExecContext(ctx, height, now, id)
if err != nil {
return err
}
}
return nil
}
Connection Pool Optimization
func setupDatabase(config *config.Database) *sql.DB {
db, err := sql.Open(config.Driver, config.URL)
if err != nil {
log.Fatal(err)
}
// Connection pool settings
db.SetMaxOpenConns(25) // Maximum open connections
db.SetMaxIdleConns(5) // Maximum idle connections
db.SetConnMaxLifetime(5 * time.Minute) // Connection lifetime
db.SetConnMaxIdleTime(1 * time.Minute) // Idle timeout
// Verify connection
if err := db.Ping(); err != nil {
log.Fatal(err)
}
return db
}
Memory Optimization
Efficient Data Structures
// Use struct pointers for large objects
type GardenWithPlants struct {
Garden *mdl.Garden // Pointer to avoid copying
Plants []*mdl.Plant // Slice of pointers
}
// Pool objects for reuse
var gardenPool = sync.Pool{
New: func() interface{} {
return &mdl.Garden{}
},
}
func (f *GardenFetcher) FindOneByIdPooled(ctx context.Context, id string) (*mdl.Garden, bool, error) {
garden := gardenPool.Get().(*mdl.Garden)
defer gardenPool.Put(garden)
// Reset all fields
*garden = mdl.Garden{}
err := f.db.QueryRowContext(ctx, "SELECT * FROM gardens WHERE id = ?", id).
Scan(&garden.Id, &garden.Name, &garden.Description, &garden.CreatedAt, &garden.UpdatedAt)
if err == sql.ErrNoRows {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
// Create new instance to return
result := &mdl.Garden{}
*result = *garden
return result, true, nil
}
Memory Profiling
import (
_ "net/http/pprof"
"net/http"
)
// Enable profiling endpoint
func setupProfiling(router *gin.Engine) {
// Add pprof routes in development
if gin.Mode() == gin.DebugMode {
pprof := router.Group("/debug/pprof")
{
pprof.GET("/", gin.WrapF(http.HandlerFunc(pprof.Index)))
pprof.GET("/heap", gin.WrapF(http.HandlerFunc(pprof.Handler("heap").ServeHTTP)))
pprof.GET("/goroutine", gin.WrapF(http.HandlerFunc(pprof.Handler("goroutine").ServeHTTP)))
pprof.GET("/allocs", gin.WrapF(http.HandlerFunc(pprof.Handler("allocs").ServeHTTP)))
}
}
}
// Memory monitoring
func monitorMemory() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
log.Printf("Memory - Alloc: %d KB, TotalAlloc: %d KB, Sys: %d KB, NumGC: %d",
bToKb(m.Alloc), bToKb(m.TotalAlloc), bToKb(m.Sys), m.NumGC)
}
func bToKb(b uint64) uint64 {
return b / 1024
}
Concurrency Optimization
Worker Pools
type WorkerPool struct {
workerCount int
jobQueue chan Job
quit chan bool
}
type Job func() error
func NewWorkerPool(workerCount int, queueSize int) *WorkerPool {
return &WorkerPool{
workerCount: workerCount,
jobQueue: make(chan Job, queueSize),
quit: make(chan bool),
}
}
func (wp *WorkerPool) Start() {
for i := 0; i < wp.workerCount; i++ {
go wp.worker()
}
}
func (wp *WorkerPool) worker() {
for {
select {
case job := <-wp.jobQueue:
if err := job(); err != nil {
log.Printf("Worker job failed: %v", err)
}
case <-wp.quit:
return
}
}
}
func (wp *WorkerPool) Submit(job Job) {
select {
case wp.jobQueue <- job:
default:
log.Println("Job queue is full, dropping job")
}
}
// Usage for background processing
func (h *EmailHandler) SendWelcomeEmail(userID string) {
job := func() error {
user, exists, err := h.userFetcher.FindOneById(context.Background(), userID, nil)
if err != nil || !exists {
return err
}
return h.emailService.SendWelcomeEmail(user.Email, user.Name)
}
h.workerPool.Submit(job)
}
Goroutine Management
// Bounded goroutine execution
func (f *GardenFetcher) LoadGardensWithPlantsParallel(ctx context.Context, gardenIDs []string) ([]*GardenWithPlants, error) {
const maxConcurrency = 10
semaphore := make(chan struct{}, maxConcurrency)
results := make([]*GardenWithPlants, len(gardenIDs))
errors := make([]error, len(gardenIDs))
var wg sync.WaitGroup
for i, gardenID := range gardenIDs {
wg.Add(1)
go func(index int, id string) {
defer wg.Done()
// Acquire semaphore
semaphore <- struct{}{}
defer func() { <-semaphore }()
garden, exists, err := f.FindOneById(ctx, id, nil)
if err != nil || !exists {
errors[index] = err
return
}
plants, err := f.plantFetcher.FindByGardenId(ctx, id)
if err != nil {
errors[index] = err
return
}
results[index] = &GardenWithPlants{
Garden: garden,
Plants: plants,
}
}(i, gardenID)
}
wg.Wait()
// Check for errors
for _, err := range errors {
if err != nil {
return nil, err
}
}
return results, nil
}
HTTP Performance
Response Compression
import "github.com/gin-contrib/gzip"
func setupCompression(router *gin.Engine) {
// Enable gzip compression
router.Use(gzip.Gzip(gzip.DefaultCompression))
}
// Custom compression for large responses
func CompressJSONResponse(data interface{}) ([]byte, error) {
jsonData, err := json.Marshal(data)
if err != nil {
return nil, err
}
var buf bytes.Buffer
gzipWriter := gzip.NewWriter(&buf)
_, err = gzipWriter.Write(jsonData)
if err != nil {
return nil, err
}
err = gzipWriter.Close()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
HTTP/2 Server Push
func setupHTTP2Push(router *gin.Engine) {
router.GET("/gardens/:id", func(c *gin.Context) {
// Push critical resources
if pusher := c.Writer.Pusher(); pusher != nil {
pusher.Push("/assets/css/output.css", nil)
pusher.Push("/assets/js/app.js", nil)
}
// Render response
// ...
})
}
Monitoring & Metrics
Performance Metrics
type MetricsCollector struct {
requestCount *prometheus.CounterVec
requestDuration *prometheus.HistogramVec
activeRequests prometheus.Gauge
}
func NewMetricsCollector() *MetricsCollector {
return &MetricsCollector{
requestCount: prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
},
[]string{"method", "endpoint", "status"},
),
requestDuration: prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
},
[]string{"method", "endpoint"},
),
activeRequests: prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "http_requests_active",
Help: "Active HTTP requests",
},
),
}
}
func (mc *MetricsCollector) MetricsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
mc.activeRequests.Inc()
defer func() {
duration := time.Since(start).Seconds()
mc.activeRequests.Dec()
mc.requestCount.WithLabelValues(
c.Request.Method,
c.FullPath(),
fmt.Sprintf("%d", c.Writer.Status()),
).Inc()
mc.requestDuration.WithLabelValues(
c.Request.Method,
c.FullPath(),
).Observe(duration)
}()
c.Next()
}
}
Database Query Monitoring
type QueryLogger struct {
slowQueryThreshold time.Duration
logger *log.Logger
}
func NewQueryLogger(threshold time.Duration, logger *log.Logger) *QueryLogger {
return &QueryLogger{
slowQueryThreshold: threshold,
logger: logger,
}
}
func (ql *QueryLogger) LogQuery(query string, args []interface{}, duration time.Duration) {
if duration > ql.slowQueryThreshold {
ql.logger.Printf("SLOW QUERY (%v): %s [args: %v]", duration, query, args)
}
}
// Wrapper for database queries
func (f *GardenFetcher) QueryWithLogging(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
start := time.Now()
rows, err := f.db.QueryContext(ctx, query, args...)
duration := time.Since(start)
f.queryLogger.LogQuery(query, args, duration)
return rows, err
}
Load Testing
Benchmark Tests
func BenchmarkGardenFetch(b *testing.B) {
fetcher := setupBenchmarkFetcher()
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, err := fetcher.FindOneById(ctx, "test-garden-id", nil)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkGardenFetchParallel(b *testing.B) {
fetcher := setupBenchmarkFetcher()
ctx := context.Background()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _, err := fetcher.FindOneById(ctx, "test-garden-id", nil)
if err != nil {
b.Fatal(err)
}
}
})
}
Load Testing Script
// Simple load test
func loadTest() {
const (
baseURL = "http://localhost:8080"
concurrency = 10
requests = 1000
)
client := &http.Client{
Timeout: 30 * time.Second,
}
var wg sync.WaitGroup
requestsPerWorker := requests / concurrency
results := make(chan time.Duration, requests)
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < requestsPerWorker; j++ {
start := time.Now()
resp, err := client.Get(baseURL + "/api/gardens")
duration := time.Since(start)
if err != nil {
log.Printf("Request failed: %v", err)
continue
}
resp.Body.Close()
results <- duration
}
}()
}
wg.Wait()
close(results)
// Calculate statistics
var durations []time.Duration
for duration := range results {
durations = append(durations, duration)
}
sort.Slice(durations, func(i, j int) bool {
return durations[i] < durations[j]
})
fmt.Printf("Total requests: %d\n", len(durations))
fmt.Printf("Average: %v\n", calculateAverage(durations))
fmt.Printf("95th percentile: %v\n", durations[int(float64(len(durations))*0.95)])
fmt.Printf("99th percentile: %v\n", durations[int(float64(len(durations))*0.99)])
}
Performance Best Practices
General Guidelines
- Profile First: Always measure before optimizing
- Database Efficiency: Optimize queries and use appropriate indexes
- Caching Strategy: Implement multi-level caching
- Connection Pooling: Configure database connection pools properly
- Concurrent Processing: Use goroutines and worker pools for I/O bound tasks
- Memory Management: Use object pools and avoid memory leaks
- HTTP Optimization: Enable compression and use HTTP/2
- Monitoring: Implement comprehensive monitoring and alerting
Common Anti-patterns
// ❌ N+1 Query Problem
for _, garden := range gardens {
plants, _ := plantFetcher.FindByGardenId(ctx, garden.Id)
// Process plants...
}
// ✅ Batch Loading
allPlants, _ := plantFetcher.FindByGardenIds(ctx, gardenIds)
plantsByGarden := groupPlantsByGarden(allPlants)
// ❌ Loading Too Much Data
gardens, _ := gardenFetcher.FindAll(ctx, nil) // Loads everything
// ✅ Pagination and Filtering
mod := gardenFetcher.Mod()
mod.Page = 1
mod.PerPage = 20
gardens, pagination, _ := gardenFetcher.FindPage(ctx, mod)
// ❌ Synchronous Blocking Operations
for _, email := range emails {
emailService.Send(email) // Blocks for each email
}
// ✅ Asynchronous Processing
for _, email := range emails {
emailQueue.Enqueue(email)
}
Performance optimization is an ongoing process. Regular monitoring, profiling, and load testing help identify bottlenecks and ensure your application scales effectively.