使用 Golang + Google Gemini API 打造 Discord AI 聊天機器人

Posted by Wayne X.Y. on Wednesday, February 11, 2026

使用 Golang + Google Gemini API 打造 Discord AI 聊天機器人

本文將使用 Golang 語言,搭配 Google Gemini API,實作一個 Discord 聊天機器人。


🛠️ 前置需求 (Prerequisites)

在開始之前,請確保你已經準備好以下開發環境與 API Key:

  1. Golang 環境: 安裝 Go 1.25 或更高版本。
  2. Discord Bot Token:
    • 前往 Discord Developer Portal
    • 建立一個新的 Application,並新增一個 Bot。
    • 取得 Bot Token (請妥善保存,不要外洩)。
    • 開啟 Message Content Intent 權限,這樣機器人才讀得到使用者的訊息內容。
    • 透過 OAuth2 將 bot 邀請到你的 Discord 伺服器。
      • 選擇 scope
      • 選擇 permission
      • 產生 url 並邀請 bot

      ⚠️ 注意:此時的 bot 應該為離線狀態

  3. Google Gemini API Key:

🚀 專案初始化

首先,建立一個新的專案資料夾並初始化 Go 模組:

mkdir discord-chatbot
cd discord-chatbot
go mod init discord-chatbot

接著,我們需要安裝兩個主要的套件:

  • github.com/bwmarrin/discordgo: Discord 的 Go 語言 SDK。
  • google.golang.org/genai: Google GenAI 的 Go 語言 SDK。
  • github.com/joho/godotenv: 用來讀取 .env 環境變數檔。
go get github.com/bwmarrin/discordgo
go get google.golang.org/genai
go get github.com/joho/godotenv

💻 程式實作

專案結構非常簡單,主要分為 main.go (處理 Discord 邏輯) 與 gemini.go (處理 AI 邏輯)。

1. Gemini Client (gemini.go)

為了讓機器人能夠像真人一樣對話,我們需要實作一個機制來儲存對話歷史。這裡我們建立一個 GeminiClient 結構,並使用 map 來儲存不同頻道的對話紀錄。

你可以將 model 替換成你需要的模型,例如 gemini-2.5-flash-lite

package main

import (
	"context"
	"fmt"
	"sync"

	"google.golang.org/genai"
)

// GeminiClient 包裝 Google GenAI 客戶端並管理對話歷史
type GeminiClient struct {
	client   *genai.Client
	sessions map[string][]*genai.Content // Key: Channel ID, Value: 對話歷史
	mu       sync.Mutex                  // Mutex 用於保護 map 的並發存取
}

func NewGeminiClient(apiKey string) (*GeminiClient, error) {
	ctx := context.Background()
	client, err := genai.NewClient(ctx, &genai.ClientConfig{
		APIKey: apiKey,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to create genai client: %w", err)
	}

	return &GeminiClient{
		client:   client,
		sessions: make(map[string][]*genai.Content),
	}, nil
}

// Chat 發送訊息給 Gemini 並維護對話歷史
func (gc *GeminiClient) Chat(channelID, userMessage string) (string, error) {
	gc.mu.Lock()
	defer gc.mu.Unlock()

	// 初始化該頻道的歷史紀錄
	if _, ok := gc.sessions[channelID]; !ok {
		gc.sessions[channelID] = []*genai.Content{}
	}

	// 新增使用者訊息到歷史紀錄
	gc.sessions[channelID] = append(gc.sessions[channelID], &genai.Content{
		Role: "user",
		Parts: []*genai.Part{
			{Text: userMessage},
		},
	})

	// 優化:只保留最近 20 則訊息,避免 Token 超出限制或消耗過多資源
	const maxHistory = 20
	if len(gc.sessions[channelID]) > maxHistory {
		gc.sessions[channelID] = gc.sessions[channelID][len(gc.sessions[channelID])-maxHistory:]
	}

	// 指定使用 gemini 模型
	model := "gemini-2.5-flash"

	// 呼叫 Gemini API
	resp, err := gc.client.Models.GenerateContent(context.Background(), model, gc.sessions[channelID], nil)
	if err != nil {
		return "", fmt.Errorf("failed to generate content: %w", err)
	}

	// 解析回應
	if len(resp.Candidates) == 0 || len(resp.Candidates[0].Content.Parts) == 0 {
		return "", fmt.Errorf("no response candidates returned")
	}

	var aiResponse string
	for _, part := range resp.Candidates[0].Content.Parts {
		if part.Text != "" {
			aiResponse += part.Text
		}
	}

	// 將 AI 的回應也加入歷史紀錄
	gc.sessions[channelID] = append(gc.sessions[channelID], &genai.Content{
		Role: "model",
		Parts: []*genai.Part{
			{Text: aiResponse},
		},
	})

	return aiResponse, nil
}

// Reset 清除指定頻道的對話紀錄
func (gc *GeminiClient) Reset(channelID string) {
	gc.mu.Lock()
	defer gc.mu.Unlock()
	delete(gc.sessions, channelID)
}

2. Main Entry (main.go)

在主程式中,我們負責連線 Discord Gateway,並監聽 MessageCreate 事件。當收到 !ai 指令時,就呼叫 GeminiClient 取得回應。

package main

import (
	"fmt"
	"log"
	"os"
	"os/signal"
	"strings"
	"syscall"

	"github.com/bwmarrin/discordgo"
	"github.com/joho/godotenv"
)

var geminiClient *GeminiClient

func main() {
	// 讀取 .env
	godotenv.Load()

	botToken := os.Getenv("DISCORD_BOT_TOKEN")
	geminiKey := os.Getenv("GEMINI_API_KEY")

	if botToken == "" || geminiKey == "" {
		log.Fatal("請在 .env 檔案中設定 DISCORD_BOT_TOKEN 與 GEMINI_API_KEY")
	}

	// 初始化 Gemini Client
	var err error
	geminiClient, err = NewGeminiClient(geminiKey)
	if err != nil {
		log.Fatal("Error creating Gemini client:", err)
	}

	// 初始化 Discord Session
	dg, err := discordgo.New("Bot " + botToken)
	if err != nil {
		log.Fatal("error creating Discord session,", err)
	}

	// 註冊訊息事件處理器
	dg.AddHandler(messageCreate)

	// 設定 Intent
	dg.Identify.Intents = discordgo.IntentsGuildMessages

	// 連線
	err = dg.Open()
	if err != nil {
		log.Fatal("error opening connection,", err)
	}
	defer dg.Close()

	fmt.Println("Bot is now running. Press CTRL-C to exit.")

	// 等待中斷訊號
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
	<-sc
}

func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
	if m.Author.ID == s.State.User.ID {
		return
	}

	// 指令: !reset (清除記憶)
	if m.Content == "!reset" {
		if geminiClient != nil {
			geminiClient.Reset(m.ChannelID)
			s.ChannelMessageSend(m.ChannelID, "🧹 已清除本頻道的對話記憶,我們可以重新開始了!")
		}
		return
	}

	// 指令: !ai <訊息>
	if strings.HasPrefix(m.Content, "!ai ") {
		userMessage := strings.TrimPrefix(m.Content, "!ai ")
		if userMessage == "" {
			s.ChannelMessageSend(m.ChannelID, "請輸入訊息,例如:`!ai 你好`")
			return
		}

		// 顯示「正在輸入...」狀態
		s.ChannelTyping(m.ChannelID)

		if geminiClient != nil {
			response, err := geminiClient.Chat(m.ChannelID, userMessage)
			if err != nil {
				log.Println("Gemini Error:", err)
				s.ChannelMessageSend(m.ChannelID, "❌ AI 發生錯誤,請稍後再試。")
				return
			}

			// Discord 訊息長度限制 2000 字,這裡做個簡單截斷處理
			if len(response) > 2000 {
				s.ChannelMessageSend(m.ChannelID, response[:1990]+"...\n(回應過長已截斷)")
			} else {
				s.ChannelMessageSend(m.ChannelID, response)
			}
		}
	}
}

🏃‍♂️ 執行與測試

1. 設定環境變數

在專案根目錄建立 .env 檔案:

DISCORD_BOT_TOKEN=你的Token
GEMINI_API_KEY=你的APIKey

2. 啟動

go run .

3. 指令測試

回到你的 Discord 伺服器 (Bot 需在伺服器內),試試看以下指令:

  • !ai 嗨,請用一句話從我介紹 Golang
  • !ai 承上題,那 Python 呢? (測試上下文記憶)
  • !reset (清除記憶)

🔮 What’s Next?

目前我們的 Bot 只能在本地電腦執行,一關機就沒了。為了讓它能夠 24 小時運作,有時間再研究看看怎麼部署到雲端~