Building High-Performance APIs with Go
go api
Go was designed for building services at scale. Its simplicity, fast compilation, and goroutines make it ideal for high-performance APIs. Here’s a practical guide.
Why Go for APIs
package main
import (
"encoding/json"
"log"
"net/http"
)
func main() {
http.HandleFunc("/health", handleHealth)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
No framework needed. Batteries included.
Project Structure
myapi/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── handlers/
│ │ └── user.go
│ ├── models/
│ │ └── user.go
│ ├── repository/
│ │ └── user.go
│ └── middleware/
│ └── auth.go
├── pkg/
│ └── response/
│ └── response.go
├── go.mod
└── go.sum
Clean separation. internal/ is Go’s convention for private packages.
Basic API Structure
Main Entry
// cmd/server/main.go
package main
import (
"log"
"net/http"
"os"
"myapi/internal/handlers"
"myapi/internal/middleware"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
mux := http.NewServeMux()
// Routes
mux.HandleFunc("/api/users", handlers.HandleUsers)
mux.HandleFunc("/api/users/", handlers.HandleUser)
// Middleware chain
handler := middleware.Logging(middleware.Auth(mux))
log.Printf("Server starting on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, handler))
}
Handler
// internal/handlers/user.go
package handlers
import (
"encoding/json"
"net/http"
"strings"
"myapi/internal/models"
"myapi/internal/repository"
"myapi/pkg/response"
)
func HandleUsers(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
listUsers(w, r)
case http.MethodPost:
createUser(w, r)
default:
response.Error(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
func listUsers(w http.ResponseWriter, r *http.Request) {
users, err := repository.GetAllUsers()
if err != nil {
response.Error(w, http.StatusInternalServerError, "Failed to fetch users")
return
}
response.JSON(w, http.StatusOK, users)
}
func createUser(w http.ResponseWriter, r *http.Request) {
var user models.User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
response.Error(w, http.StatusBadRequest, "Invalid JSON")
return
}
if err := user.Validate(); err != nil {
response.Error(w, http.StatusBadRequest, err.Error())
return
}
created, err := repository.CreateUser(&user)
if err != nil {
response.Error(w, http.StatusInternalServerError, "Failed to create user")
return
}
response.JSON(w, http.StatusCreated, created)
}
Response Helper
// pkg/response/response.go
package response
import (
"encoding/json"
"net/http"
)
func JSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func Error(w http.ResponseWriter, status int, message string) {
JSON(w, status, map[string]string{"error": message})
}
Middleware
// internal/middleware/auth.go
package middleware
import (
"context"
"log"
"net/http"
"strings"
"time"
)
type contextKey string
const UserIDKey contextKey = "userID"
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip auth for public endpoints
if strings.HasPrefix(r.URL.Path, "/api/public") {
next.ServeHTTP(w, r)
return
}
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
userID, err := validateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), UserIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Goroutines for Performance
Concurrent Requests
func fetchUserWithDetails(userID string) (*UserDetails, error) {
var user *User
var orders []*Order
var preferences *Preferences
var wg sync.WaitGroup
errs := make(chan error, 3)
wg.Add(3)
go func() {
defer wg.Done()
var err error
user, err = repository.GetUser(userID)
if err != nil {
errs <- err
}
}()
go func() {
defer wg.Done()
var err error
orders, err = repository.GetUserOrders(userID)
if err != nil {
errs <- err
}
}()
go func() {
defer wg.Done()
var err error
preferences, err = repository.GetUserPreferences(userID)
if err != nil {
errs <- err
}
}()
wg.Wait()
close(errs)
for err := range errs {
if err != nil {
return nil, err
}
}
return &UserDetails{
User: user,
Orders: orders,
Preferences: preferences,
}, nil
}
Three requests in parallel instead of sequential.
Worker Pool
func processJobs(jobs <-chan Job, results chan<- Result) {
numWorkers := runtime.NumCPU()
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
result := processJob(job)
results <- result
}
}()
}
wg.Wait()
close(results)
}
Database Access
// internal/repository/user.go
package repository
import (
"database/sql"
"myapi/internal/models"
)
var db *sql.DB
func InitDB(dataSourceName string) error {
var err error
db, err = sql.Open("postgres", dataSourceName)
if err != nil {
return err
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
return db.Ping()
}
func GetUser(id string) (*models.User, error) {
user := &models.User{}
err := db.QueryRow(
"SELECT id, email, name, created_at FROM users WHERE id = $1",
id,
).Scan(&user.ID, &user.Email, &user.Name, &user.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return user, nil
}
func CreateUser(user *models.User) (*models.User, error) {
err := db.QueryRow(
`INSERT INTO users (email, name) VALUES ($1, $2)
RETURNING id, created_at`,
user.Email, user.Name,
).Scan(&user.ID, &user.CreatedAt)
if err != nil {
return nil, err
}
return user, nil
}
Graceful Shutdown
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: setupRouter(),
}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Shutdown error: %v", err)
}
log.Println("Server stopped")
}
Performance Tips
1. Use sync.Pool
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
buf := bufferPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufferPool.Put(buf)
// Use buf...
}
2. Streaming JSON
func streamLargeResponse(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("["))
first := true
for item := range items {
if !first {
w.Write([]byte(","))
}
first = false
json.NewEncoder(w).Encode(item)
}
w.Write([]byte("]"))
}
3. Use a Router Library
// Chi router example
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Route("/api/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Post("/", createUser)
r.Route("/{userID}", func(r chi.Router) {
r.Get("/", getUser)
r.Put("/", updateUser)
r.Delete("/", deleteUser)
})
})
Final Thoughts
Go’s simplicity is its strength. No magic, no hidden behavior—just explicit, readable code that compiles fast and runs faster.
For high-throughput APIs, Go delivers.
Simple code, fast execution, happy operators.