GoCasts آموزش Go به زبان ساده

بیش از ۱۰۰۰ شرکت‌کننده یادگیری Go و Backend رو از امروز شروع کن
ثبت‌نام دوره + تیم‌سازی

ساخت REST API با گولنگ و Gin - آموزش کامل Go

در این آموزش، یک REST API کامل با Go و فریمورک Gin می‌سازیم. Gin یکی از محبوب‌ترین فریمورک‌های وب Go است که سرعت بالا و API ساده‌ای دارد.

پیش‌نیازها

راه‌اندازی پروژه

# ایجاد پوشه پروژه
mkdir bookstore-api && cd bookstore-api

# مقداردهی Go Module
go mod init github.com/username/bookstore-api

# نصب Gin
go get -u github.com/gin-gonic/gin

ساختار پروژه

bookstore-api/
├── main.go
├── models/
│   └── book.go
├── handlers/
│   └── book_handler.go
├── middleware/
│   └── auth.go
└── go.mod

گام ۱: تعریف Model

ابتدا مدل Book را تعریف می‌کنیم:

models/book.go:

package models

import "time"

type Book struct {
    ID        int       `json:"id"`
    Title     string    `json:"title" binding:"required"`
    Author    string    `json:"author" binding:"required"`
    ISBN      string    `json:"isbn"`
    Price     float64   `json:"price" binding:"required,gt=0"`
    Stock     int       `json:"stock"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// شبیه‌سازی دیتابیس با slice
var Books = []Book{
    {
        ID:        1,
        Title:     "زبان برنامه‌نویسی Go",
        Author:    "آلن دونوان",
        ISBN:      "978-0134190440",
        Price:     45.99,
        Stock:     10,
        CreatedAt: time.Now(),
    },
    {
        ID:        2,
        Title:     "Go Web Programming",
        Author:    "Sau Sheong Chang",
        ISBN:      "978-1617292569",
        Price:     39.99,
        Stock:     15,
        CreatedAt: time.Now(),
    },
}

// شمارنده ID
var NextID = 3

گام ۲: ایجاد Handler‌ها

handlers/book_handler.go:

package handlers

import (
    "net/http"
    "strconv"
    "strings"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/username/bookstore-api/models"
)

// GET /books - لیست همه کتاب‌ها
func GetBooks(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "success": true,
        "data":    models.Books,
        "total":   len(models.Books),
    })
}

// GET /books/:id - دریافت یک کتاب
func GetBook(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "success": false,
            "error":   "شناسه نامعتبر است",
        })
        return
    }

    for _, book := range models.Books {
        if book.ID == id {
            c.JSON(http.StatusOK, gin.H{
                "success": true,
                "data":    book,
            })
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{
        "success": false,
        "error":   "کتاب یافت نشد",
    })
}

// POST /books - ایجاد کتاب جدید
func CreateBook(c *gin.Context) {
    var newBook models.Book

    // Bind و Validate
    if err := c.ShouldBindJSON(&newBook); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "success": false,
            "error":   err.Error(),
        })
        return
    }

    // تنظیم فیلدها
    newBook.ID = models.NextID
    models.NextID++
    newBook.CreatedAt = time.Now()
    newBook.UpdatedAt = time.Now()

    // اضافه به لیست
    models.Books = append(models.Books, newBook)

    c.JSON(http.StatusCreated, gin.H{
        "success": true,
        "message": "کتاب با موفقیت ایجاد شد",
        "data":    newBook,
    })
}

// PUT /books/:id - به‌روزرسانی کتاب
func UpdateBook(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "success": false,
            "error":   "شناسه نامعتبر است",
        })
        return
    }

    var updatedBook models.Book
    if err := c.ShouldBindJSON(&updatedBook); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "success": false,
            "error":   err.Error(),
        })
        return
    }

    for i, book := range models.Books {
        if book.ID == id {
            updatedBook.ID = id
            updatedBook.CreatedAt = book.CreatedAt
            updatedBook.UpdatedAt = time.Now()
            models.Books[i] = updatedBook

            c.JSON(http.StatusOK, gin.H{
                "success": true,
                "message": "کتاب به‌روزرسانی شد",
                "data":    updatedBook,
            })
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{
        "success": false,
        "error":   "کتاب یافت نشد",
    })
}

// DELETE /books/:id - حذف کتاب
func DeleteBook(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "success": false,
            "error":   "شناسه نامعتبر است",
        })
        return
    }

    for i, book := range models.Books {
        if book.ID == id {
            // حذف از slice
            models.Books = append(models.Books[:i], models.Books[i+1:]...)

            c.JSON(http.StatusOK, gin.H{
                "success": true,
                "message": "کتاب حذف شد",
            })
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{
        "success": false,
        "error":   "کتاب یافت نشد",
    })
}

// GET /books/search?q=query - جستجوی کتاب
func SearchBooks(c *gin.Context) {
    query := c.Query("q")
    if query == "" {
        c.JSON(http.StatusBadRequest, gin.H{
            "success": false,
            "error":   "پارامتر جستجو الزامی است",
        })
        return
    }

    var results []models.Book
    for _, book := range models.Books {
        if contains(book.Title, query) || contains(book.Author, query) {
            results = append(results, book)
        }
    }

    c.JSON(http.StatusOK, gin.H{
        "success": true,
        "data":    results,
        "total":   len(results),
    })
}

func contains(s, substr string) bool {
    return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}

گام ۳: ایجاد Middleware

middleware/auth.go:

package middleware

import (
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
)

// Middleware برای لاگ کردن درخواست‌ها
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        // قبل از اجرای handler
        path := c.Request.URL.Path
        method := c.Request.Method

        // اجرای handler بعدی
        c.Next()

        // بعد از اجرای handler
        status := c.Writer.Status()
        log.Printf("%s %s -> %d", method, path, status)
    }
}

// Middleware برای احراز هویت ساده با API Key
func APIKeyAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        apiKey := c.GetHeader("X-API-Key")

        // در پروژه واقعی، کلید را از دیتابیس بخوانید
        validKey := "your-secret-api-key"

        if apiKey == "" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "success": false,
                "error":   "API Key الزامی است",
            })
            c.Abort()
            return
        }

        if apiKey != validKey {
            c.JSON(http.StatusUnauthorized, gin.H{
                "success": false,
                "error":   "API Key نامعتبر است",
            })
            c.Abort()
            return
        }

        c.Next()
    }
}

// Middleware برای CORS
func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
            return
        }

        c.Next()
    }
}

// Middleware برای محدودیت نرخ درخواست
func RateLimit(requestsPerMinute int) gin.HandlerFunc {
    // پیاده‌سازی ساده - در پروژه واقعی از Redis استفاده کنید
    requests := make(map[string]int)

    return func(c *gin.Context) {
        ip := c.ClientIP()

        if requests[ip] >= requestsPerMinute {
            c.JSON(http.StatusTooManyRequests, gin.H{
                "success": false,
                "error":   "تعداد درخواست‌ها بیش از حد مجاز است",
            })
            c.Abort()
            return
        }

        requests[ip]++
        c.Next()
    }
}

گام ۴: فایل اصلی

main.go:

package main

import (
    "log"

    "github.com/gin-gonic/gin"
    "github.com/username/bookstore-api/handlers"
    "github.com/username/bookstore-api/middleware"
)

func main() {
    // ایجاد router
    r := gin.Default()

    // Middleware های عمومی
    r.Use(middleware.CORS())
    r.Use(gin.Recovery())

    // گروه API نسخه 1
    v1 := r.Group("/api/v1")
    {
        // مسیرهای عمومی
        v1.GET("/health", func(c *gin.Context) {
            c.JSON(200, gin.H{
                "status": "ok",
                "version": "1.0.0",
            })
        })

        // مسیرهای کتاب
        books := v1.Group("/books")
        {
            books.GET("", handlers.GetBooks)
            books.GET("/:id", handlers.GetBook)
            books.GET("/search", handlers.SearchBooks)

            // مسیرهای محافظت شده
            protected := books.Group("")
            protected.Use(middleware.APIKeyAuth())
            {
                protected.POST("", handlers.CreateBook)
                protected.PUT("/:id", handlers.UpdateBook)
                protected.DELETE("/:id", handlers.DeleteBook)
            }
        }
    }

    // اجرای سرور
    log.Println("Server running on :8080")
    if err := r.Run(":8080"); err != nil {
        log.Fatal("Server failed to start:", err)
    }
}

تست API

اجرای سرور

go run main.go

تست با cURL

# دریافت همه کتاب‌ها
curl http://localhost:8080/api/v1/books

# دریافت یک کتاب
curl http://localhost:8080/api/v1/books/1

# جستجو
curl "http://localhost:8080/api/v1/books/search?q=Go"

# ایجاد کتاب جدید (نیاز به API Key)
curl -X POST http://localhost:8080/api/v1/books \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-api-key" \
  -d '{
    "title": "Concurrency in Go",
    "author": "Katherine Cox-Buday",
    "isbn": "978-1491941195",
    "price": 35.99,
    "stock": 20
  }'

# به‌روزرسانی کتاب
curl -X PUT http://localhost:8080/api/v1/books/1 \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-api-key" \
  -d '{
    "title": "زبان برنامه‌نویسی Go - ویرایش جدید",
    "author": "آلن دونوان",
    "price": 49.99,
    "stock": 5
  }'

# حذف کتاب
curl -X DELETE http://localhost:8080/api/v1/books/2 \
  -H "X-API-Key: your-secret-api-key"

اضافه کردن Validation پیشرفته

package models

import (
    "github.com/go-playground/validator/v10"
)

type Book struct {
    ID        int       `json:"id"`
    Title     string    `json:"title" binding:"required,min=1,max=200"`
    Author    string    `json:"author" binding:"required,min=1,max=100"`
    ISBN      string    `json:"isbn" binding:"omitempty,isbn13"`
    Price     float64   `json:"price" binding:"required,gt=0,lt=10000"`
    Stock     int       `json:"stock" binding:"gte=0"`
    Category  string    `json:"category" binding:"omitempty,oneof=fiction non-fiction technical"`
    CreatedAt time.Time `json:"created_at"`
}

// Custom validator برای ISBN
func ValidateISBN(fl validator.FieldLevel) bool {
    isbn := fl.Field().String()
    // پیاده‌سازی validation ISBN
    return len(isbn) == 13 || len(isbn) == 10
}

Error Handling مرکزی

package middleware

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        // بررسی خطاها بعد از اجرای handler
        if len(c.Errors) > 0 {
            err := c.Errors.Last()

            var statusCode int
            var message string

            switch err.Type {
            case gin.ErrorTypeBind:
                statusCode = http.StatusBadRequest
                message = "داده‌های ورودی نامعتبر است"
            case gin.ErrorTypePrivate:
                statusCode = http.StatusInternalServerError
                message = "خطای داخلی سرور"
            default:
                statusCode = http.StatusInternalServerError
                message = err.Error()
            }

            c.JSON(statusCode, gin.H{
                "success": false,
                "error": APIError{
                    Code:    statusCode,
                    Message: message,
                },
            })
        }
    }
}

Pagination

func GetBooks(c *gin.Context) {
    // پارامترهای pagination
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))

    if page < 1 {
        page = 1
    }
    if limit < 1 || limit > 100 {
        limit = 10
    }

    // محاسبه offset
    offset := (page - 1) * limit
    total := len(models.Books)

    // برش داده‌ها
    end := offset + limit
    if end > total {
        end = total
    }

    var results []models.Book
    if offset < total {
        results = models.Books[offset:end]
    }

    c.JSON(http.StatusOK, gin.H{
        "success": true,
        "data":    results,
        "pagination": gin.H{
            "page":       page,
            "limit":      limit,
            "total":      total,
            "totalPages": (total + limit - 1) / limit,
        },
    })
}

جمع‌بندی

در این آموزش یاد گرفتیم:

موضوع توضیح
Setup پروژه Go Modules و ساختار پوشه‌ها
CRUD Operations GET, POST, PUT, DELETE
Middleware Auth, CORS, Logger
Validation Gin binding tags
Error Handling مدیریت خطا مرکزی
Pagination صفحه‌بندی نتایج

قدم‌های بعدی

منابع

بیش از ۱۰۰۰ شرکت‌کننده یادگیری Go و Backend رو از امروز شروع کن
ثبت‌نام دوره + تیم‌سازی