Introduction
Go continues to be a top choice for building high-performance web services. Its simplicity, speed, and excellent concurrency model make it ideal for APIs, microservices, and web applications. In 2026, the Go web ecosystem has matured significantly with excellent frameworks and tools.
This guide covers Go web development in 2026, from foundational concepts to advanced patterns. Whether you’re building your first Go API or optimizing existing services, this guide provides practical insights.
Go Web Landscape
Why Go for Web?
- Performance: Compiled language, near C speed
- Concurrency: Goroutines and channels
- Simplicity: Minimal syntax, easy to learn
- Standard Library: Excellent built-in HTTP support
- Deployment: Single binary deployment
Popular Frameworks
| Framework | Philosophy | Performance | Maturity |
|---|---|---|---|
| net/http | Standard library | Excellent | Very High |
| Chi | Lightweight, router | Excellent | High |
| Fiber | Express-like | Excellent | High |
| Echo | Full-featured | Very Good | High |
| Gin | Fast router | Excellent | High |
Chi Framework
Getting Started
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
// Global middleware
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.RequestID)
// Routes
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})
r.Route("/api", func(r chi.Router) {
r.Get("/users", listUsers)
r.Post("/users", createUser)
r.Get("/users/{id}", getUser)
r.Put("/users/{id}", updateUser)
r.Delete("/users/{id}", deleteUser)
})
http.ListenAndServe(":3000", r)
}
func listUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`[{"id":1,"name":"Alice"}]`))
}
Chi Router Patterns
// Route groups
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/profile", profileHandler)
r.Put("/profile", updateProfileHandler)
})
// Middleware wrapper
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// URL parameters
func getUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
w.Write([]byte("User: " + id))
}
// Query parameters
func searchUsers(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
limit := r.URL.Query().Get("limit")
w.Write([]byte("Search: " + query + ", Limit: " + limit))
}
Fiber Framework
Express-like API
package main
import (
"github.com/gofiber/fiber/v2"
)
func main() {
app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {
return c.Status(500).JSON(fiber.Map{
"error": err.Error(),
})
},
})
// Middleware
app.Use(func(c *fiber.Ctx) error {
c.Locals("user", "john")
return c.Next()
})
// Routes
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, Fiber!")
})
app.Get("/api/users", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"users": []string{"Alice", "Bob"},
})
})
// POST with body parsing
app.Post("/api/users", func(c *fiber.Ctx) error {
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
var user User
if err := c.BodyParser(&user); err != nil {
return err
}
return c.Status(201).JSON(user)
})
// Groups
api := app.Group("/api")
api.Get("/posts", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"posts": []string{}})
})
app.Listen(":3000")
}
Fiber Features
// Static files
app.Static("/static", "./public")
// Templates
app.Get("/view", func(c *fiber.Ctx) error {
return c.Render("index", fiber.Map{
"title": "Hello",
})
})
// WebSocket
app.Get("/ws", func(c *fiber.Ctx) error {
if _, err := c.Response().Write([]byte("websocket upgrade required")); err != nil {
return err
}
return nil
})
// Stream
app.Get("/stream", func(c *fiber.Ctx) error {
c.Set("Content-Type", "application/json")
stream := `{"event":"message","data":"hello"}`
return c.SendString(stream)
})
Middleware Patterns
Custom Middleware
// Logging middleware
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Create response writer wrapper to capture status
rw := &responseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(rw, r)
duration := time.Since(start)
log.Printf(
"%s %s %d %v",
r.Method,
r.URL.Path,
rw.statusCode,
duration,
)
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// Auth middleware with claims
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Missing token", http.StatusUnauthorized)
return
}
claims, err := validateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Add claims to context
ctx := context.WithValue(r.Context(), "claims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
CORS Middleware
// CORS configuration
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
Data Handling
JSON Handling
// Using struct tags
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
// Response helper
func JSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Printf("JSON encode error: %v", err)
}
}
// Request parsing
func decodeUser(r *http.Request) (User, error) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
return User{}, err
}
return user, nil
}
Database Integration
// SQLx example
import (
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
var db *sqlx.DB
func initDB() {
var err error
db, err = sqlx.Connect("postgres", "user=postgres password=pass dbname=mydb sslmode=disable")
if err != nil {
log.Fatal(err)
}
}
func getUserByID(id int) (User, error) {
var user User
err := db.Get(&user, "SELECT id, name, email FROM users WHERE id = $1", id)
return user, err
}
func createUser(user User) (int, error) {
var id int
err := db.QueryRow(
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
user.Name, user.Email,
).Scan(&id)
return id, err
}
Authentication
JWT Auth
import (
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret = []byte("your-secret-key")
type Claims struct {
UserID int `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
func createToken(userID int, email string) (string, error) {
claims := Claims{
UserID: userID,
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
func validateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, err
}
Error Handling
// Custom error type
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (e *AppError) Error() string {
return e.Message
}
func NewNotFoundError(message string) *AppError {
return &AppError{Code: 404, Message: message}
}
func NewBadRequestError(message string) *AppError {
return &AppError{Code: 400, Message: message}
}
// Error handler middleware
func errorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("Panic: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
Testing
HTTP Testing
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestGetUsers(t *testing.T) {
// Create test server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`[{"id":1,"name":"Alice"}]`))
}))
defer ts.Close()
// Make request
res, err := http.Get(ts.URL)
if err != nil {
t.Fatal(err)
}
// Assert
if res.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", res.StatusCode)
}
}
func TestCreateUser(t *testing.T) {
handler := http.HandlerFunc(createUser)
// Create request with body
body := `{"name":"John","email":"[email protected]"}`
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
// Get response
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
// Assert
if rr.Code != http.StatusCreated {
t.Errorf("Expected status 201, got %d", rr.Code)
}
}
Deployment
Dockerfile
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app
# Runtime stage
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app .
EXPOSE 3000
CMD ["./app"]
Docker Compose
# docker-compose.yml
version: '3'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
External Resources
Documentation
- Go net/http - Standard library
- Chi - Chi router
- Fiber - Fiber framework
Learning
- Go by Example - Hands-on Go
- A Tour of Go - Official tutorial
Tools
- Air - Live reload
- Golangci-lint - Linter
Conclusion
Go web development in 2026 offers excellent frameworks, great tooling, and outstanding performance. Whether you choose Chi for its simplicity or Fiber for its Express-like API, you’ll build robust web services.
Start with the standard library, then add Chi or Fiber as needed. Focus on middleware patterns, error handling, and testing. The Go ecosystem continues to growโstay engaged with the community.
Go’s simplicity and performance make it ideal for modern web development. Build fast, deploy simple, scale effortlessly.
Comments