هوش مصنوعی با Go؛ آموزش ساخت API، چت‌بات و سرویس AI با Golang و Gin

در این آموزش عملی یاد می‌گیرید با Go و Gin یک REST API و چت‌بات هوش مصنوعی بسازید، آن را به API درواره متصل کنید و پاسخ عادی، Streaming و JSON دریافت کنید.

Share
هوش مصنوعی با Go؛ آموزش ساخت API، چت‌بات و سرویس AI با Golang و Gin

زبان Go یا Golang یکی از انتخاب‌های مناسب برای ساخت Backendهای سریع، سبک و مقیاس‌پذیر است. اگر در حال توسعه یک Microservice، چت‌بات، ابزار پردازش متن، پنل سازمانی یا سرویس پرترافیک هستید، می‌توانید قابلیت‌های هوش مصنوعی را بدون تغییر اساسی در معماری برنامه به پروژه Go اضافه کنید.

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

در این آموزش یک پروژه عملی با Go و فریم‌ورک Gin می‌سازیم و آن را به API درواره متصل می‌کنیم.

در پایان مقاله می‌توانید:

  • یک REST API با Go و Gin بسازید.
  • درخواست Chat Completions به درواره ارسال کنید.
  • API Key را خارج از کد منبع نگه دارید.
  • یک Endpoint چت‌بات ایجاد کنید.
  • پاسخ مدل را به‌صورت عادی دریافت کنید.
  • پاسخ Streaming را با SSE پیاده‌سازی کنید.
  • خروجی JSON ساختاریافته تولید و اعتبارسنجی کنید.
  • Timeout و لغو درخواست را با context.Context مدیریت کنید.
  • تست واحد و تست یکپارچه بنویسید.
  • پروژه را با Docker اجرا کنید.
  • سرویس را برای محیط Production آماده کنید.

چرا Go برای ساخت برنامه‌های هوش مصنوعی مناسب است؟

بخش زیادی از آموزش‌های هوش مصنوعی با پایتون (Python)، جاوااسکریپت (JavaScript) یا TypeScript نوشته می‌شوند. بااین‌حال، اگر هدف شما ساخت یک Backend سریع و قابل‌اتکا باشد، Go مزایای مهمی دارد.

مهم‌ترین مزایای Go برای سرویس‌های هوش مصنوعی عبارت‌اند از:

  • مصرف حافظه نسبتاً کم
  • زمان راه‌اندازی سریع برنامه
  • پشتیبانی داخلی از پردازش هم‌زمان با Goroutine
  • کتابخانه استاندارد قدرتمند برای HTTP و JSON
  • ساخت فایل اجرایی مستقل
  • استقرار آسان با Docker
  • مناسب برای Microserviceها
  • پشتیبانی مناسب از Context، Timeout و Cancellation
  • سادگی ساخت سرویس‌های پرترافیک
  • امکان اجرای یک کد روی Linux، Windows و macOS

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

با Go و هوش مصنوعی چه برنامه‌هایی می‌توان ساخت؟

برنامه Go شما می‌تواند تقریباً هر قابلیت متنی یا چندوجهی را از طریق API ارائه کند.

کاربردنمونه پیاده‌سازی
چت‌باتپاسخ‌گویی به کاربران سایت یا اپلیکیشن
دستیار پشتیبانیپاسخ بر اساس اطلاعات محصولات و خدمات
تولید محتواتولید عنوان، توضیحات محصول و متن تبلیغاتی
خلاصه‌سازیخلاصه مقاله، گزارش، تیکت یا جلسه
تحلیل نظراتتشخیص موضوع و احساس کلی بازخوردها
استخراج اطلاعاتتبدیل متن آزاد به JSON
دستیار برنامه‌نویسیتوضیح، بازنویسی و مستندسازی کد
جست‌وجوی هوشمندترکیب مدل زبانی با پایگاه داده یا RAG
پردازش گروهیتحلیل تعداد زیادی سند در Background Worker
ایجنت هوش مصنوعیاتصال مدل به ابزارها و سرویس‌های داخلی

معماری صحیح اتصال Go به مدل هوش مصنوعی

در یک محصول واقعی، Frontend یا اپلیکیشن موبایل نباید مستقیماً API Key درواره را در اختیار داشته باشد.

معماری پیشنهادی به این شکل است:

  1. کاربر درخواست خود را در Frontend وارد می‌کند.
  2. Frontend درخواست را به Backend نوشته‌شده با Go ارسال می‌کند.
  3. Backend ورودی، هویت کاربر و محدودیت مصرف را بررسی می‌کند.
  4. برنامه Go با API Key محرمانه به درواره درخواست می‌فرستد.
  5. درواره درخواست را به مدل انتخابی منتقل می‌کند.
  6. پاسخ مدل به Backend برمی‌گردد.
  7. Backend پاسخ کنترل‌شده را به کاربر تحویل می‌دهد.

در این معماری می‌توانید قابلیت‌های زیر را در Backend اجرا کنید:

  • احراز هویت
  • Rate Limiting
  • ثبت میزان مصرف
  • مدیریت اعتبار کاربران
  • کش پاسخ‌ها
  • حذف داده‌های حساس
  • انتخاب مدل
  • Fallback
  • مدیریت خطا
  • ثبت لاگ
  • اعتبارسنجی خروجی مدل

Gin چیست؟

Gin یک Web Framework برای زبان Go است که ساخت REST API، مسیریابی، Middleware، اعتبارسنجی ورودی و تولید پاسخ JSON را ساده‌تر می‌کند.

البته با کتابخانه استاندارد net/http نیز می‌توان API ساخت؛ اما Gin در پروژه‌هایی که Endpointهای متعدد، Middleware و اعتبارسنجی دارند، سرعت توسعه را افزایش می‌دهد.

در آموزش رسمی Go برای ساخت REST API با Gin نیز از Gin برای Routing، خواندن اطلاعات درخواست و تولید پاسخ JSON استفاده شده است.

پیش‌نیازهای آموزش

برای انجام پروژه به موارد زیر نیاز دارید:

  • یک نسخه جدید و پشتیبانی‌شده از Go
  • آشنایی مقدماتی با زبان Go
  • یک ویرایشگر مانند VS Code یا GoLand
  • حساب کاربری در درواره
  • API Key درواره
  • شناسه یک مدل متنی

برای بررسی نسخه نصب‌شده Go، دستور زیر را اجرا کنید:

go version

برای انتخاب مدل مناسب و مشاهده قیمت‌ها به صفحه مدل‌های درواره مراجعه کنید.

در نمونه‌کدها از مقادیر زیر استفاده می‌کنیم:

Base URL:
https://api.darvareh.ir/v1

API Key:
YOUR_DARVAREH_API_KEY

Model ID:
YOUR_MODEL_ID

ساخت پروژه Go

یک پوشه جدید ایجاد کنید:

mkdir darvareh-go-ai
cd darvareh-go-ai

ماژول Go را راه‌اندازی کنید:

go mod init example.com/darvareh-go-ai

Gin را نصب کنید:

go get github.com/gin-gonic/gin

در نسخه‌های جدید Gin ممکن است حداقل نسخه موردنیاز Go تغییر کند؛ بنابراین در صورت بروز خطا، نسخه موردنیاز را در راهنمای رسمی Gin بررسی کنید.

ساختار پروژه

برای اینکه کد قابل‌توسعه باشد، از ساختار زیر استفاده می‌کنیم:

darvareh-go-ai/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── ai/
│   │   ├── client.go
│   │   └── models.go
│   └── httpapi/
│       └── handler.go
├── Dockerfile
├── go.mod
└── go.sum

پوشه internal باعث می‌شود Packageهای داخلی پروژه از بیرون ماژول قابل Import نباشند.

تعریف مدل‌های درخواست و پاسخ

فایل internal/ai/models.go را ایجاد کنید:

package ai

type Message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type ChatCompletionRequest struct {
	Model          string          `json:"model"`
	Messages       []Message       `json:"messages"`
	Temperature    float64         `json:"temperature,omitempty"`
	MaxTokens      int             `json:"max_tokens,omitempty"`
	Stream         bool            `json:"stream,omitempty"`
	ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
}

type ResponseFormat struct {
	Type string `json:"type"`
}

type ChatCompletionResponse struct {
	Model   string   `json:"model"`
	Choices []Choice `json:"choices"`
	Usage   *Usage   `json:"usage,omitempty"`
}

type Choice struct {
	Index   int     `json:"index"`
	Message Message `json:"message"`
}

type Usage struct {
	PromptTokens     int `json:"prompt_tokens"`
	CompletionTokens int `json:"completion_tokens"`
	TotalTokens      int `json:"total_tokens"`
}

type StreamResponse struct {
	Choices []StreamChoice `json:"choices"`
}

type StreamChoice struct {
	Delta StreamDelta `json:"delta"`
}

type StreamDelta struct {
	Content string `json:"content"`
}

type ChatResult struct {
	Answer string `json:"answer"`
	Model  string `json:"model"`
	Usage  *Usage `json:"usage,omitempty"`
}

تگ‌هایی مانند json:"model" مشخص می‌کنند هر فیلد Go با چه نامی در JSON ارسال یا دریافت شود.

ساخت Client اتصال به درواره

فایل internal/ai/client.go را بسازید:

package ai

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"
)

var ErrEmptyResponse = errors.New("AI response contains no text")

type Client struct {
	baseURL    string
	apiKey     string
	model      string
	httpClient *http.Client
}

type ClientConfig struct {
	BaseURL    string
	APIKey     string
	Model      string
	HTTPClient *http.Client
}

func NewClient(config ClientConfig) (*Client, error) {
	if strings.TrimSpace(config.BaseURL) == "" {
		return nil, errors.New("base URL is required")
	}

	if strings.TrimSpace(config.APIKey) == "" {
		return nil, errors.New("API key is required")
	}

	if strings.TrimSpace(config.Model) == "" {
		return nil, errors.New("model ID is required")
	}

	httpClient := config.HTTPClient

	if httpClient == nil {
		httpClient = &http.Client{
			Timeout: 90 * time.Second,
		}
	}

	return &Client{
		baseURL:    strings.TrimRight(config.BaseURL, "/"),
		apiKey:     config.APIKey,
		model:      config.Model,
		httpClient: httpClient,
	}, nil
}

func (c *Client) Complete(
	ctx context.Context,
	message string,
	systemPrompt string,
) (*ChatResult, error) {
	messages := buildMessages(message, systemPrompt)

	payload := ChatCompletionRequest{
		Model:       c.model,
		Messages:    messages,
		Temperature: 0.3,
		MaxTokens:   1000,
	}

	var response ChatCompletionResponse

	if err := c.doJSON(
		ctx,
		http.MethodPost,
		"/chat/completions",
		payload,
		&response,
	); err != nil {
		return nil, err
	}

	if len(response.Choices) == 0 {
		return nil, ErrEmptyResponse
	}

	answer := strings.TrimSpace(
		response.Choices[0].Message.Content,
	)

	if answer == "" {
		return nil, ErrEmptyResponse
	}

	return &ChatResult{
		Answer: answer,
		Model:  response.Model,
		Usage:  response.Usage,
	}, nil
}

func (c *Client) CompleteJSON(
	ctx context.Context,
	prompt string,
	target any,
) error {
	payload := ChatCompletionRequest{
		Model: c.model,
		Messages: []Message{
			{
				Role: "system",
				Content: "فقط یک JSON معتبر تولید کن. " +
					"هیچ توضیح یا Markdown خارج از JSON ننویس.",
			},
			{
				Role:    "user",
				Content: prompt,
			},
		},
		Temperature: 0,
		MaxTokens:   600,
		ResponseFormat: &ResponseFormat{
			Type: "json_object",
		},
	}

	var response ChatCompletionResponse

	if err := c.doJSON(
		ctx,
		http.MethodPost,
		"/chat/completions",
		payload,
		&response,
	); err != nil {
		return err
	}

	if len(response.Choices) == 0 {
		return ErrEmptyResponse
	}

	content := strings.TrimSpace(
		response.Choices[0].Message.Content,
	)

	if content == "" {
		return ErrEmptyResponse
	}

	if err := json.Unmarshal([]byte(content), target); err != nil {
		return fmt.Errorf("decode model JSON: %w", err)
	}

	return nil
}

func (c *Client) Stream(
	ctx context.Context,
	message string,
	systemPrompt string,
	onToken func(string) error,
) error {
	payload := ChatCompletionRequest{
		Model:       c.model,
		Messages:    buildMessages(message, systemPrompt),
		Temperature: 0.3,
		MaxTokens:   1000,
		Stream:      true,
	}

	body, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("encode streaming request: %w", err)
	}

	request, err := http.NewRequestWithContext(
		ctx,
		http.MethodPost,
		c.baseURL+"/chat/completions",
		bytes.NewReader(body),
	)
	if err != nil {
		return fmt.Errorf("create streaming request: %w", err)
	}

	request.Header.Set("Authorization", "Bearer "+c.apiKey)
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("Accept", "text/event-stream")

	response, err := c.httpClient.Do(request)
	if err != nil {
		return fmt.Errorf("send streaming request: %w", err)
	}
	defer response.Body.Close()

	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return decodeAPIError(response)
	}

	scanner := bufio.NewScanner(response.Body)

	scanner.Buffer(
		make([]byte, 64*1024),
		1024*1024,
	)

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())

		if line == "" || !strings.HasPrefix(line, "data:") {
			continue
		}

		data := strings.TrimSpace(
			strings.TrimPrefix(line, "data:"),
		)

		if data == "[DONE]" {
			return nil
		}

		var event StreamResponse

		if err := json.Unmarshal([]byte(data), &event); err != nil {
			continue
		}

		if len(event.Choices) == 0 {
			continue
		}

		token := event.Choices[0].Delta.Content

		if token == "" {
			continue
		}

		if err := onToken(token); err != nil {
			return fmt.Errorf("consume stream token: %w", err)
		}
	}

	if err := scanner.Err(); err != nil {
		return fmt.Errorf("read streaming response: %w", err)
	}

	return nil
}

func (c *Client) doJSON(
	ctx context.Context,
	method string,
	path string,
	input any,
	output any,
) error {
	body, err := json.Marshal(input)
	if err != nil {
		return fmt.Errorf("encode request: %w", err)
	}

	request, err := http.NewRequestWithContext(
		ctx,
		method,
		c.baseURL+path,
		bytes.NewReader(body),
	)
	if err != nil {
		return fmt.Errorf("create request: %w", err)
	}

	request.Header.Set("Authorization", "Bearer "+c.apiKey)
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("Accept", "application/json")

	response, err := c.httpClient.Do(request)
	if err != nil {
		return fmt.Errorf("send AI request: %w", err)
	}
	defer response.Body.Close()

	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return decodeAPIError(response)
	}

	if err := json.NewDecoder(response.Body).Decode(output); err != nil {
		return fmt.Errorf("decode AI response: %w", err)
	}

	return nil
}

func decodeAPIError(response *http.Response) error {
	const maxErrorBody = 64 * 1024

	body, err := io.ReadAll(
		io.LimitReader(response.Body, maxErrorBody),
	)
	if err != nil {
		return fmt.Errorf(
			"AI API returned status %d",
			response.StatusCode,
		)
	}

	return fmt.Errorf(
		"AI API returned status %d: %s",
		response.StatusCode,
		strings.TrimSpace(string(body)),
	)
}

func buildMessages(
	message string,
	systemPrompt string,
) []Message {
	messages := make([]Message, 0, 2)

	if strings.TrimSpace(systemPrompt) != "" {
		messages = append(messages, Message{
			Role:    "system",
			Content: systemPrompt,
		})
	}

	messages = append(messages, Message{
		Role:    "user",
		Content: message,
	})

	return messages
}

این Client سه قابلیت اصلی دارد:

  • Complete: دریافت پاسخ متنی کامل
  • Stream: دریافت پاسخ به‌صورت Streaming
  • CompleteJSON: تبدیل پاسخ مدل به یک ساختار Go

چرا باید http.Client را دوباره استفاده کنیم؟

در کد بالا یک http.Client ساخته و در تمام درخواست‌ها استفاده می‌شود. ساختن یک Client جدید برای هر درخواست الگوی مناسبی نیست.

طبق مستندات رسمی net/http، Transport مربوط به Client وضعیت داخلی مانند اتصال‌های TCP کش‌شده دارد و Clientها باید دوباره استفاده شوند. همچنین http.Client برای استفاده هم‌زمان توسط چند Goroutine مناسب است.

بنابراین این الگو اشتباه است:

func sendRequest() {
	client := &http.Client{}
	// یک Client جدید در هر درخواست
}

الگوی مناسب:

client := &http.Client{
	Timeout: 90 * time.Second,
}

// استفاده مجدد از client

تعریف Handlerهای HTTP

فایل internal/httpapi/handler.go را ایجاد کنید:

package httpapi

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"

	"example.com/darvareh-go-ai/internal/ai"
)

type AIService interface {
	Complete(
		ctx context.Context,
		message string,
		systemPrompt string,
	) (*ai.ChatResult, error)

	CompleteJSON(
		ctx context.Context,
		prompt string,
		target any,
	) error

	Stream(
		ctx context.Context,
		message string,
		systemPrompt string,
		onToken func(string) error,
	) error
}

type Handler struct {
	aiClient AIService
	logger   *slog.Logger
}

func NewHandler(
	aiClient AIService,
	logger *slog.Logger,
) *Handler {
	return &Handler{
		aiClient: aiClient,
		logger:   logger,
	}
}

type ChatRequest struct {
	Message      string `json:"message" binding:"required,min=1,max=12000"`
	SystemPrompt string `json:"system_prompt" binding:"max=2000"`
}

type SummarizeRequest struct {
	Text          string `json:"text" binding:"required,min=20,max=30000"`
	MaxParagraphs int    `json:"max_paragraphs" binding:"omitempty,min=1,max=10"`
}

type FeedbackRequest struct {
	Text string `json:"text" binding:"required,min=3,max=5000"`
}

type FeedbackAnalysis struct {
	Category  string `json:"category"`
	Sentiment string `json:"sentiment"`
	Priority  int    `json:"priority"`
	Summary   string `json:"summary"`
}

func (h *Handler) RegisterRoutes(router *gin.Engine) {
	router.GET("/health", h.health)

	api := router.Group("/api/ai")
	{
		api.POST("/chat", h.chat)
		api.POST("/chat/stream", h.stream)
		api.POST("/summarize", h.summarize)
		api.POST("/analyze-feedback", h.analyzeFeedback)
	}
}

func (h *Handler) health(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"status": "ok",
	})
}

func (h *Handler) chat(c *gin.Context) {
	var input ChatRequest

	c.Request.Body = http.MaxBytesReader(
		c.Writer,
		c.Request.Body,
		32*1024,
	)

	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": "ورودی درخواست معتبر نیست.",
		})
		return
	}

	result, err := h.aiClient.Complete(
		c.Request.Context(),
		input.Message,
		input.SystemPrompt,
	)
	if err != nil {
		h.logger.Error(
			"AI completion failed",
			"error", err,
		)

		c.JSON(http.StatusBadGateway, gin.H{
			"error": "دریافت پاسخ از سرویس هوش مصنوعی ناموفق بود.",
		})
		return
	}

	c.JSON(http.StatusOK, result)
}

func (h *Handler) stream(c *gin.Context) {
	var input ChatRequest

	c.Request.Body = http.MaxBytesReader(
		c.Writer,
		c.Request.Body,
		32*1024,
	)

	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": "ورودی درخواست معتبر نیست.",
		})
		return
	}

	c.Header("Content-Type", "text/event-stream")
	c.Header("Cache-Control", "no-cache")
	c.Header("Connection", "keep-alive")
	c.Header("X-Accel-Buffering", "no")

	c.Status(http.StatusOK)
	c.Writer.Flush()

	err := h.aiClient.Stream(
		c.Request.Context(),
		input.Message,
		input.SystemPrompt,
		func(token string) error {
			data, err := json.Marshal(gin.H{
				"token": token,
			})
			if err != nil {
				return err
			}

			if _, err := fmt.Fprintf(
				c.Writer,
				"data: %s\n\n",
				data,
			); err != nil {
				return err
			}

			c.Writer.Flush()
			return nil
		},
	)

	if err != nil {
		h.logger.Error(
			"AI streaming failed",
			"error", err,
		)

		errorData, _ := json.Marshal(gin.H{
			"error": "stream_failed",
		})

		_, _ = fmt.Fprintf(
			c.Writer,
			"event: error\ndata: %s\n\n",
			errorData,
		)

		c.Writer.Flush()
		return
	}

	_, _ = fmt.Fprint(
		c.Writer,
		"data: [DONE]\n\n",
	)

	c.Writer.Flush()
}

func (h *Handler) summarize(c *gin.Context) {
	var input SummarizeRequest

	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": "متن یا تنظیمات خلاصه‌سازی معتبر نیست.",
		})
		return
	}

	if input.MaxParagraphs == 0 {
		input.MaxParagraphs = 3
	}

	prompt := fmt.Sprintf(`
متن زیر را حداکثر در %d پاراگراف خلاصه کن.

الزامات:
- اطلاعات مهم متن حفظ شوند.
- مطلب جدیدی اضافه نشود.
- پاسخ به زبان فارسی روان باشد.
- از تکرار پرهیز شود.

متن:
%s`,
		input.MaxParagraphs,
		input.Text,
	)

	result, err := h.aiClient.Complete(
		c.Request.Context(),
		prompt,
		"تو یک ویراستار دقیق فارسی هستی.",
	)
	if err != nil {
		h.logger.Error(
			"summarization failed",
			"error", err,
		)

		c.JSON(http.StatusBadGateway, gin.H{
			"error": "خلاصه‌سازی متن ناموفق بود.",
		})
		return
	}

	c.JSON(http.StatusOK, result)
}

func (h *Handler) analyzeFeedback(c *gin.Context) {
	var input FeedbackRequest

	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": "متن بازخورد معتبر نیست.",
		})
		return
	}

	prompt := fmt.Sprintf(`
بازخورد زیر را تحلیل کن:

%s

خروجی دقیقاً باید این ساختار را داشته باشد:
{
  "category": "product | delivery | payment | support | other",
  "sentiment": "positive | neutral | negative",
  "priority": 1,
  "summary": "خلاصه کوتاه فارسی"
}

priority باید عددی بین 1 تا 5 باشد.`,
		input.Text,
	)

	var result FeedbackAnalysis

	if err := h.aiClient.CompleteJSON(
		c.Request.Context(),
		prompt,
		&result,
	); err != nil {
		h.logger.Error(
			"feedback analysis failed",
			"error", err,
		)

		c.JSON(http.StatusBadGateway, gin.H{
			"error": "تحلیل بازخورد ناموفق بود.",
		})
		return
	}

	if err := validateFeedback(result); err != nil {
		h.logger.Warn(
			"model returned invalid feedback",
			"error", err,
		)

		c.JSON(http.StatusBadGateway, gin.H{
			"error": "خروجی مدل با ساختار مورد انتظار سازگار نبود.",
		})
		return
	}

	c.JSON(http.StatusOK, result)
}

func validateFeedback(result FeedbackAnalysis) error {
	validCategories := map[string]bool{
		"product":  true,
		"delivery": true,
		"payment":  true,
		"support":  true,
		"other":    true,
	}

	validSentiments := map[string]bool{
		"positive": true,
		"neutral":  true,
		"negative": true,
	}

	if !validCategories[result.Category] {
		return fmt.Errorf(
			"invalid category: %s",
			result.Category,
		)
	}

	if !validSentiments[result.Sentiment] {
		return fmt.Errorf(
			"invalid sentiment: %s",
			result.Sentiment,
		)
	}

	if result.Priority < 1 || result.Priority > 5 {
		return fmt.Errorf(
			"invalid priority: %d",
			result.Priority,
		)
	}

	if strings.TrimSpace(result.Summary) == "" {
		return fmt.Errorf("summary is empty")
	}

	return nil
}

یک Import در ابتدای فایل لازم است که در فهرست بالا باید وجود داشته باشد:

import "context"

بنابراین بخش کامل Import شامل context نیز خواهد بود:

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"strings"
)

چرا خروجی مدل را اعتبارسنجی می‌کنیم؟

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

  • یکی از فیلدها را حذف کند.
  • نام مقدار را تغییر دهد.
  • عددی خارج از محدوده برگرداند.
  • متن توضیحی به پاسخ اضافه کند.
  • JSON ناقص تولید کند.

به همین دلیل، خروجی مدل باید مانند ورودی کاربر اعتبارسنجی شود.

برای نمونه، این پاسخ از نظر JSON معتبر است اما با قرارداد برنامه سازگار نیست:

{
  "category": "shipping_problem",
  "sentiment": "angry",
  "priority": 20,
  "summary": ""
}

اعتبارسنجی Backend از ورود چنین داده‌ای به پایگاه داده یا منطق اصلی برنامه جلوگیری می‌کند.

ساخت فایل اصلی برنامه

فایل cmd/api/main.go را ایجاد کنید:

package main

import (
	"context"
	"log/slog"
	"net"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"

	"example.com/darvareh-go-ai/internal/ai"
	"example.com/darvareh-go-ai/internal/httpapi"
)

func main() {
	logger := slog.New(
		slog.NewJSONHandler(os.Stdout, nil),
	)

	apiKey := os.Getenv("DARVAREH_API_KEY")
	model := os.Getenv("DARVAREH_MODEL")

	baseURL := os.Getenv("DARVAREH_BASE_URL")
	if baseURL == "" {
		baseURL = "https://api.darvareh.ir/v1"
	}

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	transport := &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		DialContext: (&net.Dialer{
			Timeout:   10 * time.Second,
			KeepAlive: 30 * time.Second,
		}).DialContext,
		MaxIdleConns:          100,
		MaxIdleConnsPerHost:   20,
		IdleConnTimeout:       90 * time.Second,
		TLSHandshakeTimeout:   10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
	}

	httpClient := &http.Client{
		Transport: transport,
		Timeout:   120 * time.Second,
	}

	aiClient, err := ai.NewClient(ai.ClientConfig{
		BaseURL:    baseURL,
		APIKey:     apiKey,
		Model:      model,
		HTTPClient: httpClient,
	})
	if err != nil {
		logger.Error(
			"AI client configuration is invalid",
			"error", err,
		)
		os.Exit(1)
	}

	router := gin.New()
	router.Use(gin.Logger())
	router.Use(gin.Recovery())

	handler := httpapi.NewHandler(
		aiClient,
		logger,
	)

	handler.RegisterRoutes(router)

	server := &http.Server{
		Addr:              ":" + port,
		Handler:           router,
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       15 * time.Second,
		IdleTimeout:       60 * time.Second,
	}

	go func() {
		logger.Info(
			"HTTP server started",
			"port", port,
		)

		if err := server.ListenAndServe(); err != nil &&
			err != http.ErrServerClosed {
			logger.Error(
				"HTTP server failed",
				"error", err,
			)
			os.Exit(1)
		}
	}()

	stop := make(
		chan os.Signal,
		1,
	)

	signal.Notify(
		stop,
		syscall.SIGINT,
		syscall.SIGTERM,
	)

	<-stop

	shutdownContext, cancel := context.WithTimeout(
		context.Background(),
		15*time.Second,
	)
	defer cancel()

	logger.Info("shutting down HTTP server")

	if err := server.Shutdown(shutdownContext); err != nil {
		logger.Error(
			"graceful shutdown failed",
			"error", err,
		)
	}
}

در این فایل چند نکته مهم رعایت شده است:

  • API Key از Environment Variable خوانده می‌شود.
  • یک http.Client مشترک ساخته می‌شود.
  • Connection Pool تنظیم شده است.
  • Timeoutهای سرور مشخص شده‌اند.
  • Structured Logging با slog فعال است.
  • Graceful Shutdown پیاده‌سازی شده است.
  • هنگام توقف برنامه، درخواست‌های در حال اجرا فرصت تکمیل دارند.

تنظیم Environment Variable

در Linux یا macOS:

export DARVAREH_API_KEY="YOUR_DARVAREH_API_KEY"
export DARVAREH_MODEL="YOUR_MODEL_ID"
export DARVAREH_BASE_URL="https://api.darvareh.ir/v1"
export PORT="8080"

در PowerShell ویندوز:

$env:DARVAREH_API_KEY="YOUR_DARVAREH_API_KEY"
$env:DARVAREH_MODEL="YOUR_MODEL_ID"
$env:DARVAREH_BASE_URL="https://api.darvareh.ir/v1"
$env:PORT="8080"

API Key را داخل فایل Go ننویسید:

// این کار مناسب نیست
apiKey := "sk-..."

فایل‌های .env نیز نباید وارد Git شوند. اگر از آن‌ها در محیط توسعه استفاده می‌کنید، نام فایل را به .gitignore اضافه کنید.

مرتب‌سازی و بررسی کد

دستورهای زیر را اجرا کنید:

go fmt ./...
go vet ./...
go mod tidy

سپس پروژه را اجرا کنید:

go run ./cmd/api

در صورت موفقیت، باید یک Log مشابه زیر مشاهده کنید:

{
  "level": "INFO",
  "msg": "HTTP server started",
  "port": "8080"
}

آزمایش Health Check

curl http://localhost:8080/health

پاسخ:

{
  "status": "ok"
}

این Endpoint برای بررسی وضعیت Container، Load Balancer و سیستم Monitoring مناسب است.

آزمایش Endpoint چت

curl -X POST "http://localhost:8080/api/ai/chat" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "سه مزیت استفاده از Go برای ساخت Microservice را توضیح بده.",
    "system_prompt": "پاسخ را کوتاه، دقیق و فارسی بنویس."
  }'

نمونه پاسخ:

{
  "answer": "Go به دلیل مصرف حافظه مناسب، پشتیبانی ساده از هم‌زمانی و تولید فایل اجرایی مستقل، گزینه مطلوبی برای ساخت Microservice است.",
  "model": "YOUR_MODEL_ID",
  "usage": {
    "prompt_tokens": 41,
    "completion_tokens": 52,
    "total_tokens": 93
  }
}

محتوا و تعداد توکن‌های واقعی به مدل و پرامپت بستگی دارند.

آزمایش Endpoint خلاصه‌سازی

curl -X POST "http://localhost:8080/api/ai/summarize" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "متن طولانی موردنظر خود را در این قسمت قرار دهید. این متن باید حداقل چند جمله داشته باشد تا خلاصه‌سازی آن قابل ارزیابی باشد.",
    "max_paragraphs": 2
  }'

آزمایش تحلیل بازخورد

curl -X POST "http://localhost:8080/api/ai/analyze-feedback" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "محصول خوب بود اما سفارش من سه روز دیرتر از زمان اعلام‌شده تحویل داده شد."
  }'

نمونه خروجی:

{
  "category": "delivery",
  "sentiment": "negative",
  "priority": 3,
  "summary": "مشتری از تأخیر در تحویل سفارش ناراضی است."
}

پارامتر response_format باید توسط مدل انتخابی پشتیبانی شود. اگر مدل از JSON Mode پشتیبانی نمی‌کند، می‌توانید این پارامتر را حذف کنید؛ اما همچنان باید خروجی را با json.Unmarshal و قوانین برنامه اعتبارسنجی کنید.

پیاده‌سازی Streaming در Go

Endpoint زیر پاسخ را به‌صورت Server-Sent Events یا SSE ارائه می‌کند:

POST /api/ai/chat/stream

برای آزمایش از گزینه -N در cURL استفاده کنید تا Buffering خروجی غیرفعال شود:

curl -N -X POST "http://localhost:8080/api/ai/chat/stream" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Goroutine را برای یک برنامه‌نویس تازه‌کار توضیح بده.",
    "system_prompt": "با یک مثال ساده و به زبان فارسی پاسخ بده."
  }'

خروجی به‌تدریج دریافت می‌شود:

data: {"token":"Goroutine"}

data: {"token":" یک"}

data: {"token":" واحد"}

data: {"token":" اجرای سبک..."}

data: [DONE]

چرا پاسخ Streaming بهتر به نظر می‌رسد؟

در پاسخ معمولی، کاربر باید تا پایان تولید متن منتظر بماند. اگر تولید پاسخ ۲۰ ثانیه زمان ببرد، صفحه برای ۲۰ ثانیه بدون تغییر باقی می‌ماند.

در Streaming، اولین بخش‌های پاسخ زودتر نمایش داده می‌شوند. زمان کل پردازش لزوماً کمتر نمی‌شود، اما زمان دریافت اولین Token کاهش می‌یابد و تجربه کاربری بهتر می‌شود.

Streaming برای این موارد مناسب است:

  • چت‌بات
  • دستیار تولید محتوا
  • پاسخ‌های طولانی
  • توضیح کد
  • تولید گزارش
  • دستیار جست‌وجو
  • رابط‌های مکالمه‌ای

برای طبقه‌بندی کوتاه یا دریافت یک JSON کوچک، پاسخ عادی ساده‌تر است.

دریافت Streaming در JavaScript

Frontend می‌تواند پاسخ Streaming را با fetch بخواند:

async function streamChat(message, onToken) {
  const response = await fetch(
    "https://api.example.com/api/ai/chat/stream",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        message,
        system_prompt: "پاسخ را فارسی بنویس."
      })
    }
  );

  if (!response.ok || !response.body) {
    throw new Error(`Request failed: ${response.status}`);
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  let buffer = "";

  while (true) {
    const { value, done } = await reader.read();

    if (done) {
      break;
    }

    buffer += decoder.decode(value, {
      stream: true
    });

    const events = buffer.split("\n\n");
    buffer = events.pop() ?? "";

    for (const event of events) {
      const dataLine = event
        .split("\n")
        .find(line => line.startsWith("data:"));

      if (!dataLine) {
        continue;
      }

      const data = dataLine.slice(5).trim();

      if (data === "[DONE]") {
        return;
      }

      const parsed = JSON.parse(data);
      onToken(parsed.token);
    }
  }
}

let answer = "";

streamChat(
  "Context در Go چه کاربردی دارد؟",
  token => {
    answer += token;
    document.querySelector("#answer").textContent = answer;
  }
);

استفاده از buffer ضروری است؛ زیرا یک Chunk شبکه ممکن است نصف یک رویداد SSE یا چند رویداد را هم‌زمان در خود داشته باشد.

مدیریت Context و لغو درخواست

در Handler این مقدار را به Client منتقل کردیم:

c.Request.Context()

اگر کاربر اتصال را قطع کند، Context درخواست لغو می‌شود. چون همان Context به http.NewRequestWithContext منتقل شده است، درخواست برنامه Go به سرویس بالادستی نیز امکان توقف پیدا می‌کند.

این الگو منابع سرور را آزاد می‌کند و از ادامه پردازشی که دیگر مصرف‌کننده‌ای ندارد جلوگیری می‌کند.

طبق راهنمای رسمی Go درباره لغو عملیات، context.Context می‌تواند لغو یا پایان مهلت یک عملیات را در زنجیره فراخوانی‌ها منتقل کند.

الگوی مناسب:

func handler(c *gin.Context) {
	ctx := c.Request.Context()

	result, err := service.Run(ctx)
	// ...
}

الگوی نامناسب:

func handler(c *gin.Context) {
	result, err := service.Run(
		context.Background(),
	)
	// Context درخواست از بین رفته است
}

تعیین Timeout برای هر عملیات

می‌توانید علاوه بر Timeout کلی http.Client، برای یک Endpoint مهلت اختصاصی تعریف کنید:

ctx, cancel := context.WithTimeout(
	c.Request.Context(),
	45*time.Second,
)
defer cancel()

result, err := h.aiClient.Complete(
	ctx,
	input.Message,
	input.SystemPrompt,
)

Timeout مناسب به کاربرد بستگی دارد:

کاربردبازه شروع پیشنهادی
طبقه‌بندی کوتاه۱۰ تا ۳۰ ثانیه
استخراج JSON۱۵ تا ۴۵ ثانیه
چت معمولی۳۰ تا ۹۰ ثانیه
پاسخ طولانیStreaming با مهلت بیشتر
پردازش چند دقیقه‌ایJob Queue و Worker

این مقادیر قطعی نیستند و باید با داده واقعی محصول تنظیم شوند.

مدیریت تاریخچه چت

نمونه فعلی فقط یک پیام کاربر را ارسال می‌کند. برای چت‌بات واقعی باید تاریخچه مکالمه را نیز در messages قرار دهید:

messages := []ai.Message{
	{
		Role:    "system",
		Content: "تو دستیار پشتیبانی فروشگاه هستی.",
	},
	{
		Role:    "user",
		Content: "زمان ارسال سفارش چقدر است؟",
	},
	{
		Role:    "assistant",
		Content: "معمولاً دو تا چهار روز کاری.",
	},
	{
		Role:    "user",
		Content: "برای شهرستان چطور؟",
	},
}

اگر فقط پیام آخر ارسال شود، مدل نمی‌داند عبارت «برای شهرستان چطور؟» درباره چه موضوعی است.

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

  • PostgreSQL
  • MySQL
  • Redis
  • MongoDB
  • حافظه موقت برای نمونه اولیه

ساختار پیشنهادی جدول پیام‌ها:

id
conversation_id
user_id
role
content
model
prompt_tokens
completion_tokens
created_at

کنترل طول تاریخچه

ارسال تمام پیام‌های قدیمی باعث افزایش تعداد توکن (Token)، زمان پاسخ و هزینه می‌شود.

یک سیاست عملی می‌تواند چنین باشد:

  • System Prompt ثابت را نگه دارید.
  • آخرین ۱۰ تا ۲۰ پیام را ارسال کنید.
  • پیام‌های قدیمی‌تر را خلاصه کنید.
  • اطلاعات ثابت کاربر را جدا از تاریخچه نگه دارید.
  • پیام‌های غیرضروری را حذف کنید.
  • تعداد توکن را پیش از ارسال تخمین بزنید.

ساختار پیام‌ها می‌تواند شامل این بخش‌ها باشد:

System Prompt
خلاصه مکالمه قدیمی
اطلاعات ضروری کاربر
چند پیام اخیر
پیام جدید

انتخاب Temperature مناسب

پارامتر temperature میزان تنوع پاسخ را کنترل می‌کند.

کاربردTemperature پیشنهادی
استخراج JSON0 تا 0.2
طبقه‌بندی0 تا 0.2
خلاصه‌سازی دقیق0.1 تا 0.4
پاسخ‌گویی عمومی0.3 تا 0.7
ایده‌پردازی0.7 تا 1
نوشتن خلاقانه0.8 تا 1.2

برای عملیات قابل‌اندازه‌گیری مانند استخراج داده، مقدار پایین‌تر معمولاً خروجی باثبات‌تری ایجاد می‌کند.

مدیریت خطاهای API

در Client، هر پاسخ خارج از محدوده ۲xx به خطا تبدیل می‌شود. بهتر است وضعیت‌های مختلف را در سطح Handler به پاسخ مناسب تبدیل کنید.

وضعیتعلت احتمالیاقدام پیشنهادی
400ساختار درخواست نامعتبربررسی JSON و پارامترها
401API Key اشتباهبررسی متغیر محیطی
403دسترسی نامعتبربررسی حساب یا مدل
404Endpoint یا مدل اشتباهبررسی Base URL و Model ID
429محدودیت درخواستBackoff و کنترل مصرف
500 تا 599خطای موقت سرویسRetry محدود یا Fallback
Timeoutطولانی‌شدن پاسخکاهش خروجی یا استفاده از Streaming

خطای داخلی سرویس بالادستی را مستقیماً به کاربر نمایش ندهید. جزئیات را در Log ثبت و یک پیام کنترل‌شده بازگردانید.

پیاده‌سازی Retry محدود

Retry نباید برای همه خطاها انجام شود. برای نمونه، ارسال دوباره درخواست با API Key اشتباه نتیجه‌ای ندارد.

Retry بیشتر برای این موارد مناسب است:

  • بعضی خطاهای ۵xx
  • قطع موقت اتصال
  • خطای موقت DNS
  • وضعیت 429 با رعایت Retry-After

یک Backoff ساده:

func retryDelay(attempt int) time.Duration {
	switch attempt {
	case 0:
		return 500 * time.Millisecond
	case 1:
		return time.Second
	default:
		return 2 * time.Second
	}
}

پیش از Retry باید بررسی کنید:

  • Context لغو نشده باشد.
  • وضعیت خطا موقت باشد.
  • تعداد تلاش‌ها محدود باشد.
  • درخواست تکراری اثر جانبی ناخواسته ایجاد نکند.
  • تأخیر باعث عبور از Deadline نشود.

Rate Limiting در API

اگر Endpoint هوش مصنوعی بدون محدودیت منتشر شود، یک کاربر می‌تواند تعداد زیادی درخواست ایجاد کند.

Rate Limit را می‌توانید بر اساس این شناسه‌ها اعمال کنید:

  • User ID
  • API Key داخلی
  • Organization ID
  • IP Address
  • نوع اشتراک

برای سرویس تک‌سرور می‌توان از حافظه برنامه استفاده کرد؛ اما در چند Instance بهتر است وضعیت Rate Limit در Redis یا سامانه مشترک نگه‌داری شود.

یک سیاست نمونه:

کاربر عادی: 20 درخواست در دقیقه
کاربر حرفه‌ای: 100 درخواست در دقیقه
سازمان: بر اساس قرارداد

Rate Limiting جایگزین محدودیت هزینه نیست. بهتر است سقف مصرف روزانه یا ماهانه نیز تعریف شود.

کاهش هزینه API هوش مصنوعی

برای کنترل هزینه در برنامه Go:

  • طول ورودی را محدود کنید.
  • max_tokens را متناسب با کاربرد تنظیم کنید.
  • تاریخچه قدیمی را خلاصه کنید.
  • پاسخ‌های تکراری را Cache کنید.
  • از مدل مناسب همان وظیفه استفاده کنید.
  • درخواست‌های نامعتبر را پیش از ارسال رد کنید.
  • مصرف هر کاربر را ثبت کنید.
  • سقف روزانه و ماهانه تعریف کنید.
  • System Promptهای بسیار طولانی را بازبینی کنید.
  • عملیات سنگین را وارد صف کنید.

برای مشاهده قیمت به‌روز مدل‌ها به صفحه مدل‌های درواره مراجعه کنید.

کش‌کردن پاسخ‌ها

اگر یک پرامپت ثابت بارها تکرار می‌شود، می‌توانید پاسخ را Cache کنید.

کلید Cache باید حداقل شامل این موارد باشد:

Model
System Prompt
User Prompt
Temperature
Max Tokens
Prompt Version

ساخت Hash در Go:

package cachekey

import (
	"crypto/sha256"
	"encoding/hex"
)

func Create(value string) string {
	sum := sha256.Sum256([]byte(value))
	return hex.EncodeToString(sum[:])
}

کش برای این سناریوها مناسب است:

  • سؤال‌های متداول
  • خلاصه یک سند ثابت
  • تولید توضیح برای داده‌ای که تغییر نمی‌کند
  • طبقه‌بندی ورودی تکراری

برای پاسخ شخصی‌سازی‌شده یا اطلاعات لحظه‌ای، سیاست Cache باید با دقت بیشتری طراحی شود.

ثبت مصرف توکن

اگر پاسخ API شامل اطلاعات usage باشد، آن را در پایگاه داده ثبت کنید:

type UsageRecord struct {
	UserID           string
	Model            string
	PromptTokens     int
	CompletionTokens int
	TotalTokens      int
	DurationMS       int64
	Status           string
}

این اطلاعات برای موارد زیر مفید است:

  • محاسبه هزینه هر قابلیت
  • محدودکردن مصرف کاربران
  • شناسایی Endpointهای پرهزینه
  • مقایسه مدل‌ها
  • بررسی افزایش ناگهانی مصرف
  • طراحی بسته‌های اشتراک

API Key، پرامپت حساس یا اطلاعات خصوصی کاربر را بدون ضرورت در Log ذخیره نکنید.

نوشتن تست برای Handler

برای تست HTTP در Go می‌توان از Package استاندارد net/http/httptest استفاده کرد.

ابتدا یک Fake Client ایجاد کنید:

package httpapi_test

import (
	"context"

	"example.com/darvareh-go-ai/internal/ai"
)

type fakeAIClient struct {
	result *ai.ChatResult
	err    error
}

func (f *fakeAIClient) Complete(
	ctx context.Context,
	message string,
	systemPrompt string,
) (*ai.ChatResult, error) {
	return f.result, f.err
}

func (f *fakeAIClient) CompleteJSON(
	ctx context.Context,
	prompt string,
	target any,
) error {
	return f.err
}

func (f *fakeAIClient) Stream(
	ctx context.Context,
	message string,
	systemPrompt string,
	onToken func(string) error,
) error {
	if f.err != nil {
		return f.err
	}

	for _, token := range []string{
		"پاسخ",
		" ",
		"آزمایشی",
	} {
		if err := onToken(token); err != nil {
			return err
		}
	}

	return nil
}

نمونه تست Endpoint چت:

package httpapi_test

import (
	"bytes"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"

	"example.com/darvareh-go-ai/internal/ai"
	"example.com/darvareh-go-ai/internal/httpapi"
)

func TestChatEndpoint(t *testing.T) {
	gin.SetMode(gin.TestMode)

	client := &fakeAIClient{
		result: &ai.ChatResult{
			Answer: "پاسخ آزمایشی",
			Model:  "test-model",
		},
	}

	logger := slog.New(
		slog.NewTextHandler(io.Discard, nil),
	)

	router := gin.New()

	handler := httpapi.NewHandler(
		client,
		logger,
	)

	handler.RegisterRoutes(router)

	body := bytes.NewBufferString(`{
		"message": "سلام"
	}`)

	request := httptest.NewRequest(
		http.MethodPost,
		"/api/ai/chat",
		body,
	)

	request.Header.Set(
		"Content-Type",
		"application/json",
	)

	recorder := httptest.NewRecorder()

	router.ServeHTTP(
		recorder,
		request,
	)

	if recorder.Code != http.StatusOK {
		t.Fatalf(
			"expected status 200, got %d",
			recorder.Code,
		)
	}

	expected := `"answer":"پاسخ آزمایشی"`

	if !bytes.Contains(
		recorder.Body.Bytes(),
		[]byte(expected),
	) {
		t.Fatalf(
			"unexpected response: %s",
			recorder.Body.String(),
		)
	}
}

دستور اجرای تست:

go test ./...

توصیه رسمی Gin نیز استفاده از httptest برای تست Handlerهای HTTP است که در مستندات تست Gin توضیح داده شده است.

تست اتصال واقعی به API

تست‌های واحد نباید در هر بار اجرا به مدل واقعی درخواست بفرستند؛ زیرا:

  • هزینه دارند.
  • به شبکه وابسته‌اند.
  • ممکن است کند باشند.
  • پاسخ مدل کاملاً ثابت نیست.

بهتر است تست‌ها را تفکیک کنید:

  • Unit Test با Fake Client
  • Handler Test با httptest
  • Integration Test محدود با API واقعی
  • Evaluation Test برای کیفیت خروجی

Integration Test واقعی را می‌توانید فقط در Pipeline مشخص یا به‌صورت دستی اجرا کنید:

RUN_AI_INTEGRATION_TESTS=true go test ./...

آزمایش کیفیت مدل

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

برای تحلیل بازخورد می‌توانید این موارد را آزمایش کنید:

  • نظر مثبت
  • نظر منفی
  • نظر خنثی
  • متن دارای چند موضوع
  • متن بسیار کوتاه
  • متن فارسی و انگلیسی ترکیبی
  • متن نامرتبط
  • متن دارای غلط املایی
  • ورودی طولانی

معیارها:

  • JSON معتبر باشد.
  • Category معتبر باشد.
  • Sentiment درست تشخیص داده شود.
  • Priority در محدوده باشد.
  • Summary خالی نباشد.
  • زمان پاسخ قابل‌قبول باشد.
  • مصرف توکن بیش‌ازحد نباشد.

پس از تغییر مدل یا پرامپت، همین مجموعه را دوباره اجرا کنید.

فعال‌کردن CORS در Gin

اگر Frontend روی دامنه دیگری اجرا می‌شود، باید CORS را تنظیم کنید.

Package رسمی Middleware را نصب کنید:

go get github.com/gin-contrib/cors

سپس در main.go:

import (
	"time"

	"github.com/gin-contrib/cors"
)

Middleware:

router.Use(cors.New(cors.Config{
	AllowOrigins: []string{
		"https://example.com",
	},
	AllowMethods: []string{
		"GET",
		"POST",
		"OPTIONS",
	},
	AllowHeaders: []string{
		"Origin",
		"Content-Type",
		"Authorization",
	},
	AllowCredentials: true,
	MaxAge:           12 * time.Hour,
}))

آدرس https://example.com را با دامنه Frontend خود جایگزین کنید.

از AllowOrigins: []string{"*"} برای API دارای Cookie یا اطلاعات حساس استفاده نکنید. CORS نیز جایگزین احراز هویت و مجوزدهی نیست.

ساخت Dockerfile

فایل Dockerfile را ایجاد کنید:

FROM golang:1.25-alpine AS builder

WORKDIR /src

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

COPY . .

RUN CGO_ENABLED=0 GOOS=linux \
    go build \
    -trimpath \
    -ldflags="-s -w" \
    -o /out/api \
    ./cmd/api

FROM alpine:3.22

RUN addgroup -S appgroup \
    && adduser -S appuser -G appgroup

WORKDIR /app

COPY --from=builder /out/api /app/api

USER appuser

EXPOSE 8080

ENTRYPOINT ["/app/api"]

نسخه Go در Dockerfile را با نسخه موردنیاز پروژه و Gin هماهنگ کنید.

ساخت Image:

docker build -t darvareh-go-ai .

اجرای Container:

docker run --rm \
  -p 8080:8080 \
  -e DARVAREH_API_KEY="YOUR_DARVAREH_API_KEY" \
  -e DARVAREH_MODEL="YOUR_MODEL_ID" \
  -e DARVAREH_BASE_URL="https://api.darvareh.ir/v1" \
  darvareh-go-ai

API Key را داخل Dockerfile یا Image قرار ندهید.

تنظیم Production Mode در Gin

در محیط Production مقدار زیر را تنظیم کنید:

export GIN_MODE=release

یا هنگام اجرای Container:

docker run \
  -e GIN_MODE=release \
  ...

Release Mode خروجی‌های Debug غیرضروری Gin را غیرفعال می‌کند. تنظیمات استقرار Gin در مستندات رسمی Deployment توضیح داده شده است.

نکات Reverse Proxy برای Streaming

اگر برنامه پشت Nginx، CDN یا Reverse Proxy اجرا شود، ممکن است پاسخ Streaming بافر شود و تمام Tokenها یک‌باره نمایش داده شوند.

نمونه تنظیم Nginx برای مسیر Streaming:

location /api/ai/chat/stream {
    proxy_pass http://127.0.0.1:8080;

    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;

    proxy_buffering off;
    proxy_cache off;

    proxy_read_timeout 180s;
    proxy_send_timeout 180s;

    gzip off;
}

پیش از تغییر تنظیمات، Endpoint را مستقیماً روی پورت برنامه آزمایش کنید. اگر Streaming مستقیم درست است اما پشت Proxy یک‌باره نمایش داده می‌شود، احتمالاً مشکل از Buffering واسط است.

اجرای وظایف طولانی با Job Queue

درخواست HTTP برای کارهایی که چند دقیقه زمان می‌برند مناسب نیست.

برای پردازش طولانی بهتر است:

  1. کاربر درخواست را ثبت کند.
  2. Backend یک Job بسازد.
  3. Job وارد Queue شود.
  4. Worker پردازش را انجام دهد.
  5. نتیجه در پایگاه داده ذخیره شود.
  6. کاربر وضعیت را با Job ID دریافت کند.

نمونه پاسخ اولیه:

{
  "job_id": "job_8f2a1",
  "status": "queued"
}

این الگو برای موارد زیر مناسب است:

  • تحلیل تعداد زیادی سند
  • پردازش فایل‌های طولانی
  • تولید محتوای گروهی
  • ساخت گزارش‌های بزرگ
  • اجرای چند درخواست متوالی
  • پردازش فایل صوتی یا تصویری

استفاده از Goroutine؛ اشتباه رایج

ممکن است وسوسه شوید برای هر درخواست مدل یک Goroutine بدون مدیریت بسازید:

go func() {
	_, _ = client.Complete(
		context.Background(),
		message,
		systemPrompt,
	)
}()

این کد چند مشکل دارد:

  • Context کاربر از بین می‌رود.
  • نتیجه عملیات مشخص نیست.
  • خطا مدیریت نمی‌شود.
  • تعداد Goroutineها ممکن است نامحدود شود.
  • خاموش‌شدن برنامه می‌تواند کار را ناقص کند.

برای Background Job از Queue، Worker Pool و ذخیره وضعیت استفاده کنید. Goroutine به‌تنهایی جایگزین سامانه مدیریت Job نیست.

چک‌لیست آماده‌سازی برای Production

قبل از انتشار، موارد زیر را بررسی کنید:

  • API Key در کد یا Repository وجود ندارد.
  • Frontend مستقیماً به درواره متصل نمی‌شود.
  • ورودی JSON اعتبارسنجی می‌شود.
  • اندازه بدنه درخواست محدود شده است.
  • Endpointها احراز هویت دارند.
  • Rate Limiting فعال است.
  • Timeout مشخص شده است.
  • Context درخواست به سرویس‌های پایین‌دستی منتقل می‌شود.
  • http.Client بین درخواست‌ها به اشتراک گذاشته می‌شود.
  • تعداد Retry محدود است.
  • پاسخ JSON مدل اعتبارسنجی می‌شود.
  • میزان مصرف توکن ثبت می‌شود.
  • سقف هزینه کاربران مشخص است.
  • API Key در Logها ذخیره نمی‌شود.
  • CORS فقط برای دامنه‌های لازم فعال است.
  • Streaming پشت Reverse Proxy آزمایش شده است.
  • Graceful Shutdown فعال است.
  • Health Check وجود دارد.
  • تست‌های واحد و یکپارچه نوشته شده‌اند.
  • برای عملیات طولانی Job Queue در نظر گرفته شده است.
  • مدل و پرامپت نسخه‌بندی شده‌اند.
  • رفتار Fallback تعریف شده است.

خطاهای رایج اتصال Go به API هوش مصنوعی

خطای API key is required

متغیر محیطی تنظیم نشده است:

export DARVAREH_API_KEY="YOUR_DARVAREH_API_KEY"

در Windows PowerShell:

$env:DARVAREH_API_KEY="YOUR_DARVAREH_API_KEY"

خطای model ID is required

شناسه مدل را تنظیم کنید:

export DARVAREH_MODEL="YOUR_MODEL_ID"

شناسه مدل را از صفحه مدل‌های درواره دریافت کنید.

خطای 401

موارد زیر را بررسی کنید:

  • API Key صحیح باشد.
  • فاصله اضافی در مقدار کلید وجود نداشته باشد.
  • Header به‌شکل صحیح ارسال شود.
  • متغیر محیطی در همان Shell تعریف شده باشد.

Header صحیح:

Authorization: Bearer YOUR_DARVAREH_API_KEY

خطای 404

آدرس نهایی Chat Completions باید به این شکل باشد:

https://api.darvareh.ir/v1/chat/completions

Base URL:

https://api.darvareh.ir/v1

مسیر Client:

/chat/completions

خطای Timeout

این موارد را بررسی کنید:

  • مقدار max_tokens بیش‌ازحد نباشد.
  • تاریخچه طولانی غیرضروری حذف شود.
  • مدل سریع‌تری انتخاب شود.
  • برای پاسخ طولانی از Streaming استفاده شود.
  • Timeout Client و Reverse Proxy هماهنگ باشند.

پاسخ Streaming یک‌باره ظاهر می‌شود

  • curl -N را استفاده کنید.
  • Flush را پس از هر Token اجرا کنید.
  • Buffering در Nginx را غیرفعال کنید.
  • فشرده‌سازی مسیر SSE را بررسی کنید.
  • ابتدا مسیر مستقیم برنامه را آزمایش کنید.

خطای invalid character هنگام Parse JSON

ممکن است مدل علاوه بر JSON، Markdown یا توضیح متنی تولید کرده باشد.

راهکارها:

  • System Prompt را دقیق‌تر کنید.
  • Temperature را روی صفر قرار دهید.
  • از مدل دارای JSON Mode استفاده کنید.
  • response_format را فعال کنید.
  • خروجی را پیش از استفاده اعتبارسنجی کنید.

آیا باید از Gin استفاده کنیم یا net/http؟

هر دو انتخاب معتبر هستند.

استفاده از net/http

مناسب برای:

  • سرویس‌های کوچک
  • وابستگی حداقلی
  • کنترل کامل روی HTTP
  • تیمی که کتابخانه استاندارد را ترجیح می‌دهد

استفاده از Gin

مناسب برای:

  • Endpointهای متعدد
  • نیاز به Middleware
  • اعتبارسنجی ورودی
  • توسعه سریع REST API
  • تیمی که ساختار Framework را ترجیح می‌دهد

اتصال به API درواره به Gin وابسته نیست. Client نوشته‌شده در این مقاله را می‌توانید در Echo، Fiber، Chi یا یک برنامه مبتنی بر net/http نیز استفاده کنید.

چرا درواره برای پروژه‌های Go مناسب است؟

اگر برنامه شما به مدل‌های مختلف هوش مصنوعی نیاز داشته باشد، اتصال جداگانه به هر ارائه‌دهنده باعث افزایش کد، تنظیمات و هزینه نگه‌داری می‌شود.

درواره یک API یکپارچه در اختیار توسعه‌دهندگان قرار می‌دهد. در نتیجه می‌توانید:

  • از یک Base URL ثابت استفاده کنید.
  • API Key را در Backend نگه دارید.
  • مدل مناسب هر کاربرد را انتخاب کنید.
  • بدون بازنویسی کل Client، مدل را تغییر دهید.
  • مصرف مدل‌ها را از یک مسیر مدیریت کنید.
  • از Go، Python، Java، C#، Node.js و سایر زبان‌ها استفاده کنید.

برای شروع، در درواره ثبت‌نام کنید، API Key بسازید و مدل موردنظر را از فهرست مدل‌ها انتخاب کنید.

پرسش‌های متداول

آیا می‌توان با Go برنامه هوش مصنوعی ساخت؟

بله. Go برای ساخت Backend، REST API، Microservice، چت‌بات، Worker و سرویس‌های متصل به مدل‌های هوش مصنوعی مناسب است.

آیا Go برای آموزش مدل هوش مصنوعی مناسب است؟

بیشتر ابزارهای آموزش مدل با Python توسعه یافته‌اند. Go معمولاً برای لایه API، پردازش هم‌زمان، مدیریت درخواست‌ها و اتصال محصول به مدل استفاده می‌شود.

Golang با Go چه تفاوتی دارد؟

نام رسمی زبان Go است. واژه Golang به دلیل نام دامنه قدیمی پروژه و جست‌وجوی آسان‌تر رایج شده است. هر دو معمولاً به همان زبان اشاره می‌کنند.

آیا برای اتصال Go به هوش مصنوعی به SDK نیاز داریم؟

خیر. کتابخانه استاندارد net/http و encoding/json برای ارسال درخواست و دریافت پاسخ کافی هستند.

آیا می‌توان از Gin برای چت‌بات استفاده کرد؟

بله. Gin می‌تواند Endpoint چت، Streaming، احراز هویت، Rate Limiting و مدیریت تاریخچه را در Backend ارائه کند.

آیا API Key را می‌توان در Frontend قرار داد؟

خیر. API Key باید فقط در Backend نگه‌داری شود. قرار دادن آن در JavaScript، اپلیکیشن موبایل یا Repository عمومی می‌تواند باعث افشای کلید شود.

آیا می‌توان پاسخ مدل را Stream کرد؟

بله. با تنظیم stream: true و خواندن SSE می‌توانید Tokenها را هنگام تولید دریافت کنید.

آیا Go می‌تواند چند درخواست هوش مصنوعی را هم‌زمان پردازش کند؟

بله. سرور HTTP در Go درخواست‌ها را هم‌زمان مدیریت می‌کند. بااین‌حال باید محدودیت اتصال، Rate Limit و هزینه را کنترل کنید.

آیا خروجی مدل همیشه JSON معتبر است؟

خیر. خروجی باید با json.Unmarshal خوانده و سپس بر اساس قوانین برنامه اعتبارسنجی شود.

بهترین مدل برای Go کدام است؟

انتخاب مدل به نوع کاربرد، کیفیت، سرعت، هزینه و طول Context بستگی دارد. Go به مدل خاصی وابسته نیست. مدل‌های موجود و قیمت آن‌ها را در صفحه مدل‌های درواره بررسی کنید.

چگونه هزینه API را کاهش دهیم؟

با محدودکردن طول ورودی، کاهش خروجی، خلاصه‌سازی تاریخچه، کش پاسخ‌های تکراری، انتخاب مدل متناسب و ثبت مصرف هر کاربر می‌توانید هزینه را کنترل کنید.

آیا این پروژه روی Linux اجرا می‌شود؟

بله. می‌توانید برنامه Go را مستقیماً یا با Docker روی Linux اجرا کنید.

آیا می‌توان این سرویس را چندسروری کرد؟

بله. اگر وضعیت مکالمه، Rate Limit و Cache را در سرویس‌های مشترکی مانند PostgreSQL و Redis نگه دارید، می‌توانید چند Instance از API اجرا و آن‌ها را پشت Load Balancer قرار دهید.

جمع‌بندی

Go و Gin ترکیب مناسبی برای ساخت Backend برنامه‌های هوش مصنوعی هستند. Go کتابخانه‌های استاندارد مناسبی برای HTTP، JSON، Context و پردازش هم‌زمان دارد و Gin نیز ساخت REST API، Routing و اعتبارسنجی را ساده می‌کند.

در این آموزش یک پروژه عملی ساختیم که:

  • به API درواره متصل می‌شود.
  • API Key را خارج از کد نگه می‌دارد.
  • Endpoint چت ارائه می‌دهد.
  • متن را خلاصه می‌کند.
  • بازخورد را به JSON تبدیل می‌کند.
  • پاسخ مدل را به‌صورت Streaming نمایش می‌دهد.
  • Context و Cancellation را مدیریت می‌کند.
  • از Connection Pool و Timeout استفاده می‌کند.
  • قابلیت تست با Fake Client دارد.
  • با Docker قابل‌استقرار است.
  • برای توسعه به یک سرویس Production آماده شده است.

برای شروع ساخت برنامه هوش مصنوعی با Go، وارد درواره شوید، یک API Key ایجاد کنید و شناسه مدل متناسب با پروژه را از صفحه مدل‌ها انتخاب کنید.

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more