Website Backend Development with Go (Gin)
Go and Gin are the choice when performance and predictability matter more than coding speed. Go compiles to a static binary without dependencies, consumes ~10–20 MB memory on startup (versus 200–500 MB for JVM applications), handles thousands of concurrent connections on goroutines. Gin is the most common HTTP framework for Go with minimal overhead over the standard library.
Typical scenarios: public APIs with high load, microservices, replacement of heavy Node.js/Python servers during scaling.
Project Structure
Go projects are organized by functional domains, not technical layers:
cmd/
api/
main.go # entry point
internal/
config/
config.go # configuration via envconfig or viper
domain/
product/
handler.go # HTTP handlers
service.go # business logic
repository.go
model.go
user/
order/
middleware/
auth.go
logger.go
recovery.go
database/
postgres.go
migrations/
server/
router.go # register all routes
server.go
pkg/
validator/
response/
Main Server
// cmd/api/main.go
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/myapp/internal/config"
"github.com/myapp/internal/database"
"github.com/myapp/internal/server"
)
func main() {
cfg := config.Load()
db := database.NewPostgres(cfg.DatabaseURL)
defer db.Close()
srv := server.New(cfg, db)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}
Router
// internal/server/router.go
package server
import (
"github.com/gin-gonic/gin"
"github.com/myapp/internal/middleware"
)
func (s *Server) setupRouter() *gin.Engine {
if s.cfg.Env == "production" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(middleware.Logger())
r.Use(middleware.Recovery())
r.Use(middleware.CORS(s.cfg.AllowedOrigins))
v1 := r.Group("/api/v1")
{
auth := v1.Group("/auth")
auth.POST("/login", s.authHandler.Login)
auth.POST("/refresh", s.authHandler.Refresh)
products := v1.Group("/products")
products.GET("", s.productHandler.List)
products.GET("/:id", s.productHandler.Get)
products.Use(middleware.JWT(s.cfg.JWTSecret))
{
products.POST("", middleware.RequireRole("admin"), s.productHandler.Create)
products.PUT("/:id", middleware.RequireRole("admin"), s.productHandler.Update)
products.DELETE("/:id", middleware.RequireRole("admin"), s.productHandler.Delete)
}
}
return r
}
Handler
// internal/domain/product/handler.go
package product
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
type ListQuery struct {
Page int `form:"page,default=1" binding:"min=1"`
Limit int `form:"limit,default=20" binding:"min=1,max=100"`
CategoryID *int `form:"category_id"`
Search string `form:"search"`
}
func (h *Handler) List(c *gin.Context) {
var q ListQuery
if err := c.ShouldBindQuery(&q); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
products, total, err := h.service.List(c.Request.Context(), ListParams{
Page: q.Page,
Limit: q.Limit,
CategoryID: q.CategoryID,
Search: q.Search,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
return
}
c.JSON(http.StatusOK, gin.H{
"data": products,
"pagination": gin.H{
"page": q.Page,
"limit": q.Limit,
"total": total,
},
})
}
type CreateRequest struct {
Name string `json:"name" binding:"required,min=2,max=255"`
Price float64 `json:"price" binding:"required,gt=0"`
CategoryID *int `json:"category_id"`
Description string `json:"description" binding:"max=5000"`
}
func (h *Handler) Create(c *gin.Context) {
var req CreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": parseValidationErrors(err)})
return
}
product, err := h.service.Create(c.Request.Context(), req)
if err != nil {
handleServiceError(c, err)
return
}
c.JSON(http.StatusCreated, product)
}
Repository and pgx
// internal/domain/product/repository.go
package product
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
)
type Repository struct {
db *pgxpool.Pool
}
func (r *Repository) FindAll(ctx context.Context, params ListParams) ([]*Product, int, error) {
offset := (params.Page - 1) * params.Limit
var countQuery = `SELECT COUNT(*) FROM products WHERE is_active = true`
var listQuery = `
SELECT p.id, p.name, p.slug, p.price, p.created_at,
c.id as category_id, c.name as category_name
FROM products p
LEFT JOIN categories c ON c.id = p.category_id
WHERE p.is_active = true
ORDER BY p.created_at DESC
LIMIT $1 OFFSET $2
`
var total int
if err := r.db.QueryRow(ctx, countQuery).Scan(&total); err != nil {
return nil, 0, err
}
rows, err := r.db.Query(ctx, listQuery, params.Limit, offset)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var products []*Product
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Slug, &p.Price, &p.CreatedAt,
&p.Category.ID, &p.Category.Name); err != nil {
return nil, 0, err
}
products = append(products, &p)
}
return products, total, rows.Err()
}
pgx/v5 — fastest PostgreSQL driver for Go. pgxpool manages connection pooling.
JWT Middleware
// internal/middleware/auth.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserID int `json:"sub"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func JWT(secret string) gin.HandlerFunc {
return func(c *gin.Context) {
auth := c.GetHeader("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
token, err := jwt.ParseWithClaims(auth[7:], &Claims{}, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(secret), nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
claims := token.Claims.(*Claims)
c.Set("userID", claims.UserID)
c.Set("role", claims.Role)
c.Next()
}
}
func RequireRole(roles ...string) gin.HandlerFunc {
roleSet := make(map[string]struct{}, len(roles))
for _, r := range roles {
roleSet[r] = struct{}{}
}
return func(c *gin.Context) {
role, _ := c.Get("role")
if _, ok := roleSet[role.(string)]; !ok {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
c.Next()
}
}
Testing
func TestProductHandler_List(t *testing.T) {
gin.SetMode(gin.TestMode)
mockService := &MockProductService{}
h := NewHandler(mockService)
r := gin.New()
r.GET("/products", h.List)
mockService.On("List", mock.Anything, mock.AnythingOfType("ListParams")).
Return([]*Product{{ID: 1, Name: "Test"}}, 1, nil)
req := httptest.NewRequest(http.MethodGet, "/products?page=1&limit=20", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// ...
}
Development Timeline
- Structure + configuration + DB — 3–5 days
- Handlers + router + middleware — 1–1.5 weeks
- Business logic + repository — 1–3 weeks
- Tests — 1 week
- Docker + CI — 2–3 days
API for website or service: 4–8 weeks. Go requires more code than Python/Node.js, but provides a binary with predictable performance and minimal operational costs.







