1. Introducing Gin Framework
Gin is the most popular HTTP web framework for Go. Gin stands out for its high performance (using httprouter), intuitive API, and rich middleware ecosystem.
1.1. Compare Gin vs other Frameworks
| Criteria | Gin | Echo | Fiber | Chi |
|---|---|---|---|---|
| Performance | Very high | Very high | Highest (fasthttp) | High |
| Popularity | ⭐ 80k+ | ⭐ 30k+ | ⭐ 34k+ | ⭐ 18k+ |
| API Style | Express-like | Express-like | Express-like | net/http compatible |
| Middleware | Many | Many | Many | Chi compatible |
| net/http compat | Partial | Full | ❌ (fasthttp) | Full |
| Validation | Built-in | Built-in | Built-in | Manual |
| Swagger | swaggo | swaggo | swaggo | swaggo |
When to choose Gin? Most popular, lots of resources, good balance between features and performance. Suitable for most projects.
1.2. Setup
mkdir gin-api && cd gin-api
go mod init github.com/yourname/gin-api
go get github.com/gin-gonic/gin
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
// gin.Default() = Logger + Recovery middleware
r := gin.Default()
// gin.New() = no middleware (production)
// r := gin.New()
// r.Use(gin.Logger(), gin.Recovery())
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run(":8080") // Default :8080
}
2. Routing
func main() {
r := gin.Default()
// Basic routes
r.GET("/users", listUsers)
r.POST("/users", createUser)
r.GET("/users/:id", getUser) // Path parameter
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)
// Route groups
api := r.Group("/api/v1")
{
users := api.Group("/users")
{
users.GET("", listUsers)
users.POST("", createUser)
users.GET("/:id", getUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}
posts := api.Group("/posts")
posts.Use(authMiddleware()) // Middleware cho group
{
posts.GET("", listPosts)
posts.POST("", createPost)
}
}
// Wildcard route
r.GET("/files/*filepath", func(c *gin.Context) {
filepath := c.Param("filepath")
c.String(http.StatusOK, "File: %s", filepath)
})
r.Run(":8080")
}
3. Request Handling
3.1. Parameters
func getUser(c *gin.Context) {
// Path parameter
id := c.Param("id")
// Query parameters
page := c.DefaultQuery("page", "1")
limit := c.DefaultQuery("limit", "10")
search := c.Query("search") // "" if not present
// Header
token := c.GetHeader("Authorization")
c.JSON(http.StatusOK, gin.H{
"id": id,
"page": page,
"limit": limit,
"search": search,
"token": token,
})
}
3.2. Request Binding & Validation
// Gin sử dụng go-playground/validator cho validation
type CreateUserInput struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8,max=72"`
Age int `json:"age" binding:"required,gte=18,lte=120"`
Role string `json:"role" binding:"required,oneof=admin user moderator"`
}
type UpdateUserInput struct {
Name *string `json:"name" binding:"omitempty,min=2,max=50"`
Email *string `json:"email" binding:"omitempty,email"`
Age *int `json:"age" binding:"omitempty,gte=18,lte=120"`
}
// Query parameters binding
type ListUsersQuery struct {
Page int `form:"page" binding:"omitempty,min=1"`
Limit int `form:"limit" binding:"omitempty,min=1,max=100"`
Search string `form:"search" binding:"omitempty,max=200"`
SortBy string `form:"sort_by" binding:"omitempty,oneof=name email created_at"`
}
func createUser(c *gin.Context) {
var input CreateUserInput
// ShouldBindJSON - returns error (không abort)
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Validation failed",
"details": formatValidationErrors(err),
})
return
}
// ... create user
c.JSON(http.StatusCreated, gin.H{
"message": "User created",
"data": input,
})
}
func listUsers(c *gin.Context) {
var query ListUsersQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Set defaults
if query.Page == 0 { query.Page = 1 }
if query.Limit == 0 { query.Limit = 20 }
// ... fetch users
}
// Format validation errors
func formatValidationErrors(err error) map[string]string {
errors := make(map[string]string)
var ve validator.ValidationErrors
if stderrors.As(err, &ve) {
for _, fe := range ve {
field := fe.Field()
switch fe.Tag() {
case "required":
errors[field] = field + " is required"
case "email":
errors[field] = "Invalid email format"
case "min":
errors[field] = field + " must be at least " + fe.Param()
case "max":
errors[field] = field + " must be at most " + fe.Param()
default:
errors[field] = "Invalid " + field
}
}
}
return errors
}
3.3. Custom Validators
import "github.com/go-playground/validator/v10"
// Custom validator
func setupValidators(r *gin.Engine) {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// Custom validation tag
v.RegisterValidation("strong_password", func(fl validator.FieldLevel) bool {
password := fl.Field().String()
hasUpper := false
hasLower := false
hasDigit := false
for _, ch := range password {
switch {
case unicode.IsUpper(ch):
hasUpper = true
case unicode.IsLower(ch):
hasLower = true
case unicode.IsDigit(ch):
hasDigit = true
}
}
return hasUpper && hasLower && hasDigit
})
// Vietnamese phone number
v.RegisterValidation("vn_phone", func(fl validator.FieldLevel) bool {
phone := fl.Field().String()
matched, _ := regexp.MatchString(`^(0|\+84)(3|5|7|8|9)\d{8}$`, phone)
return matched
})
}
}
type RegisterInput struct {
Password string `json:"password" binding:"required,min=8,strong_password"`
Phone string `json:"phone" binding:"required,vn_phone"`
}
4. Middleware
// Auth middleware
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Authorization header required",
})
return
}
// Remove "Bearer " prefix
token = strings.TrimPrefix(token, "Bearer ")
claims, err := validateJWT(token)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Invalid token",
})
return
}
// Set user info in context
c.Set("user_id", claims.UserID)
c.Set("user_role", claims.Role)
c.Next() // Continue to next handler
}
}
// Rate limiting middleware
func rateLimitMiddleware(rps int) gin.HandlerFunc {
limiter := rate.NewLimiter(rate.Limit(rps), rps)
return func(c *gin.Context) {
if !limiter.Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": "Rate limit exceeded",
})
return
}
c.Next()
}
}
// Request ID middleware
func requestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
c.Set("request_id", requestID)
c.Header("X-Request-ID", requestID)
c.Next()
}
}
// Usage
func main() {
r := gin.New()
// Global middleware
r.Use(gin.Logger())
r.Use(gin.Recovery())
r.Use(requestIDMiddleware())
r.Use(corsMiddleware())
// Public routes
r.POST("/auth/login", loginHandler)
r.POST("/auth/register", registerHandler)
// Protected routes
protected := r.Group("/api")
protected.Use(authMiddleware())
protected.Use(rateLimitMiddleware(100))
{
protected.GET("/profile", getProfile)
protected.PUT("/profile", updateProfile)
}
r.Run(":8080")
}
5. Response Handling
// Standardized API Response
type APIResponse struct {
Success bool `json:"success"`
Data any `json:"data,omitempty"`
Error *APIError `json:"error,omitempty"`
Pagination *Pagination `json:"pagination,omitempty"`
}
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]string `json:"details,omitempty"`
}
type Pagination struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
}
// Helper functions
func respondSuccess(c *gin.Context, data any) {
c.JSON(http.StatusOK, APIResponse{
Success: true,
Data: data,
})
}
func respondCreated(c *gin.Context, data any) {
c.JSON(http.StatusCreated, APIResponse{
Success: true,
Data: data,
})
}
func respondError(c *gin.Context, status int, code, message string) {
c.JSON(status, APIResponse{
Success: false,
Error: &APIError{
Code: code,
Message: message,
},
})
}
func respondPaginated(c *gin.Context, data any, page, limit int, total int64) {
totalPages := int(total) / limit
if int(total)%limit > 0 {
totalPages++
}
c.JSON(http.StatusOK, APIResponse{
Success: true,
Data: data,
Pagination: &Pagination{
Page: page,
Limit: limit,
Total: total,
TotalPages: totalPages,
},
})
}
// Usage
func listUsersHandler(c *gin.Context) {
users, total, err := userService.List(c.Request.Context(), page, limit)
if err != nil {
respondError(c, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to fetch users")
return
}
respondPaginated(c, users, page, limit, total)
}
6. Centralized Error Handling
// Custom error types
type AppError struct {
Code int `json:"-"`
ErrCode string `json:"code"`
Message string `json:"message"`
}
func (e *AppError) Error() string {
return e.Message
}
var (
ErrNotFound = &AppError{Code: 404, ErrCode: "NOT_FOUND", Message: "Resource not found"}
ErrUnauthorized = &AppError{Code: 401, ErrCode: "UNAUTHORIZED", Message: "Unauthorized"}
ErrForbidden = &AppError{Code: 403, ErrCode: "FORBIDDEN", Message: "Forbidden"}
ErrBadRequest = &AppError{Code: 400, ErrCode: "BAD_REQUEST", Message: "Bad request"}
)
// Error handling middleware
func errorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next() // Process request
// Check for errors after handler
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
var appErr *AppError
if errors.As(err, &appErr) {
c.JSON(appErr.Code, APIResponse{
Success: false,
Error: &APIError{Code: appErr.ErrCode, Message: appErr.Message},
})
return
}
// Unknown error
c.JSON(http.StatusInternalServerError, APIResponse{
Success: false,
Error: &APIError{Code: "INTERNAL_ERROR", Message: "Internal server error"},
})
}
}
}
// Usage in handler
func getUserHandler(c *gin.Context) {
id := c.Param("id")
user, err := userService.GetByID(c.Request.Context(), id)
if err != nil {
c.Error(err) // Add error to context
return
}
respondSuccess(c, user)
}
7. Project Structure for Gin API
gin-api/
├── cmd/
│ └── api/
│ └── main.go # Entry point
├── internal/
│ ├── config/
│ │ └── config.go # App configuration
│ ├── handler/
│ │ ├── user_handler.go # HTTP handlers
│ │ ├── auth_handler.go
│ │ └── router.go # Route definitions
│ ├── middleware/
│ │ ├── auth.go
│ │ ├── cors.go
│ │ ├── logger.go
│ │ └── ratelimit.go
│ ├── model/
│ │ ├── user.go # Domain models
│ │ └── post.go
│ ├── repository/
│ │ ├── user_repository.go # Data access
│ │ └── post_repository.go
│ ├── service/
│ │ ├── user_service.go # Business logic
│ │ └── auth_service.go
│ └── dto/
│ ├── request.go # Request DTOs
│ └── response.go # Response DTOs
├── pkg/
│ ├── database/
│ │ └── postgres.go
│ └── validator/
│ └── custom.go
├── migrations/
├── docs/ # Swagger generated
├── go.mod
├── go.sum
├── Makefile
└── Dockerfile
8. Summary
- Gin: Most popular framework, high performance, rich middleware
- Routing: Groups, path params, query params, wildcards
- Binding: ShouldBindJSON, ShouldBindQuery with validator tags
- Middleware: Auth, rate limiting, CORS, request ID, error handling
- Response: Standardized API response format
- Project structure: Handler → Service → Repository layers
Next part: GORM & Database Integration — connect Go to PostgreSQL and process the data layer.