---
# === IDENTITY ===
id: software/migrations/php-to-go/2026
canonical_question: "How do I migrate a PHP backend to Go?"
aliases:
  - "PHP to Go migration guide"
  - "convert PHP to Golang"
  - "replace PHP with Go"
  - "PHP to Go rewrite"
  - "modernize PHP backend with Go"
  - "PHP Laravel to Go migration"
entity_type: software_reference
domain: software > migrations > php_to_go
region: global
jurisdiction: global
temporal_scope: 2018-2026

# === VERIFICATION ===
last_verified: 2026-05-29
confidence: 0.90
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "Go 1.26 Green Tea GC enabled by default (Feb 2026)"
  next_review: 2026-11-25
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Go's concurrency model (goroutines + channels) has no direct PHP equivalent — you must redesign concurrent workflows, not translate them line-by-line"
  - "Go's html/template is intentionally minimal — there is no Blade/Twig equivalent; server-rendered UIs require a fundamentally different approach or a separate frontend"
  - "Go has no generics-based ORM as mature as Eloquent — use sqlx for direct SQL, GORM for rapid development, or sqlc for type-safe code generation from SQL"
  - "PHP's exception-based error handling does not map to Go — adopt explicit if err != nil returns throughout; panic/recover is reserved for truly unrecoverable situations"
  - "Go binaries are statically linked single files — there is no composer autoload or dynamic class loading; all dependencies are compiled in at build time"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Migrating PHP to Node.js or TypeScript"
    use_instead: "software/migrations/php-to-nodejs/2026"
  - condition: "Migrating PHP to Python or Django"
    use_instead: "software/migrations/php-to-python/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: php_framework
    question: "Which PHP framework is the codebase using?"
    type: choice
    options: ["Laravel", "Symfony", "CodeIgniter", "Slim", "CakePHP", "No framework (vanilla PHP)"]
  - key: app_type
    question: "What type of application is this?"
    type: choice
    options: ["REST API", "Monolithic web app", "Microservices", "CLI tool", "Queue worker"]
  - key: performance_needs
    question: "What is the primary reason for migrating?"
    type: choice
    options: ["Performance/scaling limits", "Concurrency (WebSocket, streaming)", "Deployment simplification (containers)", "Team preference/hiring", "Reducing infrastructure costs"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/php-to-go/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-29)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/migrations/php-to-nodejs/2026"
      label: "PHP to Node.js Migration"
    - id: "software/migrations/php-to-python/2026"
      label: "PHP to Python Migration"
    - id: "software/migrations/java-to-go/2026"
      label: "Java to Go Migration"
  solves:
    - id: "software/debugging/go-goroutine-leak/2026"
      label: "Go Goroutine Leak Detection"
  alternative_to:
    - id: "software/migrations/php-to-nodejs/2026"
      label: "PHP to Node.js Migration"
  often_confused_with:
    - id: "software/migrations/php-to-python/2026"
      label: "PHP to Python Migration"

# === SOURCES (10 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Tutorial: Developing a RESTful API with Go and Gin"
    author: Go Team
    url: https://go.dev/doc/tutorial/web-service-gin
    type: official_docs
    published: 2024-01-01
    reliability: authoritative
  - id: src2
    title: "Swipe Right on Go: Why We Ghosted PHP — Muzz Engineering"
    author: Muzz Engineering
    url: https://engineering.muzz.com/swipe-right-on-go-why-we-ghosted-php
    type: technical_blog
    published: 2025-03-15
    reliability: high
  - id: src3
    title: "Implementing the Strangler Fig Pattern for Gradual Migration of Monolithic Applications to Go Microservices"
    author: Hemaks
    url: https://hemaks.org/posts/implementing-the-strangler-fig-pattern-for-gradual-migration-of-monolithic-applications-to-go-microservices/
    type: technical_blog
    published: 2025-01-10
    reliability: high
  - id: src4
    title: "Strangler Fig Pattern — Azure Architecture Center"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/azure/architecture/patterns/strangler-fig
    type: official_docs
    published: 2024-03-01
    reliability: authoritative
  - id: src5
    title: "The Impact of Migrating from PHP to Golang"
    author: Roelof Jan Elsinga
    url: https://roelofjanelsinga.com/articles/the-impact-of-migrating-from-php-to-golang/
    type: technical_blog
    published: 2023-08-20
    reliability: moderate_high
  - id: src6
    title: "Comparing database/sql, GORM, sqlx, and sqlc"
    author: JetBrains
    url: https://blog.jetbrains.com/go/2023/04/27/comparing-db-packages/
    type: technical_blog
    published: 2023-04-27
    reliability: high
  - id: src7
    title: "Go 1.24 Release Notes"
    author: Go Team
    url: https://go.dev/doc/go1.24
    type: official_docs
    published: 2025-02-01
    reliability: authoritative
  - id: src8
    title: "Transitioning From PHP to Go in 2025"
    author: AryaLinux
    url: https://aryalinux.org/blog/transitioning-from-php-to-go
    type: technical_blog
    published: 2025-06-01
    reliability: moderate_high
  - id: src9
    title: "Go 1.26 Release Notes"
    author: Go Team
    url: https://go.dev/doc/go1.26
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src10
    title: "PHP to Go Migration: Skelar's Backend Playbook 2026"
    author: Hammani Tech
    url: https://hammanitech.com/blog/skelar-php-to-go-migration/
    type: technical_blog
    published: 2026-03-01
    reliability: moderate_high
---

# How to Migrate a PHP Backend to Go

## How do I migrate a PHP backend to Go?

## TL;DR

- **Bottom line**: Use the strangler fig pattern -- run Go and PHP side by side behind a reverse proxy, migrate one endpoint at a time, and let PHP shrink until it is gone. Muzz cut P50 latency by 57% doing exactly this at 2,000 req/s. [src2, src4]
- **Key tool/command**: `go mod init yourproject && go get github.com/gin-gonic/gin`
- **Watch out for**: Attempting a big-bang rewrite -- most PHP-to-Go projects that fail do so because teams tried to rewrite everything at once instead of migrating incrementally. [src2, src3]
- **Works with**: Go 1.22+ (1.26 current as of Feb 2026), any PHP version (5.6-8.4), works with NGINX/Caddy/Traefik reverse proxy, Docker, Kubernetes.

## Constraints

- Go's concurrency model (goroutines + channels) has no direct PHP equivalent -- you must redesign concurrent workflows, not translate them line-by-line. [src2]
- Go's `html/template` is intentionally minimal -- there is no Blade/Twig equivalent. Server-rendered UIs require a fundamentally different approach or a separate frontend (React, Vue, htmx). [src8]
- Go has no generics-based ORM as mature as Eloquent -- use `sqlx` for direct SQL, `GORM` for rapid development, or `sqlc` for type-safe code generation from SQL. [src6]
- PHP's exception-based error handling does not map to Go -- adopt explicit `if err != nil` returns throughout. Using `panic/recover` as a substitute is an anti-pattern. [src5, src8]
- Go binaries are statically linked single files -- there is no `composer autoload` or dynamic class loading. All dependencies are compiled in at build time.
- Never open a new database connection per request (the PHP-FPM habit) -- Go uses a long-lived connection pool shared across all goroutines. [src6]

## Quick Reference

| PHP Pattern | Go Equivalent | Example |
|---|---|---|
| `$router->get('/users', fn)` (Laravel) | `r.GET("/users", handler)` (Gin) | `r := gin.Default(); r.GET("/users", getUsers)` [src1] |
| `PDO::query($sql)` | `db.QueryContext(ctx, sql)` (database/sql) | `rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")` [src6] |
| `Eloquent::find($id)` | `db.First(&user, id)` (GORM) | `var user User; db.First(&user, id)` [src6] |
| `$app->middleware(fn)` | `r.Use(middleware)` (Gin) | `r.Use(gin.Logger(), gin.Recovery())` [src1] |
| `json_encode($data)` | `json.Marshal(data)` | `bytes, err := json.Marshal(response)` |
| `try { } catch (Exception $e)` | `if err != nil { }` | `result, err := doSomething(); if err != nil { return err }` |
| `$_SESSION['user']` | `gorilla/sessions` or JWT | `session, _ := store.Get(r, "session-name")` |
| `Auth::user()` (Laravel) | Custom middleware + JWT | `claims := ctx.MustGet("claims").(*Claims)` |
| `.env` / `config()` (Laravel) | `os.Getenv()` or Viper | `viper.SetConfigFile(".env"); viper.ReadInConfig()` |
| `composer require package` | `go get package` | `go get github.com/gin-gonic/gin@v1.10` |
| `Blade::render('view')` | `html/template` | `tmpl.ExecuteTemplate(w, "layout.html", data)` |
| `php artisan make:command` | `cobra` CLI framework | `rootCmd.AddCommand(migrateCmd)` |
| `Carbon::now()` | `time.Now()` | `now := time.Now().UTC()` |
| `$request->validate([...])` | `binding` tags (Gin) or `go-playground/validator` | `type Req struct { Name string `binding:"required"` }` [src1] |
| `php artisan migrate` | `golang-migrate/migrate` | `migrate -path db/migrations -database $DB_URL up` |

## Decision Tree

```
START
├── Is the PHP app a monolith or microservices?
│   ├── MONOLITH → Use strangler fig pattern: proxy routes to Go or PHP per endpoint [src3, src4]
│   └── MICROSERVICES → Rewrite one service at a time, starting with highest-traffic or simplest [src2]
├── What is the team's Go experience?
│   ├── NONE → Start with a small, non-critical internal tool or CLI in Go first [src2, src8]
│   └── SOME/EXPERIENCED ↓
├── What framework complexity do you need?
│   ├── MINIMAL (few routes, simple middleware) → Use net/http + chi router (Go 1.22+ has built-in path params)
│   ├── MODERATE (REST API, validation, middleware) → Use Gin (most popular, best docs) [src1]
│   ├── HIGH PERFORMANCE (>50K req/s, WebSocket) → Use Fiber (fasthttp-based)
│   └── ENTERPRISE (type safety, OpenAPI generation) → Use Echo
├── What database access pattern?
│   ├── SIMPLE QUERIES, MAX CONTROL → database/sql + sqlx [src6]
│   ├── RAPID DEVELOPMENT, FAMILIAR WITH ORMs → GORM [src6]
│   ├── COMPLEX RELATIONS, CODE-FIRST SCHEMA → Ent (Facebook/Meta)
│   └── TYPE-SAFE FROM SQL → sqlc (generates Go from SQL) [src6]
└── DEFAULT → Gin + sqlx + strangler fig behind NGINX
```

## Decision Logic

Agent-facing if/then rules for recommending a migration path. Apply top-down; the first matching rule wins.

### If the team has zero production Go experience
--> Do not migrate a critical path first. Ship one small internal CLI or a low-traffic read endpoint in Go to build fluency, then expand. [src2, src8]

### If the PHP monolith still ships features daily and cannot freeze
--> Use the strangler fig pattern behind NGINX/Traefik: route migrated endpoints to Go, leave everything else on PHP-FPM, and ramp traffic 1% -> 10% -> 50% -> 100% with shadow traffic validation. Never attempt a big-bang rewrite. [src3, src4, src10]

### If you need a thin REST API with validation and middleware
--> Use Gin (most popular, best docs) with `sqlx` for data access; reach for `net/http` + chi only if you want zero framework dependency on Go 1.22+. [src1, src6]

### If you need maximum throughput (>50K req/s) or WebSocket/streaming
--> Use Fiber (fasthttp-based) or Echo, and lean on goroutines + `errgroup` for concurrent I/O — this is Go's structural advantage over single-threaded PHP-FPM. [src2, src10]

### If you deploy to Kubernetes
--> Build on Go 1.25+ so container-aware GOMAXPROCS auto-respects the cgroup CPU limit; do not hard-code `GOMAXPROCS` or rely on `automaxprocs` anymore. [src9]

### If the app is I/O-bound and PHP 8.4 + OPcache + JIT already meets SLOs
--> Do NOT migrate. Optimize PHP first (FrankenPHP, Swoole/RoadRunner for async) — migrating buys little and costs a rewrite. [src5, src8]

### If you migrate the data layer and the team knows ORMs
--> Use GORM for speed of development, `sqlc` for type-safe code generation from SQL, or `sqlx` for PDO-like control; always share one long-lived connection pool, never open a connection per request. [src6]

## Step-by-Step Guide

### 1. Set up the Go project alongside PHP

Create a Go module in a separate directory or repository. Both the PHP app and Go app will run simultaneously during migration, served by a reverse proxy. [src3, src4]

```bash
# Initialize Go project
mkdir go-backend && cd go-backend
go mod init github.com/yourorg/yourproject
go get github.com/gin-gonic/gin@v1.10.0
go get github.com/jmoiron/sqlx@v1.4.0
go get github.com/lib/pq@v1.10.9       # PostgreSQL driver
go get github.com/go-sql-driver/mysql@v1.8.1  # MySQL driver (if applicable)
```

**Verify**: `go build ./...` compiles without errors.

### 2. Configure a reverse proxy to split traffic

Use NGINX or Caddy to route requests -- migrated endpoints go to Go, everything else goes to PHP. This is the strangler fig pattern in action. [src3, src4]

```nginx
# nginx.conf — route migrated endpoints to Go, rest to PHP
upstream go_backend {
    server 127.0.0.1:8080;
}
upstream php_backend {
    server 127.0.0.1:9000;  # PHP-FPM
}

server {
    listen 80;
    server_name api.example.com;

    # Migrated endpoints → Go
    location /api/v2/users {
        proxy_pass http://go_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Everything else → PHP (Laravel/Symfony)
    location / {
        fastcgi_pass php_backend;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}
```

**Verify**: `curl http://api.example.com/api/v2/users` returns a Go response; `curl http://api.example.com/api/v1/orders` returns a PHP response.

### 3. Replicate the database access layer

Port your PHP database queries to Go. Use sqlx for direct SQL (closest to PDO) or GORM if your team prefers an ORM. Both apps share the same database during migration. [src5, src6]

```go
package models

import (
    "context"
    "time"

    "github.com/jmoiron/sqlx"
)

// User maps to the users table — struct tags match column names
type User struct {
    ID        int64     `db:"id" json:"id"`
    Name      string    `db:"name" json:"name"`
    Email     string    `db:"email" json:"email"`
    CreatedAt time.Time `db:"created_at" json:"created_at"`
}

// UserRepository replaces Laravel's Eloquent User model
type UserRepository struct {
    db *sqlx.DB
}

func NewUserRepository(db *sqlx.DB) *UserRepository {
    return &UserRepository{db: db}
}

// FindByID replaces User::find($id)
func (r *UserRepository) FindByID(ctx context.Context, id int64) (*User, error) {
    var user User
    err := r.db.GetContext(ctx, &user, "SELECT id, name, email, created_at FROM users WHERE id = $1", id)
    if err != nil {
        return nil, err
    }
    return &user, nil
}

// List replaces User::paginate($perPage)
func (r *UserRepository) List(ctx context.Context, limit, offset int) ([]User, error) {
    var users []User
    err := r.db.SelectContext(ctx, &users,
        "SELECT id, name, email, created_at FROM users ORDER BY id LIMIT $1 OFFSET $2",
        limit, offset)
    return users, err
}
```

**Verify**: Write a test -- `go test ./models/... -v` -- that connects to your existing database and retrieves a row.

### 4. Implement the first API endpoint in Go

Pick a simple, high-traffic read endpoint first (e.g., GET /users). This builds team confidence and provides measurable performance comparison. Muzz started with their Discover endpoint and saw a 57% P50 latency reduction. [src1, src2]

```go
package main

import (
    "log"
    "net/http"
    "os"
    "strconv"

    "github.com/gin-gonic/gin"
    "github.com/jmoiron/sqlx"
    _ "github.com/lib/pq"
    "github.com/yourorg/yourproject/models"
)

func main() {
    // Connect to the SAME database your PHP app uses
    db, err := sqlx.Connect("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatalf("db connect: %v", err)
    }
    defer db.Close()

    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(5)

    userRepo := models.NewUserRepository(db)

    r := gin.Default()

    // Health check
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

    // GET /api/v2/users — replaces the PHP endpoint
    r.GET("/api/v2/users", func(c *gin.Context) {
        limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
        offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))

        users, err := userRepo.List(c.Request.Context(), limit, offset)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch users"})
            log.Printf("list users: %v", err)
            return
        }
        c.JSON(http.StatusOK, users)
    })

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    log.Printf("Go backend listening on :%s", port)
    r.Run(":" + port)
}
```

**Verify**: `curl http://localhost:8080/api/v2/users?limit=5` returns JSON matching the PHP endpoint's output.

### 5. Port middleware (auth, logging, CORS)

Translate your PHP middleware stack to Gin middleware. Go middleware follows the same pattern -- intercept the request, do work, then call `c.Next()`. [src1, src2]

```go
package middleware

import (
    "log"
    "net/http"
    "strings"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

// Logger replaces Laravel's logging middleware
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        log.Printf("%s %s %d %s",
            c.Request.Method,
            c.Request.URL.Path,
            c.Writer.Status(),
            time.Since(start),
        )
    }
}

// CORS replaces Laravel's CORS middleware
func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type")
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
            return
        }
        c.Next()
    }
}

// JWTAuth replaces Laravel's auth:api middleware
func JWTAuth(secret string) gin.HandlerFunc {
    return func(c *gin.Context) {
        header := c.GetHeader("Authorization")
        if !strings.HasPrefix(header, "Bearer ") {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
            return
        }

        tokenStr := strings.TrimPrefix(header, "Bearer ")
        token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
            return []byte(secret), nil
        })
        if err != nil || !token.Valid {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
            return
        }

        claims := token.Claims.(jwt.MapClaims)
        c.Set("user_id", claims["sub"])
        c.Next()
    }
}
```

**Verify**: `curl -H "Authorization: Bearer <token>" http://localhost:8080/api/v2/protected` returns 200 with valid token, 401 without.

### 6. Migrate database schemas with golang-migrate

Replace `php artisan migrate` with golang-migrate for version-controlled schema changes that work across both PHP and Go. [src5]

```bash
# Install golang-migrate
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

# Create a migration
migrate create -ext sql -dir db/migrations -seq add_user_roles

# Run migrations
migrate -path db/migrations -database "postgres://user:pass@localhost/mydb?sslmode=disable" up

# Rollback last migration
migrate -path db/migrations -database "postgres://user:pass@localhost/mydb?sslmode=disable" down 1
```

```sql
-- db/migrations/000001_add_user_roles.up.sql
ALTER TABLE users ADD COLUMN role VARCHAR(20) NOT NULL DEFAULT 'user';
CREATE INDEX idx_users_role ON users(role);

-- db/migrations/000001_add_user_roles.down.sql
DROP INDEX IF EXISTS idx_users_role;
ALTER TABLE users DROP COLUMN role;
```

**Verify**: `migrate -path db/migrations -database $DATABASE_URL version` shows the current migration version.

### 7. Containerize and deploy Go alongside PHP

Build a minimal Docker image for the Go service. Go compiles to a single static binary, so the image can be under 20 MB compared to PHP's 300+ MB. [src2, src5]

```dockerfile
# Build stage
FROM golang:1.26-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/server

# Runtime stage — ~5 MB base image
FROM alpine:3.21
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
COPY --from=builder /app/db/migrations ./db/migrations
EXPOSE 8080
CMD ["./server"]
```

```bash
docker build -t go-backend:latest .
docker run -p 8080:8080 -e DATABASE_URL="$DATABASE_URL" go-backend:latest
```

**Verify**: `docker images go-backend` shows image size under 20 MB. `curl http://localhost:8080/health` returns `{"status":"ok"}`.

## Code Examples

### Go/Gin: REST API with CRUD operations (replaces Laravel Resource Controller)

> Full script: [go-gin-rest-api-with-crud-operations-replaces-lara.go](scripts/go-gin-rest-api-with-crud-operations-replaces-lara.go) (94 lines)

```go
// Input:  HTTP requests to /api/v2/products (GET, POST, PUT, DELETE)
// Output: JSON responses with proper status codes and error handling
package main
import (
    "net/http"
# ... (see full script)
```

### Go: Database migration from PDO to database/sql + sqlx

> Full script: [go-database-migration-from-pdo-to-database-sql-sql.go](scripts/go-database-migration-from-pdo-to-database-sql-sql.go) (67 lines)

```go
// Input:  PHP PDO-style query patterns
// Output: Equivalent Go database/sql + sqlx patterns with proper error handling
package db
import (
    "context"
# ... (see full script)
```

### Go: Middleware chain (replaces Laravel middleware stack)

> Full script: [go-middleware-chain-replaces-laravel-middleware-st.go](scripts/go-middleware-chain-replaces-laravel-middleware-st.go) (86 lines)

```go
// Input:  HTTP request requiring rate limiting, auth, and request logging
// Output: Gin middleware chain equivalent to Laravel's middleware groups
package middleware
import (
    "net/http"
# ... (see full script)
```

## Anti-Patterns

### Wrong: Translating PHP line-by-line to Go

```go
// ❌ BAD — writing Go like PHP (global state, no error handling)
var db *sql.DB  // global mutable state

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    row := db.QueryRow("SELECT * FROM users WHERE id = " + id)  // SQL injection!
    var user User
    row.Scan(&user.ID, &user.Name)  // ignoring error
    json.NewEncoder(w).Encode(user) // ignoring error
}
```

### Correct: Idiomatic Go with dependency injection and error handling

```go
// ✅ GOOD — proper Go patterns: DI, parameterized queries, error handling
type Handler struct {
    db *sqlx.DB
}

func (h *Handler) GetUser(c *gin.Context) {
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
        return
    }

    var user User
    err = h.db.GetContext(c.Request.Context(), &user,
        "SELECT id, name, email FROM users WHERE id = $1", id) // parameterized
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
        return
    }
    c.JSON(http.StatusOK, user)
}
```

### Wrong: Using Go like PHP's request-per-process model

```go
// ❌ BAD — opening a new DB connection per request (PHP-FPM habit)
func handler(c *gin.Context) {
    db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    defer db.Close()  // opens and closes connection every request
    // ... query ...
}
```

### Correct: Use connection pooling (Go's built-in strength)

```go
// ✅ GOOD — single connection pool shared across all goroutines
func main() {
    db, err := sqlx.Connect("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal(err)
    }
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(5)

    h := &Handler{db: db}  // inject the pool
    r := gin.Default()
    r.GET("/users/:id", h.GetUser)
    r.Run(":8080")
}
```

### Wrong: Ignoring Go's error handling (using panic/recover like exceptions)

```go
// ❌ BAD — using panic/recover as try/catch (PHP exception habit)
func processOrder(order Order) {
    if order.Total <= 0 {
        panic("invalid order total")  // don't use panic for business logic
    }
    // ...
}

func handler(c *gin.Context) {
    defer func() {
        if r := recover(); r != nil {
            c.JSON(500, gin.H{"error": r})
        }
    }()
    processOrder(order)
}
```

### Correct: Return errors explicitly

```go
// ✅ GOOD — explicit error returns (the Go way)
func processOrder(order Order) error {
    if order.Total <= 0 {
        return fmt.Errorf("invalid order total: %.2f", order.Total)
    }
    // ...
    return nil
}

func handler(c *gin.Context) {
    if err := processOrder(order); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"status": "processed"})
}
```

### Wrong: Porting PHP's dynamic typing patterns to Go

```go
// ❌ BAD — using map[string]interface{} everywhere (mimicking PHP arrays)
func getConfig() map[string]interface{} {
    return map[string]interface{}{
        "db_host": "localhost",
        "db_port": 5432,         // is this int or string? who knows
        "debug":   true,
    }
}

func handler(c *gin.Context) {
    cfg := getConfig()
    host := cfg["db_host"].(string)  // runtime panic if wrong type
}
```

### Correct: Use typed structs for configuration

```go
// ✅ GOOD — strongly typed configuration
type Config struct {
    DBHost string `env:"DB_HOST" envDefault:"localhost"`
    DBPort int    `env:"DB_PORT" envDefault:"5432"`
    Debug  bool   `env:"DEBUG"   envDefault:"false"`
}

func LoadConfig() (*Config, error) {
    var cfg Config
    if err := env.Parse(&cfg); err != nil {
        return nil, fmt.Errorf("parse config: %w", err)
    }
    return &cfg, nil
}
```

### Wrong: Not using goroutines for concurrent work

```go
// ❌ BAD — sequential processing (PHP habit: one thing at a time)
func handler(c *gin.Context) {
    users := fetchUsers()      // 200ms
    orders := fetchOrders()    // 300ms
    stats := fetchStats()      // 150ms
    // Total: 650ms sequential
    c.JSON(200, gin.H{"users": users, "orders": orders, "stats": stats})
}
```

### Correct: Use goroutines and errgroup for concurrent fetches

> Full script: [correct-use-goroutines-and-errgroup-for-concurrent.go](scripts/correct-use-goroutines-and-errgroup-for-concurrent.go) (29 lines)

```go
// ✅ GOOD — concurrent fetches (Go's killer feature)
import "golang.org/x/sync/errgroup"
func handler(c *gin.Context) {
    var users []User
    var orders []Order
# ... (see full script)
```

## Common Pitfalls

- **SQL injection from string concatenation**: PHP developers used to PDO prepared statements sometimes concatenate SQL strings in Go. Fix: Always use parameterized queries -- `db.Query("SELECT * FROM users WHERE id = $1", id)`, never string formatting. [src6]
- **Forgetting to close `sql.Rows`**: In PHP, PDO result sets are garbage-collected. In Go, unclosed `Rows` objects leak database connections. Fix: Always `defer rows.Close()` immediately after `db.Query()`. [src6]
- **Not handling Go's zero values**: PHP returns `null` for unset variables. Go initializes variables to zero values (`""` for strings, `0` for ints, `false` for bools). Fix: Use pointers (`*string`, `*int`) for nullable fields, or use `sql.NullString` for database columns. [src5]
- **Deploying without container-aware GOMAXPROCS on older Go**: On Go < 1.25, the runtime defaults GOMAXPROCS to the host CPU count, not the container's CPU limit -- starving or over-scheduling the service. Fix: Build on Go 1.25+ (GOMAXPROCS now reads the cgroup CPU limit automatically); on older runtimes use `go.uber.org/automaxprocs` or set `GOMAXPROCS` explicitly. [src2, src9]
- **Using the default HTTP client with no timeouts**: Go's `http.DefaultClient` has no timeout, unlike PHP's cURL defaults. Fix: Always create a client with explicit timeouts -- `&http.Client{Timeout: 10 * time.Second}`. [src1]
- **Migrating everything at once instead of using the strangler fig pattern**: Big-bang rewrites stall feature delivery and introduce massive regression risk. Fix: Migrate one endpoint at a time, keep PHP running for unmigrated routes, and compare outputs. [src2, src3, src4]
- **Not leveraging Go's concurrency**: PHP developers write sequential code by habit since PHP is single-threaded per request. Fix: Use goroutines with `errgroup` for concurrent I/O operations -- this is Go's biggest advantage over PHP. [src2, src5]
- **Running both systems without cross-team coordination**: Muzz reported features being built in PHP despite the language being phased out, creating redundant work. Fix: Establish clear migration ownership and freeze non-critical PHP development early. [src2]

## Diagnostic Commands

```bash
# Check Go installation and version
go version

# Verify module dependencies are resolved
go mod tidy && go mod verify

# Run all tests with race detector (catches concurrency bugs)
go test -race ./...

# Benchmark a specific function (compare with PHP performance)
go test -bench=. -benchmem ./...

# Profile CPU usage (find bottlenecks vs PHP baseline)
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

# Check for common Go mistakes
go vet ./...

# Run the Go linter (catches non-idiomatic patterns)
golangci-lint run ./...

# Compare response output between PHP and Go endpoints
diff <(curl -s http://localhost:9000/api/v1/users) <(curl -s http://localhost:8080/api/v2/users)

# Monitor goroutine count (detect leaks)
curl http://localhost:8080/debug/pprof/goroutine?debug=1 | head -1

# Database connection pool stats
curl http://localhost:8080/debug/db/stats
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Go 1.26 (Feb 2026) | Current | Green Tea GC on by default, heap base address randomization, `crypto/hpke`, `net/url.Parse` rejects malformed host colons | 10-40% less GC overhead (no code change); `go fix` adds modernizers; ~30% lower cgo overhead; deprecates `ReverseProxy.Director` (use `Rewrite`) [src9] |
| Go 1.25 (Aug 2025) | Supported | Container-aware `GOMAXPROCS` (respects cgroup CPU limit), `testing/synctest`, experimental `encoding/json/v2` (`GOEXPERIMENT=jsonv2`) | Drop `automaxprocs` in K8s — runtime now reads the cgroup limit; use `sync.WaitGroup.Go()`; SHA-1 disallowed in TLS 1.2 [src9] |
| Go 1.24 (Feb 2025) | Supported | Swiss Tables map implementation, `go:wasmexport`, tool directives in go.mod | Use `go get -tool` for linters/generators; 2-3% CPU overhead reduction; `json:"name,omitzero"` tag; `os.Root` for filesystem sandboxing [src7] |
| Go 1.23 (Aug 2024) | Supported | Range-over-func iterators, `unique` package | Use `iter` package for cleaner collection iteration; new `Request.Pattern` field in net/http |
| Go 1.22 (Feb 2024) | Maintenance | Enhanced `net/http` routing with path params | Can use `http.NewServeMux()` with `{id}` params -- reduces need for Gin/Chi for simple APIs |
| Go 1.21 (Aug 2023) | EOL | `log/slog` added (structured logging), `maps`/`slices` packages | Replace `log.Printf` with `slog.Info()` for structured logging |
| Go 1.18 (Mar 2022) | EOL | Generics introduced | Enables type-safe utility functions (no more `interface{}` casts) |
| PHP 8.4 (Nov 2024) | Current source | Property hooks, asymmetric visibility, new DOM API | Consider PHP 8.4 features before deciding to migrate -- property hooks reduce boilerplate significantly |
| PHP 8.3 (Nov 2023) | Supported | Typed class constants, `json_validate()` | PHP performance has improved with JIT -- verify migration is still warranted |
| PHP 7.4 (Nov 2019) | EOL | Typed properties, arrow functions | If stuck on PHP 7.x, migration urgency is higher due to security EOL |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| PHP app has hit CPU/memory scaling limits under high concurrency (>1K req/s) | PHP app handles load fine with PHP 8.4 + OPcache + FrankenPHP | Optimize PHP first (OPcache, JIT, connection pooling) |
| Building new microservices alongside existing PHP monolith | Entire team knows only PHP and timeline is tight | Stay with PHP, consider Swoole/RoadRunner for async |
| Need sub-millisecond response times or WebSocket/streaming support | Building a content-heavy website (CMS, blog, e-commerce) | Laravel/WordPress with caching layer |
| Deploying to Kubernetes and want minimal container images (<20 MB vs 300+ MB) | Startup/MVP needing rapid prototyping with minimal boilerplate | Keep PHP (Laravel) or consider Node.js |
| Team already has Go experience or is eager to learn | Heavily invested in Laravel ecosystem (Nova, Horizon, Forge, Vapor) | Continue with Laravel, add Go for specific bottleneck services |
| Reducing infrastructure costs (Go uses 5-10x less memory per request than PHP-FPM) | Application is I/O bound, not CPU bound | PHP 8.4 with async (Swoole) or Node.js |

## Important Caveats

- Go has no generics-based ORM as mature as Laravel's Eloquent -- expect to write more SQL or use code generation tools like sqlc. The trade-off is more code but more control and better performance. [src6]
- PHP's exception-based error handling does not map to Go -- you must adopt Go's explicit `if err != nil` pattern throughout. Using panic/recover as a substitute is an anti-pattern. [src5, src8]
- Go binaries are statically linked and self-contained -- there is no equivalent of `php artisan serve` during development. Use tools like `air` or `gow` for hot-reload during development.
- The PHP-to-Go migration is a one-way door -- once team skills shift to Go, maintaining PHP becomes increasingly expensive. Plan for full migration, not permanent dual-stack. [src2]
- Go's standard library `net/http` is production-ready. Unlike PHP where you almost always need a framework (Laravel, Symfony), simple Go APIs can use only the standard library with Go 1.22+'s enhanced router. [src1, src7]
- Go 1.24 introduced Swiss Tables for maps (faster hash maps), `os.Root` for filesystem sandboxing, and `omitzero` JSON tag -- these simplify migration of PHP patterns. [src7]
- Go 1.25 made `GOMAXPROCS` container-aware: on Linux it now reads the cgroup CPU bandwidth limit, so the long-standing `go.uber.org/automaxprocs` workaround is no longer needed on Go 1.25+. Go 1.26 then made the Green Tea garbage collector the default, cutting GC overhead 10-40% with no code change -- both directly benefit high-concurrency services ported from PHP-FPM. [src9]
- Go 1.26 deprecated `httputil.ReverseProxy.Director` in favor of `ReverseProxy.Rewrite`; if you front your PHP backend with a Go-based proxy during the strangler-fig phase, use `Rewrite`. [src9]
- Muzz's migration showed 57% P50 latency reduction but noted the hardest part was keeping both systems running in parallel while shipping features. Budget 2-3x the estimated migration time, and ramp Go traffic gradually (1% -> 10% -> 50% -> 100%) with shadow traffic before promoting each endpoint. [src2, src10]

## Related Units

- [PHP to Node.js Migration](/software/migrations/php-to-nodejs/2026)
- [PHP to Python Migration](/software/migrations/php-to-python/2026)
- [Java to Go Migration](/software/migrations/java-to-go/2026)
- [Go Goroutine Leak Detection](/software/debugging/go-goroutine-leak/2026)
- [jQuery to React Migration](/software/migrations/jquery-to-react/2026)
