ساخت یک API RESTful با Go: راهنمای گام به گام

فهرست مطالب

ساخت یک API RESTful با Go: راهنمای گام به گام

در دنیای مدرن توسعه نرم‌افزار، رابط‌های برنامه‌نویسی کاربردی (APIها) ستون فقرات ارتباط بین سیستم‌های مختلف را تشکیل می‌دهند. از برنامه‌های موبایل گرفته تا وب‌سایت‌های پیچیده و سرویس‌های میکروسرویس، همه و همه به APIها برای تبادل داده متکی هستند. در میان انواع مختلف APIها، RESTful APIها به دلیل سادگی، مقیاس‌پذیری و استفاده از پروتکل HTTP استاندارد، به محبوب‌ترین انتخاب تبدیل شده‌اند.

Go، یا Golang، زبانی است که توسط گوگل توسعه یافته و به دلیل عملکرد بالا، همزمانی داخلی (built-in concurrency)، و سادگی ساختار، به سرعت در میان توسعه‌دهندگان بک‌اند و سیستم‌های توزیع شده محبوبیت یافته است. این زبان، با کتابخانه استاندارد قدرتمند خود، ابزاری عالی برای ساخت RESTful APIهای کارآمد و قابل نگهداری ارائه می‌دهد.

در این راهنمای جامع، ما قدم به قدم با شما همراه خواهیم بود تا یک RESTful API کامل با Go بسازیم. این پست برای توسعه‌دهندگانی طراحی شده است که با Go آشنایی قبلی دارند یا حداقل با مفاهیم پایه‌ای برنامه‌نویسی و HTTP آشنا هستند و تمایل دارند قدرت Go را در ساخت APIها کشف کنند. از راه‌اندازی محیط توسعه گرفته تا طراحی API، پیاده‌سازی منطق اصلی، استفاده از فریم‌ورک‌های روتر، اتصال به پایگاه داده و حتی ملاحظات تست و استقرار، همه جنبه‌ها را پوشش خواهیم داد. هدف ما این است که شما را با دانش و ابزارهای لازم برای ساخت APIهای قدرتمند و مقیاس‌پذیر مجهز کنیم.

مقدمه‌ای بر RESTful APIها: اصول و مفاهیم کلیدی

قبل از اینکه وارد کدنویسی شویم، درک عمیقی از ماهیت REST و اصول آن ضروری است. REST (Representational State Transfer) یک سبک معماری برای سیستم‌های توزیع شده است و نه یک پروتکل. این سبک معماری بر پایه اصول خاصی بنا شده است که به ما امکان می‌دهد APIهایی بسازیم که مقیاس‌پذیر، قابل نگهداری و مستقل از پلتفرم باشند.

منابع (Resources)

در REST، همه چیز یک منبع است. یک منبع می‌تواند هر چیزی باشد که بتوانید به آن نامی منحصربه‌فرد (URL) اختصاص دهید، مانند یک کاربر، یک محصول، یک سفارش و غیره. به عنوان مثال، /users می‌تواند منبعی برای لیست کاربران باشد، و /users/123 منبعی برای یک کاربر خاص با شناسه 123.

شناسایی منبع (Resource Identification)

منابع با استفاده از URLها (Uniform Resource Locators) شناسایی می‌شوند. هر URL باید به صورت منحصربه‌فرد یک منبع را مشخص کند. به عنوان مثال:

  • /api/v1/books: برای دسترسی به مجموعه کتاب‌ها.
  • /api/v1/books/123: برای دسترسی به کتابی خاص با شناسه 123.

ارتباط بدون حالت (Stateless Communication)

یکی از مهمترین اصول REST، بدون حالت بودن است. به این معنی که هر درخواست کلاینت به سرور باید حاوی تمام اطلاعات لازم برای پردازش آن درخواست باشد. سرور نباید هیچ اطلاعاتی در مورد وضعیت قبلی کلاینت ذخیره کند. این ویژگی باعث می‌شود APIها مقیاس‌پذیرتر و مقاوم‌تر در برابر خطا باشند، زیرا هر سروری می‌تواند هر درخواستی را پردازش کند و نیازی به حفظ سشن‌ها در سمت سرور نیست.

رابط یکنواخت (Uniform Interface)

این اصل به چهار زیربنای اصلی تقسیم می‌شود:

  1. شناسایی منابع در درخواست‌ها (Identification of resources in requests): منابع باید توسط URLها شناسایی شوند.
  2. دستکاری منابع از طریق نمایش (Manipulation of resources through representations): کلاینت با دریافت نمایش (Representation) یک منبع (مثلاً یک شیء JSON یا XML) می‌تواند آن را تغییر داده و دوباره به سرور ارسال کند.
  3. پیام‌های خودتوصیف‌گر (Self-descriptive messages): هر پیام (درخواست یا پاسخ) باید شامل تمام اطلاعات لازم برای تفسیر خود باشد، از جمله متدهای HTTP (GET, POST, PUT, DELETE) و کدهای وضعیت (Status Codes).
  4. هایپرمیدیا به عنوان موتور وضعیت برنامه (Hypermedia as the Engine of Application State – HATEOAS): این اصل که کمی پیشرفته‌تر است، بیان می‌کند که پاسخ‌های API باید شامل لینک‌هایی باشند که کلاینت را به عملیات‌های بعدی یا منابع مرتبط هدایت کنند. مثلاً، در پاسخ GET یک کاربر، لینک‌هایی برای “ویرایش کاربر” یا “حذف کاربر” قرار گیرد.

متدهای HTTP (HTTP Methods)

REST از متدهای استاندارد HTTP برای انجام عملیات CRUD (Create, Read, Update, Delete) بر روی منابع استفاده می‌کند:

  • GET: بازیابی یک یا چند منبع. (بدون تغییر حالت سرور، ایمن و Idempotent)
  • POST: ایجاد یک منبع جدید. (ممکن است حالت سرور را تغییر دهد، غیر-Idempotent)
  • PUT: به روزرسانی کامل یک منبع موجود یا ایجاد آن در صورت عدم وجود. (Idempotent)
  • PATCH: به روزرسانی جزئی یک منبع موجود. (غیر-Idempotent)
  • DELETE: حذف یک منبع. (Idempotent)

کدهای وضعیت HTTP (HTTP Status Codes)

سرور باید با استفاده از کدهای وضعیت HTTP، نتیجه عملیات را به کلاینت اطلاع دهد. برخی از کدهای رایج:

  • 200 OK: درخواست با موفقیت انجام شد.
  • 201 Created: منبع جدید با موفقیت ایجاد شد (معمولاً پس از POST).
  • 204 No Content: درخواست با موفقیت انجام شد اما پاسخی برای ارسال وجود ندارد (معمولاً پس از DELETE).
  • 400 Bad Request: درخواست نامعتبر است.
  • 401 Unauthorized: احراز هویت ناموفق.
  • 403 Forbidden: احراز هویت موفق، اما کاربر اجازه دسترسی ندارد.
  • 404 Not Found: منبع درخواستی یافت نشد.
  • 500 Internal Server Error: خطایی در سمت سرور رخ داد.

چرا Go یک انتخاب عالی برای ساخت APIهای RESTful است؟

با درک اصول REST، اکنون می‌توانیم به این بپردازیم که چرا Go برای پیاده‌سازی این اصول و ساخت APIهای کارآمد، گزینه‌ای بسیار مطلوب است.

همزمانی داخلی (Built-in Concurrency)

Go از ابتدای طراحی، مفهوم همزمانی را در هسته خود جای داده است. Goroutines (روال‌های سبک و کارآمد) و Channels (راهی امن برای ارتباط بین گوروتین‌ها) به توسعه‌دهندگان اجازه می‌دهند تا به راحتی برنامه‌هایی با قابلیت همزمانی بالا بنویسند. این ویژگی برای APIهایی که نیاز به مدیریت تعداد زیادی درخواست همزمان دارند، حیاتی است. سرورهای وب Go می‌توانند با حداقل سربار، تعداد زیادی درخواست را به صورت موازی پردازش کنند.

عملکرد بالا (High Performance)

Go یک زبان کامپایل شده است که به کد ماشین بومی کامپایل می‌شود. این بدان معناست که برنامه‌های Go بسیار سریع و با کمترین سربار اجرا می‌شوند. استفاده کارآمد از حافظه و مدیریت گاربج کالکشن (Garbage Collection) بهینه نیز به عملکرد بالای Go کمک می‌کند. برای APIهایی که نیاز به پاسخگویی سریع و توان عملیاتی بالا (high throughput) دارند، Go یک انتخاب عالی است.

کتابخانه استاندارد قدرتمند (Robust Standard Library)

کتابخانه استاندارد Go، به خصوص پکیج net/http، قابلیت‌های کامل و قدرتمندی برای ساخت سرورهای وب و کلاینت‌های HTTP ارائه می‌دهد. شما می‌توانید بدون نیاز به هیچ فریم‌ورک خارجی، یک RESTful API کامل را با استفاده از پکیج‌های استاندارد Go پیاده‌سازی کنید. این امر باعث می‌شود که وابستگی‌ها کمتر شده و نگهداری کد آسان‌تر شود.

سادگی و خوانایی (Simplicity and Readability)

سینتکس Go ساده، تمیز و بدون پیچیدگی‌های غیرضروری است. این سادگی، خوانایی کد را افزایش داده و همکاری تیمی را آسان‌تر می‌کند. همچنین، وجود ابزارهایی مانند go fmt برای فرمت‌بندی کد و go vet برای یافتن خطاهای رایج، به حفظ کیفیت کد کمک می‌کنند.

زبان تایپ شده استاتیک (Statically Typed Language)

Go یک زبان تایپ شده استاتیک است، به این معنی که بررسی نوع (type checking) در زمان کامپایل انجام می‌شود. این ویژگی به شناسایی بسیاری از خطاهای رایج در مراحل اولیه توسعه کمک کرده و منجر به تولید نرم‌افزار قابل اطمینان‌تری می‌شود.

اکوسیستم در حال رشد (Growing Ecosystem)

با وجود اینکه Go نسبت به زبان‌هایی مانند Java یا Python جوان‌تر است، اکوسیستم آن به سرعت در حال رشد است. تعداد زیادی کتابخانه، فریم‌ورک و ابزار برای توسعه وب، پایگاه داده، میکروسرویس‌ها و موارد دیگر در دسترس هستند.

تنظیم محیط توسعه Go

برای شروع کار با Go، ابتدا باید محیط توسعه خود را آماده کنید.

نصب Go

آخرین نسخه Go را از وب‌سایت رسمی golang.org/dl/ دانلود و نصب کنید. دستورالعمل‌های نصب برای سیستم‌عامل‌های مختلف (Windows, macOS, Linux) در این سایت موجود است.

پس از نصب، با اجرای دستور زیر در ترمینال، از نصب صحیح آن اطمینان حاصل کنید:

go version

باید نسخه‌ای شبیه به go version go1.22.4 linux/amd64 را مشاهده کنید.

Go Modules

Go Modules روش استاندارد مدیریت وابستگی‌ها در Go است. برای شروع یک پروژه جدید:

  1. یک دایرکتوری برای پروژه خود ایجاد کنید:
  2. mkdir go-rest-api
    cd go-rest-api
  3. فایل go.mod را با دستور زیر مقداردهی اولیه کنید:
  4. go mod init github.com/your-username/go-rest-api

    (نام ماژول را به مسیری که کد شما در آن ذخیره می‌شود، تغییر دهید؛ مثلاً نام ریپازیتوری گیت‌هاب شما.)

این دستور یک فایل go.mod ایجاد می‌کند که وابستگی‌های پروژه شما را ردیابی می‌کند.

ویرایشگر کد (IDE)

برای توسعه Go، انتخاب یک ویرایشگر کد مناسب بسیار مهم است. گزینه‌های محبوب شامل:

  • Visual Studio Code (VS Code): رایگان، متن‌باز، و دارای افزونه Go بسیار قدرتمند که قابلیت‌هایی مانند تکمیل خودکار کد، دیباگینگ، و فرمت‌بندی خودکار را ارائه می‌دهد.
  • GoLand: یک IDE تجاری از JetBrains که به طور خاص برای Go طراحی شده و امکانات پیشرفته‌ای را فراهم می‌کند.

توصیه می‌شود از VS Code به همراه افزونه Go استفاده کنید.

طراحی API: تعریف منابع و مسیرها

برای این راهنما، ما یک API ساده برای مدیریت کتاب‌ها (Books) ایجاد خواهیم کرد. هر کتاب دارای ویژگی‌های زیر خواهد بود:

  • ID (شناسه منحصر به فرد)
  • Title (عنوان کتاب)
  • Author (نویسنده کتاب)
  • ISBN (شماره استاندارد بین‌المللی کتاب)

ساختار داده (Struct)

در Go، ما این ساختار را با استفاده از یک struct تعریف می‌کنیم:

package main

type Book struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Author *Author `json:"author"` // یک کتاب یک نویسنده دارد
    ISBN   string  `json:"isbn"`
}

type Author struct {
    FirstName string `json:"firstName"`
    LastName  string `json:"lastName"`
}

تگ‌های `json:"..."` به Go کمک می‌کنند تا هنگام تبدیل ساختارها به JSON و بالعکس (marshal/unmarshal)، نام فیلدها را به صورت استاندارد JSON (camelCase) نگاشت کند.

نقاط پایانی API (API Endpoints)

بر اساس اصول REST، ما نقاط پایانی (endpoints) زیر را برای مدیریت کتاب‌ها تعریف می‌کنیم:

  • GET /books: بازیابی لیست تمام کتاب‌ها.
  • GET /books/{id}: بازیابی اطلاعات یک کتاب خاص با استفاده از شناسه آن.
  • POST /books: ایجاد یک کتاب جدید.
  • PUT /books/{id}: به‌روزرسانی اطلاعات یک کتاب موجود.
  • DELETE /books/{id}: حذف یک کتاب.

ساخت منطق اصلی API با `net/http`

اکنون زمان آن رسیده است که کدنویسی را شروع کنیم. ما ابتدا از پکیج net/http استاندارد Go استفاده خواهیم کرد که برای سرورهای وب بسیار قدرتمند و بهینه است.

فایل `main.go`

یک فایل به نام main.go در ریشه پروژه خود ایجاد کنید.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings" 
    "github.com/google/uuid"
)

type Author struct {
    FirstName string `json:"firstName"`
    LastName  string `json:"lastName"`
}

type Book struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Author *Author `json:"author"`
    ISBN   string  `json:"isbn"`
}

var books []Book

func main() {
    books = append(books, Book{
        ID:    uuid.New().String(),
        Title: "The Go Programming Language",
        Author: &Author{
            FirstName: "Alan",
            LastName:  "A.A. Donovan",
        },
        ISBN: "978-0134190440",
    })
    books = append(books, Book{
        ID:    uuid.New().String(),
        Title: "Building Microservices",
        Author: &Author{
            FirstName: "Sam",
            LastName:  "Newman",
        },
        ISBN: "978-1491950357",
    })

    http.HandleFunc("/books", func(w http.ResponseWriter, r *http.Request) {
        if r.Method == "GET" {
            getBooks(w, r)
        } else if r.Method == "POST" {
            createBook(w, r)
        } else {
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        }
    })
    http.HandleFunc("/books/", handleBook)

    fmt.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func getBooks(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(books)
}

func handleBook(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    pathSegments := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
    if len(pathSegments) < 2 {
        http.Error(w, "Invalid URL", http.StatusBadRequest)
        return
    }
    id := pathSegments[len(pathSegments)-1]

    switch r.Method {
    case "GET":
        getBook(w, r, id)
    case "PUT":
        updateBook(w, r, id)
    case "DELETE":
        deleteBook(w, r, id)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func getBook(w http.ResponseWriter, r *http.Request, id string) {
    for _, item := range books {
        if item.ID == id {
            json.NewEncoder(w).Encode(item)
            return
        }
    }
    http.Error(w, "Book not found", http.StatusNotFound)
}

func createBook(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    var book Book
    err := json.NewDecoder(r.Body).Decode(&book)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    book.ID = uuid.New().String()
    books = append(books, book)
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(book)
}

func updateBook(w http.ResponseWriter, r *http.Request, id string) {
    var updatedBook Book
    err := json.NewDecoder(r.Body).Decode(&updatedBook)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    found := false
    for i, item := range books {
        if item.ID == id {
            updatedBook.ID = item.ID
            books[i] = updatedBook
            found = true
            json.NewEncoder(w).Encode(updatedBook)
            return
        }
    }
    if !found {
        http.Error(w, "Book not found", http.StatusNotFound)
    }
}

func deleteBook(w http.ResponseWriter, r *http.Request, id string) {
    found := false
    for i, item := range books {
        if item.ID == id {
            books = append(books[:i], books[i+1:]...)
            found = true
            w.WriteHeader(http.StatusNoContent)
            return
        }
    }
    if !found {
        http.Error(w, "Book not found", http.StatusNotFound)
    }
}

توضیحات کد بالا:

  • package main و main function: نقطه شروع هر برنامه Go.
  • Book و Author structs: ساختار داده‌های ما را تعریف می‌کنند.
  • books []Book: یک اسلایس سراسری برای ذخیره موقت کتاب‌ها در حافظه. در یک برنامه واقعی، این قسمت با یک پایگاه داده جایگزین می‌شود.
  • http.HandleFunc: این تابع یک الگو URL را به یک تابع هندلر نگاشت می‌کند. در اینجا، /books برای عملیات‌های روی مجموعه کتاب‌ها و /books/ برای عملیات روی یک کتاب خاص استفاده شده است.
  • http.ListenAndServe(":8080", nil): سرور را روی پورت 8080 شروع می‌کند. nil به این معنی است که از DefaultServeMux استفاده شود، که توسط http.HandleFunc پر شده است.
  • توابع هندلر (getBooks, handleBook, getBook, createBook, updateBook, deleteBook):

    • w http.ResponseWriter: برای ارسال پاسخ به کلاینت استفاده می‌شود.
    • r *http.Request: شامل تمام اطلاعات درخواست کلاینت (متد، هدرها، بدنه درخواست و غیره) است.
    • w.Header().Set("Content-Type", "application/json"): هدر Content-Type پاسخ را به application/json تنظیم می‌کند.
    • json.NewEncoder(w).Encode(...): یک ساختار Go را به JSON تبدیل کرده و آن را به پاسخ می‌نویسد.
    • json.NewDecoder(r.Body).Decode(&book): بدنه درخواست JSON را می‌خواند و آن را به یک ساختار Go تبدیل می‌کند.
    • http.Error(...): راهی برای ارسال پاسخ‌های خطا به کلاینت با یک کد وضعیت HTTP و پیام خطا.
    • uuid.New().String(): برای تولید شناسه‌های منحصر به فرد (GUIDs/UUIDs) استفاده می‌شود. باید پکیج github.com/google/uuid را با go get github.com/google/uuid نصب کنید.

اجرای برنامه

برای اجرای این API، در ترمینال در دایرکتوری پروژه خود، دستور زیر را اجرا کنید:

go run main.go

سپس می‌توانید با ابزارهایی مانند Postman، Insomnia، curl یا حتی مرورگر خود، API را تست کنید.

  • GET http://localhost:8080/books
  • POST http://localhost:8080/books (با بدنه JSON)
  • GET http://localhost:8080/books/{id}
  • PUT http://localhost:8080/books/{id} (با بدنه JSON)
  • DELETE http://localhost:8080/books/{id}

استفاده از روترها: بهبود مسیریابی با Gorilla Mux

همانطور که در مثال قبلی دیدید، مدیریت مسیرهای پویا (مانند /books/{id}) با net/http کمی پیچیده است. همچنین، مدیریت متدهای HTTP در یک هندلر واحد، کد را کمی ناخوانا می‌کند. برای حل این مشکلات و افزودن قابلیت‌هایی مانند middleware، می‌توانیم از یک روتر (Router) خارجی استفاده کنیم. Gorilla Mux یکی از محبوب‌ترین و قدرتمندترین روترها برای Go است.

نصب Gorilla Mux

با استفاده از go get آن را نصب کنید:

go get github.com/gorilla/mux

بازنویسی `main.go` با Gorilla Mux

حالا کد main.go را برای استفاده از Gorilla Mux بازنویسی می‌کنیم:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"github.com/google/uuid"
	"github.com/gorilla/mux"
)

type Author struct {
    FirstName string `json:"firstName"`
    LastName  string `json:"lastName"`
}

type Book struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Author *Author `json:"author"`
    ISBN   string  `json:"isbn"`
}

var books []Book

func main() {
    books = append(books, Book{
        ID:    uuid.New().String(),
        Title: "The Go Programming Language",
        Author: &Author{
            FirstName: "Alan",
            LastName:  "A.A. Donovan",
        },
        ISBN: "978-0134190440",
    })
    books = append(books, Book{
        ID:    uuid.New().String(),
        Title: "Building Microservices",
        Author: &Author{
            FirstName: "Sam",
            LastName:  "Newman",
        },
        ISBN: "978-1491950357",
    })

    router := mux.NewRouter()

    router.HandleFunc("/books", getBooks).Methods("GET")
    router.HandleFunc("/books/{id}", getBook).Methods("GET")
    router.HandleFunc("/books", createBook).Methods("POST")
    router.HandleFunc("/books/{id}", updateBook).Methods("PUT")
    router.HandleFunc("/books/{id}", deleteBook).Methods("DELETE")

    fmt.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", router))
}

func getBooks(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(books)
}

func getBook(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    vars := mux.Vars(r)
    id := vars["id"]

    for _, item := range books {
        if item.ID == id {
            json.NewEncoder(w).Encode(item)
            return
        }
    }
    http.Error(w, "Book not found", http.StatusNotFound)
}

func createBook(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    var book Book
    err := json.NewDecoder(r.Body).Decode(&book)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    book.ID = uuid.New().String()
    books = append(books, book)
    
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(book)
}

func updateBook(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    vars := mux.Vars(r)
    id := vars["id"]

    var updatedBook Book
    err := json.NewDecoder(r.Body).Decode(&updatedBook)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    found := false
    for i, item := range books {
        if item.ID == id {
            updatedBook.ID = item.ID
            books[i] = updatedBook
            found = true
            json.NewEncoder(w).Encode(updatedBook)
            return
        }
    }
    if !found {
        http.Error(w, "Book not found", http.StatusNotFound)
    }
}

func deleteBook(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    vars := mux.Vars(r)
    id := vars["id"]

    found := false
    for i, item := range books {
        if item.ID == id {
            books = append(books[:i], books[i+1:]...)
            found = true
            w.WriteHeader(http.StatusNoContent)
            return
        }
    }
    if !found {
        http.Error(w, "Book not found", http.StatusNotFound)
    }
}

تغییرات کلیدی با Gorilla Mux:

  • router := mux.NewRouter(): یک نمونه جدید از روتر Mux ایجاد می‌کند.
  • router.HandleFunc("/books", getBooks).Methods("GET"): این خط به Mux می‌گوید که تابع getBooks فقط باید برای درخواست‌های GET به مسیر /books فراخوانی شود.
  • router.HandleFunc("/books/{id}", getBook).Methods("GET"): این مورد نشان می‌دهد که چگونه Mux از متغیرهای مسیر (مانند {id}) پشتیبانی می‌کند.
  • mux.Vars(r): در داخل توابع هندلر، می‌توانید به راحتی به متغیرهای مسیر مانند id با استفاده از mux.Vars(r) دسترسی پیدا کنید.
  • log.Fatal(http.ListenAndServe(":8080", router)): روتر Mux به عنوان هندلر اصلی به ListenAndServe ارسال می‌شود.

استفاده از Gorilla Mux کد را تمیزتر، خواناتر و قابل نگهداری‌تر می‌کند، به خصوص برای APIهایی با مسیرهای پیچیده و متدهای مختلف.

اتصال به پایگاه داده: Persisting Data با PostgreSQL

پایگاه داده در حافظه (in-memory) که تاکنون استفاده کردیم، برای مثال‌های ساده مناسب است، اما برای یک API واقعی، شما نیاز به ذخیره‌سازی دائمی داده‌ها دارید. PostgreSQL یک سیستم مدیریت پایگاه داده رابطه‌ای (RDBMS) قدرتمند و متن‌باز است که در پروژه‌های Go بسیار محبوب است.

پیش‌نیازها: نصب PostgreSQL

قبل از ادامه، باید PostgreSQL را روی سیستم خود نصب کنید. دستورالعمل‌های نصب برای سیستم‌عامل‌های مختلف در وب‌سایت رسمی PostgreSQL (postgresql.org) موجود است.

پس از نصب، یک پایگاه داده و یک کاربر برای پروژه خود ایجاد کنید. برای مثال:

CREATE DATABASE goapi_db;
CREATE USER goapi_user WITH PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE goapi_db TO goapi_user;

همچنین، یک جدول books ایجاد کنید:

CREATE TABLE books (
    id VARCHAR(255) PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    author_first_name VARCHAR(255),
    author_last_name VARCHAR(255),
    isbn VARCHAR(255) UNIQUE NOT NULL
);

پکیج `database/sql` و درایور PostgreSQL

Go دارای پکیج database/sql برای کار با پایگاه داده‌های رابطه‌ای است، اما شما به یک درایور (driver) خاص برای هر پایگاه داده نیاز دارید. برای PostgreSQL، ما از درایور pq استفاده می‌کنیم:

go get github.com/lib/pq

بازنویسی منطق CRUD با پایگاه داده

حالا منطق CRUD را به گونه‌ای بازنویسی می‌کنیم که با پایگاه داده PostgreSQL کار کند. برای سادگی و اجتناب از طولانی شدن بیش از حد کد، در این بخش فقط توابع اصلی CRUD را نشان می‌دهیم و فرض می‌کنیم اتصال به پایگاه داده در main ایجاد شده است.

package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"github.com/google/uuid"
	"github.com/gorilla/mux"
	_ "github.com/lib/pq"
)

type Author struct {
    FirstName string `json:"firstName"`
    LastName  string `json:"lastName"`
}

type Book struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Author *Author `json:"author"`
    ISBN   string  `json:"isbn"`
}

var db *sql.DB

func main() {
    connStr := "user=goapi_user password=your_secure_password dbname=goapi_db host=localhost sslmode=disable"
    var err error
    db, err = sql.Open("postgres", connStr)
    if err != nil {
        log.Fatalf("Error opening database: %v", err)
    }
    defer db.Close()

    err = db.Ping()
    if err != nil {
        log.Fatalf("Error connecting to database: %v", err)
    }
    fmt.Println("Successfully connected to PostgreSQL!")

    router := mux.NewRouter()

    router.HandleFunc("/books", getBooks).Methods("GET")
    router.HandleFunc("/books/{id}", getBook).Methods("GET")
    router.HandleFunc("/books", createBook).Methods("POST")
    router.HandleFunc("/books/{id}", updateBook).Methods("PUT")
    router.HandleFunc("/books/{id}", deleteBook).Methods("DELETE")

    fmt.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", router))
}

func getBooks(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    rows, err := db.Query("SELECT id, title, author_first_name, author_last_name, isbn FROM books")
    if err != nil {
        http.Error(w, fmt.Sprintf("Error querying books: %v", err), http.StatusInternalServerError)
        return
    }
    defer rows.Close()

    var books []Book
    for rows.Next() {
        var book Book
        var authorFirstName, authorLastName sql.NullString
        err := rows.Scan(&book.ID, &book.Title, &authorFirstName, &authorLastName, &book.ISBN)
        if err != nil {
            log.Printf("Error scanning row: %v", err)
            continue
        }
        
        book.Author = &Author{}
        if authorFirstName.Valid {
            book.Author.FirstName = authorFirstName.String
        }
        if authorLastName.Valid {
            book.Author.LastName = authorLastName.String
        }

        books = append(books, book)
    }

    if err = rows.Err(); err != nil {
        http.Error(w, fmt.Sprintf("Error iterating rows: %v", err), http.StatusInternalServerError)
        return
    }

    json.NewEncoder(w).Encode(books)
}

func getBook(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    vars := mux.Vars(r)
    id := vars["id"]

    var book Book
    var authorFirstName, authorLastName sql.NullString
    row := db.QueryRow("SELECT id, title, author_first_name, author_last_name, isbn FROM books WHERE id = $1", id)
    err := row.Scan(&book.ID, &book.Title, &authorFirstName, &authorLastName, &book.ISBN)
    if err == sql.ErrNoRows {
        http.Error(w, "Book not found", http.StatusNotFound)
        return
    } else if err != nil {
        http.Error(w, fmt.Sprintf("Error querying book: %v", err), http.StatusInternalServerError)
        return
    }

    book.Author = &Author{}
    if authorFirstName.Valid {
        book.Author.FirstName = authorFirstName.String
    }
    if authorLastName.Valid {
        book.Author.LastName = authorLastName.String
    }

    json.NewEncoder(w).Encode(book)
}

func createBook(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    var book Book
    err := json.NewDecoder(r.Body).Decode(&book)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    book.ID = uuid.New().String()

    authorFirstName := sql.NullString{Valid: false}
    authorLastName := sql.NullString{Valid: false}

    if book.Author != nil {
        if book.Author.FirstName != "" {
            authorFirstName.String = book.Author.FirstName
            authorFirstName.Valid = true
        }
        if book.Author.LastName != "" {
            authorLastName.String = book.Author.LastName
            authorLastName.Valid = true
        }
    }
    
    _, err = db.Exec("INSERT INTO books (id, title, author_first_name, author_last_name, isbn) VALUES ($1, $2, $3, $4, $5)",
        book.ID, book.Title, authorFirstName, authorLastName, book.ISBN)
    if err != nil {
        http.Error(w, fmt.Sprintf("Error inserting book: %v", err), http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(book)
}

func updateBook(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    vars := mux.Vars(r)
    id := vars["id"]

    var updatedBook Book
    err := json.NewDecoder(r.Body).Decode(&updatedBook)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    authorFirstName := sql.NullString{Valid: false}
    authorLastName := sql.NullString{Valid: false}

    if updatedBook.Author != nil {
        if updatedBook.Author.FirstName != "" {
            authorFirstName.String = updatedBook.Author.FirstName
            authorFirstName.Valid = true
        }
        if updatedBook.Author.LastName != "" {
            authorLastName.String = updatedBook.Author.LastName
            authorLastName.Valid = true
        }
    }

    result, err := db.Exec("UPDATE books SET title=$1, author_first_name=$2, author_last_name=$3, isbn=$4 WHERE id=$5",
        updatedBook.Title, authorFirstName, authorLastName, updatedBook.ISBN, id)
    if err != nil {
        http.Error(w, fmt.Sprintf("Error updating book: %v", err), http.StatusInternalServerError)
        return
    }

    rowsAffected, err := result.RowsAffected()
    if err != nil {
        http.Error(w, fmt.Sprintf("Error checking rows affected: %v", err), http.StatusInternalServerError)
        return
    }

    if rowsAffected == 0 {
        http.Error(w, "Book not found", http.StatusNotFound)
        return
    }

    updatedBook.ID = id
    json.NewEncoder(w).Encode(updatedBook)
}

func deleteBook(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    vars := mux.Vars(r)
    id := vars["id"]

    result, err := db.Exec("DELETE FROM books WHERE id=$1", id)
    if err != nil {
        http.Error(w, fmt.Sprintf("Error deleting book: %v", err), http.StatusInternalServerError)
        return
    }

    rowsAffected, err := result.RowsAffected()
    if err != nil {
        http.Error(w, fmt.Sprintf("Error checking rows affected: %v", err), http.StatusInternalServerError)
        return
    }

    if rowsAffected == 0 {
        http.Error(w, "Book not found", http.StatusNotFound)
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

توضیحات تغییرات پایگاه داده:

  • import _ "github.com/lib/pq": این "ایمپورت خالی" (blank import) فقط برای اجرای تابع init درایور pq است که آن را در سیستم database/sql ثبت می‌کند.
  • db *sql.DB: یک متغیر سراسری برای نگهداری اتصال به پایگاه داده. در یک برنامه بزرگتر، این را می‌توان به یک ساختار (struct) منتقل کرد و به هندلرها تزریق کرد.
  • sql.Open("postgres", connStr): اتصال به پایگاه داده را برقرار می‌کند.
  • defer db.Close(): اطمینان حاصل می‌کند که اتصال به پایگاه داده پس از خروج از تابع main بسته می‌شود.
  • db.Ping(): برای بررسی اینکه آیا اتصال به پایگاه داده واقعاً برقرار شده است یا خیر.
  • db.Query(...): برای اجرای کوئری‌های SELECT که چندین ردیف را برمی‌گردانند.
  • rows.Next() و rows.Scan(...): برای پیمایش نتایج کوئری و اسکن مقادیر ردیف‌ها به متغیرهای Go.
  • db.QueryRow(...): برای اجرای کوئری SELECT که انتظار می‌رود حداکثر یک ردیف برگرداند.
  • db.Exec(...): برای اجرای دستورات INSERT, UPDATE, DELETE که ردیفی بر نمی‌گردانند.
  • sql.NullString: برای مدیریت فیلدهایی که ممکن است در پایگاه داده NULL باشند (مانند author_first_name و author_last_name).
  • مدیریت خطا: اکنون خطاها نه تنها به خاطر JSON یا مسیریابی، بلکه به دلیل مشکلات پایگاه داده نیز بررسی می‌شوند و کد وضعیت 500 Internal Server Error بازگردانده می‌شود.

ملاحظات مربوط به پایگاه داده:

  • استخر اتصال (Connection Pooling): database/sql به طور خودکار یک استخر اتصال مدیریت می‌کند. شما می‌توانید تنظیماتی مانند حداکثر تعداد اتصالات باز یا بیکار را با db.SetMaxOpenConns و db.SetMaxIdleConns پیکربندی کنید.
  • ORMs (Object-Relational Mappers): برای پروژه‌های بزرگتر، ممکن است به استفاده از ORMها مانند GORM یا SQLBoiler علاقه داشته باشید که تعامل با پایگاه داده را انتزاعی‌تر و آسان‌تر می‌کنند. با این حال، استفاده مستقیم از database/sql کنترل بیشتری را فراهم کرده و سربار کمتری دارد.
  • Migrations: برای مدیریت تغییرات شمای پایگاه داده در طول زمان، استفاده از ابزارهای migration مانند migrate (github.com/golang-migrate/migrate) یا goose (github.com/pressly/goose) توصیه می‌شود.

مدیریت خطا و اعتبارسنجی ورودی

مدیریت صحیح خطاها و اعتبارسنجی ورودی، از جنبه‌های حیاتی ساخت APIهای قابل اطمینان و قوی است.

مدیریت خطا در Go

در Go، خطاها مقادیری هستند که از نوع error (یک اینترفیس داخلی) هستند. توابع معمولاً دو مقدار را برمی‌گردانند: نتیجه عملیات و یک خطا.

value, err := someFunction()
if err != nil {
    log.Printf("Error: %v", err)
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}

در مثال‌های قبلی، ما از http.Error برای ارسال خطاهای کلی استفاده کردیم. برای APIهای RESTful، بهتر است پاسخ‌های خطای ساختاریافته‌تری ارائه دهیم که شامل جزئیات بیشتری باشند.

type ErrorResponse struct {
    Message string `json:"message"`
    Code    int    `json:"code,omitempty"`
    Details string `json:"details,omitempty"`
}

func sendErrorResponse(w http.ResponseWriter, message string, statusCode int, details ...string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(statusCode)
    
    response := ErrorResponse{
        Message: message,
        Code:    statusCode,
    }
    if len(details) > 0 {
        response.Details = details[0]
    }
    json.NewEncoder(w).Encode(response)
}

اعتبارسنجی ورودی (Input Validation)

شما هرگز نباید به داده‌هایی که از کلاینت دریافت می‌کنید، اعتماد کنید. اعتبارسنجی ورودی برای جلوگیری از آسیب‌پذیری‌های امنیتی (مانند SQL Injection اگر از ORM استفاده نمی‌کنید) و اطمینان از صحت داده‌ها حیاتی است.

برای اعتبارسنجی، می‌توانید از پکیج‌های شخص ثالث مانند github.com/go-playground/validator/v10 استفاده کنید، یا اعتبارسنجی دستی را پیاده‌سازی کنید.

مثال اعتبارسنجی ساده برای createBook:

func createBook(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    var book Book
    err := json.NewDecoder(r.Body).Decode(&book)
    if err != nil {
        sendErrorResponse(w, "Invalid request payload", http.StatusBadRequest, err.Error())
        return
    }

    if book.Title == "" {
        sendErrorResponse(w, "Book title is required", http.StatusBadRequest)
        return
    }
    if book.ISBN == "" {
        sendErrorResponse(w, "Book ISBN is required", http.StatusBadRequest)
        return
    }

    book.ID = uuid.New().String()

    authorFirstName := sql.NullString{Valid: false}
    authorLastName := sql.NullString{Valid: false}

    if book.Author != nil {
        if book.Author.FirstName != "" {
            authorFirstName.String = book.Author.FirstName
            authorFirstName.Valid = true
        }
        if book.Author.LastName != "" {
            authorLastName.String = book.Author.LastName
            authorLastName.Valid = true
        }
    }
    
    _, err = db.Exec("INSERT INTO books (id, title, author_first_name, author_last_name, isbn) VALUES ($1, $2, $3, $4, $5)",
        book.ID, book.Title, authorFirstName, authorLastName, book.ISBN)
    if err != nil {
        sendErrorResponse(w, fmt.Sprintf("Error inserting book: %v", err), http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(book)
}

میان‌افزارها (Middleware): افزودن قابلیت‌های متقابل

میان‌افزارها توابعی هستند که قبل یا بعد از اجرای هندلر اصلی یک درخواست HTTP اجرا می‌شوند. آنها برای افزودن قابلیت‌های متقابل (cross-cutting concerns) مانند لاگ‌برداری، احراز هویت (authentication)، اجازه دسترسی (authorization)، مدیریت CORS و بازیابی از panicها بسیار مفید هستند. Gorilla Mux به خوبی از میان‌افزارها پشتیبانی می‌کند.

ساخت یک میان‌افزار لاگ‌برداری ساده

این میان‌افزار هر درخواست ورودی را لاگ می‌کند:

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s %s", r.Method, r.RequestURI, r.Proto)
        next.ServeHTTP(w, r)
    })
}

و نحوه استفاده از آن در main:

func main() {
    // ... اتصال به پایگاه داده و تنظیم روتر

    router := mux.NewRouter()

    router.Use(loggingMiddleware)

    router.HandleFunc("/books", getBooks).Methods("GET")
    // ... سایر مسیرها
    
    fmt.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", router))
}

میان‌افزار احراز هویت (Authentication Middleware)

یک میان‌افزار احراز هویت می‌تواند توکن‌های JWT را بررسی کند یا از سیستم‌های احراز هویت دیگری استفاده کند. در اینجا یک مثال ساده (غیر تولیدی) آورده شده است:

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token != "Bearer my-secret-token" {
            sendErrorResponse(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// در main:
// router.Use(loggingMiddleware)
// router.Use(authMiddleware)

ترتیب اعمال میان‌افزارها مهم است. میان‌افزاری که زودتر اضافه می‌شود، زودتر اجرا می‌شود.

تست API: اطمینان از کارکرد صحیح

نوشتن تست‌ها برای API شما ضروری است تا مطمئن شوید که به درستی کار می‌کند و تغییرات آینده باعث ایجاد خطا نمی‌شوند. Go دارای فریم‌ورک تست داخلی قدرتمندی است.

تست‌های واحد (Unit Tests)

تست‌های واحد برای بررسی عملکرد توابع کوچک و مجزا هستند.

تست‌های یکپارچه‌سازی (Integration Tests) با `httptest`

پکیج net/http/httptest در Go به شما امکان می‌دهد درخواست‌های HTTP مجازی ایجاد کنید و پاسخ‌های سرور را بدون نیاز به راه‌اندازی واقعی سرور HTTP، تست کنید.

یک فایل تست به نام main_test.go (در همان دایرکتوری main.go) ایجاد کنید:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"database/sql"
	"github.com/gorilla/mux"
)

func setupTestDB() {
    connStr := "user=goapi_user password=your_secure_password dbname=goapi_db host=localhost sslmode=disable"
    var err error
    db, err = sql.Open("postgres", connStr)
    if err != nil {
        panic(fmt.Sprintf("Error opening test database: %v", err))
    }
    _, err = db.Exec("DELETE FROM books")
    if err != nil {
        panic(fmt.Sprintf("Error cleaning test database: %v", err))
    }
}

func TestGetBooks(t *testing.T) {
    setupTestDB()
    
    book := Book{
        ID:    "test-id-1",
        Title: "Test Book 1",
        Author: &Author{
            FirstName: "Test",
            LastName:  "Author",
        },
        ISBN: "1234567890",
    }
    _, err := db.Exec("INSERT INTO books (id, title, author_first_name, author_last_name, isbn) VALUES ($1, $2, $3, $4, $5)",
        book.ID, book.Title, book.Author.FirstName, book.Author.LastName, book.ISBN)
    if err != nil {
        t.Fatalf("Failed to insert test book: %v", err)
    }

    router := mux.NewRouter()
    router.HandleFunc("/books", getBooks).Methods("GET")

    req, _ := http.NewRequest("GET", "/books", nil)
    rr := httptest.NewRecorder()
    router.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }

    var books []Book
    err = json.NewDecoder(rr.Body).Decode(&books)
    if err != nil {
        t.Fatalf("Failed to decode response body: %v", err)
    }

    if len(books) == 0 {
        t.Errorf("Expected at least one book, got 0")
    }
}

func TestCreateBook(t *testing.T) {
    setupTestDB()

    router := mux.NewRouter()
    router.HandleFunc("/books", createBook).Methods("POST")

    newBook := Book{
        Title: "New Test Book",
        Author: &Author{
            FirstName: "New",
            LastName:  "Writer",
        },
        ISBN: "9876543210",
    }
    body, _ := json.Marshal(newBook)
    req, _ := http.NewRequest("POST", "/books", bytes.NewBuffer(body))
    req.Header.Set("Content-Type", "application/json")

    rr := httptest.NewRecorder()
    router.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusCreated {
        t.Errorf("handler returned wrong status code: got %v want %v. Body: %s",
            status, http.StatusCreated, rr.Body.String())
    }

    var createdBook Book
    err := json.NewDecoder(rr.Body).Decode(&createdBook)
    if err != nil {
        t.Fatalf("Failed to decode response body: %v", err)
    }

    if createdBook.Title != newBook.Title {
        t.Errorf("Handler returned unexpected book title: got %v want %v",
            createdBook.Title, newBook.Title)
    }
    if createdBook.ID == "" {
        t.Errorf("Expected a new ID, got empty string")
    }

    var count int
    db.QueryRow("SELECT COUNT(*) FROM books WHERE id = $1", createdBook.ID).Scan(&count)
    if count != 1 {
        t.Errorf("Book not found in database after creation")
    }
}

func TestGetBookNotFound(t *testing.T) {
    setupTestDB()

    router := mux.NewRouter()
    router.HandleFunc("/books/{id}", getBook).Methods("GET")

    req, _ := http.NewRequest("GET", "/books/non-existent-id", nil)
    rr := httptest.NewRecorder()
    router.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusNotFound {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusNotFound)
    }
}

اجرای تست‌ها:

go test -v

نکات مهم در مورد تست‌ها:

  • setupTestDB(): این تابع برای هر تست، پایگاه داده را تمیز می‌کند تا تست‌ها ایزوله باشند و به یکدیگر وابسته نباشند. در محیط‌های تولیدی، از یک پایگاه داده تست جداگانه استفاده کنید.
  • httptest.NewRecorder(): یک پاسخ HTTP را در حافظه شبیه‌سازی می‌کند که می‌توانید وضعیت، هدرها و بدنه آن را بررسی کنید.
  • http.NewRequest(...): یک درخواست HTTP جدید ایجاد می‌کند.
  • router.ServeHTTP(rr, req): درخواست را از طریق روتر به هندلر مربوطه ارسال می‌کند و نتیجه را در rr (response recorder) ذخیره می‌کند.
  • t.Errorf(...) و t.Fatalf(...): متدهای پکیج testing برای گزارش خطاها. Fatalf تست را بلافاصله متوقف می‌کند.

ملاحظات استقرار (Deployment)

پس از ساخت و تست API، نوبت به استقرار آن در محیط تولید می‌رسد.

ساختیناری Go (Go Binaries)

یکی از مزایای بزرگ Go، تولید فایل‌های اجرایی (binaries) مستقل است. شما می‌توانید برنامه Go خود را برای یک سیستم‌عامل و معماری خاص کامپایل کنید:

GOOS=linux GOARCH=amd64 go build -o myapi .

این دستور یک فایل اجرایی به نام myapi برای لینوکس (64 بیتی) در دایرکتوری فعلی ایجاد می‌کند. شما فقط باید این یک فایل را به سرور خود منتقل کنید.

متغیرهای محیطی (Environment Variables)

اطلاعات حساس مانند رشته اتصال به پایگاه داده یا کلیدهای API هرگز نباید در کد سخت‌افزاری (hardcoded) شوند. به جای آن، از متغیرهای محیطی استفاده کنید.

// در main.go:
// connStr := os.Getenv("DATABASE_URL")
// if connStr == "" {
//     log.Fatal("DATABASE_URL environment variable not set")
// }

سپس هنگام اجرای برنامه در سرور:

DATABASE_URL="user=..." ./myapi

داکریزاسیون (Dockerization)

Docker یک راه عالی برای بسته‌بندی برنامه شما با تمام وابستگی‌هایش در یک کانتینر ایزوله است. یک فایل Dockerfile ساده:

# مرحله اول: ساخت برنامه
FROM golang:1.22-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -o /go-rest-api

# مرحله دوم: ساخت ایمیج نهایی
FROM alpine:latest

WORKDIR /app

COPY --from=builder /go-rest-api .

EXPOSE 8080

CMD ["./go-rest-api"]

برای ساخت و اجرای ایمیج داکر:

docker build -t go-rest-api .
docker run -p 8080:8080 -e DATABASE_URL="user=..." go-rest-api

پلتفرم‌های استقرار

  • VPS/Bare Metal: خودتان سرور را مدیریت می‌کنید. فایل اجرایی را کپی کرده و آن را با یک ابزار مدیریت فرایند (مانند Systemd یا Supervisor) اجرا کنید.
  • Container Orchestrators (Kubernetes): برای مقیاس‌پذیری بالا و مدیریت پیچیده میکروسرویس‌ها.
  • PaaS (Platform as a Service): مانند Heroku، Google App Engine، یا DigitalOcean App Platform که استقرار را بسیار ساده می‌کنند.
  • Cloud Providers (AWS, GCP, Azure): می‌توانید API خود را روی EC2، Lambda (Serverless)، ECS، EKS و غیره در AWS، یا معادل‌های آن در GCP و Azure مستقر کنید.

مباحث پیشرفته (Advanced Topics)

این راهنما اصول اساسی ساخت یک RESTful API با Go را پوشش داد. با این حال، دنیای توسعه API بسیار گسترده‌تر است و مباحث پیشرفته‌تری وجود دارند که می‌توانند قابلیت‌ها و امنیت API شما را افزایش دهند:

  • احراز هویت و اجازه دسترسی (Authentication & Authorization):

    • JWT (JSON Web Tokens): یک استاندارد رایج برای ایجاد توکن‌های دسترسی برای کاربران.
    • OAuth2: یک چارچوب استاندارد برای اجازه دسترسی که به کاربران امکان می‌دهد به برنامه‌ها اجازه دسترسی به منابع محافظت‌شده خود را بدهند بدون اینکه رمز عبور خود را به اشتراک بگذارند.
    • RBAC (Role-Based Access Control): کنترل دسترسی مبتنی بر نقش‌ها برای تعریف اینکه چه کاربران یا نقش‌هایی می‌توانند به چه منابعی دسترسی داشته باشند یا چه عملیاتی را انجام دهند.
  • محدودیت نرخ (Rate Limiting): جلوگیری از سوءاستفاده یا حملات DDoS با محدود کردن تعداد درخواست‌هایی که یک کلاینت می‌تواند در یک بازه زمانی مشخص ارسال کند.
  • نسخه‌بندی API (API Versioning): برای مدیریت تغییرات در API در طول زمان بدون شکستن برنامه‌های کلاینت قدیمی (مثلاً /api/v1/books، /api/v2/books).
  • مستندسازی API (API Documentation): استفاده از ابزارهایی مانند OpenAPI (Swagger) برای تولید مستندات خودکار و تعاملی برای API شما.
  • WebSockets: برای ارتباطات دوطرفه بی‌درنگ (real-time) بین کلاینت و سرور، که REST به طور ذاتی برای آن طراحی نشده است.
  • پایش و لاگ‌برداری (Monitoring & Logging): استفاده از ابزارهایی مانند Prometheus/Grafana برای پایش عملکرد API و جمع‌آوری لاگ‌ها برای اشکال‌زدایی.
  • میکروسرویس‌ها (Microservices): در برنامه‌های بزرگتر، تقسیم API به سرویس‌های کوچک‌تر و مستقل.

نتیجه‌گیری

در این راهنمای جامع، ما به تفصیل به ساخت یک RESTful API با Go پرداختیم. از درک اصول معماری REST و مزایای Go در توسعه API گرفته تا راه‌اندازی محیط توسعه، طراحی منابع، پیاده‌سازی منطق CRUD با net/http و بهبود آن با Gorilla Mux، و نهایتاً اتصال به پایگاه داده PostgreSQL، همه گام‌های اساسی را طی کردیم. همچنین به اهمیت مدیریت خطا، اعتبارسنجی ورودی، استفاده از میان‌افزارها، نوشتن تست‌ها و ملاحظات استقرار پرداختیم.

Go با عملکرد بالا، همزمانی قدرتمند و کتابخانه استاندارد غنی خود، ابزاری استثنایی برای ساخت APIهای RESTful است که هم کارآمد هستند و هم قابل نگهداری. با دانش کسب شده در این پست، شما اکنون پایه محکمی برای شروع ساخت APIهای قدرتمند خود با Go دارید.

به یاد داشته باشید که توسعه نرم‌افزار یک فرآیند تکراری است. با تمرین و کاوش بیشتر در اکوسیستم Go، می‌توانید مهارت‌های خود را بهبود بخشید و APIهای پیچیده‌تر و قوی‌تری بسازید. همیشه به دنبال بهترین روش‌ها باشید، کد خود را تست کنید و به امنیت و مقیاس‌پذیری توجه کنید. موفق باشید در مسیر توسعه Go!

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان