ساخت REST API با گولنگ و Gin - آموزش کامل Go
2025/11/24در این آموزش، یک REST API کامل با Go و فریمورک Gin میسازیم. Gin یکی از محبوبترین فریمورکهای وب Go است که سرعت بالا و API سادهای دارد.
پیشنیازها
- نصب Go روی سیستم
- آشنایی با مفاهیم پایه Go
- آشنایی با مفهوم REST 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 | صفحهبندی نتایج |