09/06/2026 04:21am

Golang The Series EP.150: Workshop 1: Building a Simple AI Chatbot Server with Gin Framework
#Gin Framework
#Go Web Server
#AI Chatbot Backend
#Real-time Streaming
#Server-Sent Events
#Go
#Golang
We have finally reached EP.150, Gophers! Congratulations on gathering all the essential puzzle pieces—from managing environments with Docker and streaming with Channels to budget control via Token Management.
In this episode, it's time to assemble everything we've learned into our first practical workshop. We will build a Simple AI Chatbot Server that supports real-time data streaming using the Gin Framework and the OpenAI SDK.
Project Structure
To keep our codebase clean and maintainable, we will adhere to a standard, lightweight Go project structure:
Plaintext
ai-chatbot-server/
├── main.go
├── handlers/
│ └── chat.go
├── go.mod
└── go.sum
Initialize the project and install the necessary dependencies via your terminal:
Bash
go mod init ai-chatbot-server
go get github.com/gin-gonic/gin
go get github.com/sashabaranov/go-openai
The Main Driver: main.go
This file handles retrieving the API Key from environment variables, initializing the OpenAI Client, and setting up Gin routes to prepare the data streaming pipeline.
Go
package main
import (
"log"
"os"
"ai-chatbot-server/handlers"
"github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai"
)
func main() {
// 1. Retrieve the API Key from environment variables for security
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
log.Fatal("ERROR: OPENAI_API_KEY env variable is required")
}
// 2. Initialize the OpenAI Client
aiClient := openai.NewClient(apiKey)
// 3. Set up the Gin Engine
r := gin.Default()
// Inject the dependency (AI Client) into the handler using a Closure
r.POST("/api/chat/stream", handlers.HandleChatStream(aiClient))
log.Println("🚀 AI Chatbot Server starting on :8080...")
r.Run(":8080")
}
The Streaming Pipeline: handlers/chat.go
We will implement Server-Sent Events (SSE) alongside OpenAI's streaming capability to push text chunks to the client immediately as the AI generates them. Gin provides the c.Stream() function, making this elegant and straightforward to implement.
Go
package handlers
import (
"context"
"errors"
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai"
)
// ChatRequest defines the incoming payload from the client
type ChatRequest struct {
Message string `json:"message" binding:"required"`
}
func HandleChatStream(client *openai.Client) gin.HandlerFunc {
return func(c *gin.Context) {
var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
ctx := c.Request.Context()
// 1. Configure the request payload with streaming enabled
streamReq := openai.ChatCompletionRequest{
Model: openai.GPT4o, // Ensure you are using the latest go-openai version, or use the "gpt-4o" string directly
Stream: true,
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleUser,
Content: req.Message,
},
},
}
stream, err := client.CreateChatCompletionStream(ctx, streamReq)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer stream.Close()
// Production Essential: Set headers for Server-Sent Events (SSE)
// This prevents reverse proxies (like Nginx or Cloudflare) from buffering the response.
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("Transfer-Encoding", "chunked")
// 2. Utilize Gin's c.Stream to flush real-time tokens continuously
c.Stream(func(w io.Writer) bool {
select {
case <-ctx.Done():
// Best Practice: If the client disconnects or cancels, exit the loop immediately
return false
default:
response, err := stream.Recv()
if errors.Is(err, io.EOF) {
// Notify the client that the stream has finished successfully
c.SSEvent("message", "[DONE]")
return false
}
if err != nil {
// Broadcast the error event to the client and stop streaming
c.SSEvent("error", err.Error())
return false
}
// Extract and stream individual text chunks
if len(response.Choices) > 0 {
content := response.Choices[0].Delta.Content
if content != "" {
// Deliver data chunks to the client instantly via SSE format
c.SSEvent("message", content)
}
}
return true // Keep looping to fetch the next data chunk
}
})
}
}
🎯 Daily Mission
Once your server is running (remember to set your environment variable using export OPENAI_API_KEY="your-key"), you can test your streaming backend using curl in your terminal:
Bash
# The -N flag is crucial here; it disables curl's internal buffering, letting you see the characters flow in real-time.
curl -N -X POST http://localhost:8080/api/chat/stream \
-H "Content-Type: application/json" \
-d '{"message": "Give me 3 concise reasons why we should write Go in an AI-First era."}'
Pro-Tip Challenge: Right now, our bot is stateless (it answers prompt-by-prompt). Try leveraging Go slices to implement a persistent Chat History memory array before sending the payload to OpenAI. This will make your chatbot capable of fluid, contextual conversations!
FAQ
Q: Why do we explicitly set Cache-Control: no-cache and Transfer-Encoding: chunked before executing c.Stream()?
A: When running a Go application behind a reverse proxy (such as Nginx, Apache, or Cloudflare), these proxies naturally attempt to "buffer" data until they receive the entire response payload to optimize network distribution. Setting these headers explicitly tells the proxy: "Do not buffer this data. Let it pass through piece-by-piece immediately." This guarantees smooth, low-latency rendering on the client interface.
Q: How does Gin’s c.Stream() function operate under the hood, and is it memory-safe?
A: Under the hood, c.Stream() continuously executes the anonymous function you provide as long as that function returns true and the underlying HTTP connection remains open. This architecture maintains an incredibly low memory footprint because individual chunks are serialized into bytes and immediately flushed to the network buffer rather than accumulating inside the server's RAM.
Q: What happens if a user abruptly closes their browser tab mid-stream?
A: The ctx variable inherited from c.Request.Context() will capture a cancellation signal instantly. Thanks to our select block guarding the operation, the routine detects <-ctx.Done() on the next loop cycle, returns false, and triggers a graceful termination of the Goroutine, completely eliminating potential Goroutine leaks.
Conclusion
Combining the Gin Framework with Go Concurrency and OpenAI's streaming API empowers us to build incredibly efficient, resilient, and high-performance streaming servers on the backend without exhausting infrastructure resources. That is the true engineering beauty of Go!
Next Episode (EP.151): No matter how smart or fast our AI is, it remains blind to internal company data, private assets, or real-time documentation updates. Next time, we're stepping into an advanced paradigm: "What is RAG? Why Your AI Needs a Private Knowledge Base." Get ready to unpack Retrieval-Augmented Generation!
Follow Superdev Academy on all platforms:
🔵 Facebook: Superdev Academy Thailand
🎬 YouTube: Superdev Academy Channel
📸 Instagram: @superdevacademy
🎬 TikTok: @superdevacademy
🌐 Website: superdevacademy.com