Go’s standard library includes a production-ready HTTP server with support for HTTP/2, TLS, and concurrent request handling — no external framework is required for simple web applications. For more complex applications, the Gin framework adds routing, middleware, request binding, and response rendering while maintaining excellent performance. Go web applications compile to a single binary, making deployment as simple as copying one file to the server. This guide covers building a complete Go REST API with Gin, implementing JSON request/response handling, middleware (logging, rate limiting), graceful shutdown, and deploying it behind Nginx with systemd management on RHEL 9.
Prerequisites
- Go 1.22 and Nginx installed on RHEL 9
Step 1 — Initialise the Project
mkdir /var/www/mygoapi && cd /var/www/mygoapi
go mod init github.com/mycompany/mygoapi
go get github.com/gin-gonic/gin
go get github.com/gin-contrib/cors
Step 2 — Build a Structured REST API
# main.go — structured Go REST API with Gin
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
type Item struct {
ID int `json:"id"`
Name string `json:"name" binding:"required"`
}
var items = []Item{{ID: 1, Name: "Widget"}, {ID: 2, Name: "Gadget"}}
func setupRouter() *gin.Engine {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())
r.Use(cors.Default())
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
api := r.Group("/api/v1")
{
api.GET("/items", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"items": items, "count": len(items)})
})
api.POST("/items", func(c *gin.Context) {
var newItem Item
if err := c.ShouldBindJSON(&newItem); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
newItem.ID = len(items) + 1
items = append(items, newItem)
c.JSON(http.StatusCreated, newItem)
})
}
return r
}
func main() {
srv := &http.Server{
Addr: ":" + getEnv("PORT", "8080"),
Handler: setupRouter(),
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %v", err)
}
}()
// Graceful shutdown on SIGTERM/SIGINT
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)
}
func getEnv(key, fallback string) string {
if val := os.Getenv(key); val != "" {
return val
}
return fallback
}
Step 3 — Build and Test
go mod tidy
go build -ldflags="-s -w" -o mygoapi . # -s -w strips debug symbols (smaller binary)
ls -lh mygoapi
./mygoapi &
curl http://localhost:8080/health
curl http://localhost:8080/api/v1/items
curl -X POST http://localhost:8080/api/v1/items
-H 'Content-Type: application/json'
-d '{"name":"Sprocket"}'
Step 4 — Nginx Reverse Proxy
# /etc/nginx/conf.d/mygoapi.conf
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 30;
}
}
Conclusion
A Go REST API on RHEL 9 with Gin, graceful shutdown, and Nginx can handle tens of thousands of requests per second on modest hardware. The -ldflags="-s -w" build flags reduce binary size by stripping the symbol table and DWARF debug information. Graceful shutdown (waiting for in-flight requests to complete before exiting) is essential for zero-downtime deployments — when systemd sends SIGTERM to restart the service, the application finishes serving all active requests before stopping.
Next steps: How to Install Go on RHEL 9, How to Install Nginx on RHEL 9, and How to Deploy with Docker on RHEL 9.