一直以來,我都喜歡用新技術做一些小專案,從前端框架到部署平台,只要覺得有趣、有挑戰性,就會忍不住想親手試一次。這次的主角是 Go 和 Telegram Bot。我做了一個小小的專案:calpa/go-bot,用 Go 寫成、以 Webhook 為基礎的 Telegram Echo Bot。表面上它只是一個會把訊息原封不動加上前綴再回傳的機器人,但對我來說,它代表的是一個完整串起來的實戰:從本機環境、到 Telegram API、再到雲端服務。
這篇文章想記錄幾件事。首先是為什麼會選擇用 Go 來寫 Telegram Bot,接著看看 go-bot 這個專案實際在做什麼,再一路走到它的架構設計、關鍵程式碼和環境設定,最後回到它在本機與雲端實際跑起來時的樣子。這些步驟拆開來都不複雜,但串在一起,會讓人很清楚地感受到「原來我真的用 Go 把一個可以被世界使用的小服務搭起來了」。
為什麼用 Go 寫 Telegram Bot?
我對 Go 的好感來自幾個很直觀的特點。它的編譯速度很快,對開發者來說幾乎沒有「等 build」的痛苦,可以很自然地進入一種快速嘗試的節奏。同時,語言本身把並行當成一等公民,goroutine 和 channel 的設計,讓人很容易想像未來要在同一個服務裡做更多事情時,系統會長成什麼樣子。再加上 Go 編譯出來是一個單一 binary,部署非常乾淨,很適合放上各種雲端平台。
Telegram Bot 則是一個非常適合拿來練習的小題目。它的 API 清楚、文件齊全,只要有一個 HTTP server,提供一個可以接收 Webhook 的 endpoint,就能完成最基本的互動。對我來說,目標很簡單:用 Go 寫一個乾淨、單純,但架構清楚的 Echo Bot。它不需要一開始就做很多事,只要日後想加功能時,不會被一團糊在一起的程式碼卡住就好。
go-bot 就是在這樣的前提下長出來的。
專案本身在做什麼?
在談細節架構之前,先從使用者的角度看,這個 bot 到底在做什麼會比較直覺。從功能層面來看,go-bot 做的事情其實很純粹。當程式啟動時,它會建立一個 HTTP server,預設監聽在 :8000。這個 server 有三個路由。根路由 / 回傳一個非常傳統的文字回應 Hello World,算是確認服務已經正常啟動。/telegram/webhook 則是給 Telegram 發送更新用的 endpoint,當使用者在 Telegram 裡對 bot 說話時,Telegram 會把相關的 Update 結構 POST 到這個路徑。最後一個路由 /set-webhook 則是一個小助手,用來幫我從程式本身呼叫 Telegram 的 setWebhook API,把 Webhook 設定到正確的網址上。
當有訊息進來時,實際處理邏輯發生在 handler 裡。現在的行為非常簡單:如果這次的更新裡沒有 Message,就直接忽略;如果有,就先把收到的文字印到 log,然後再透過 SendMessage 回到同一個聊天裡,訊息內容是前面加上 Echo: 的原文。換句話說,Telegram 變成了一個可以幫你確認系統是不是正常運作的介面,你打什麼,它就幫你回聲一次。
從 main.go 看整體架構
雖然整個專案目前只有一個 main.go 檔案,但我在寫的時候還是刻意把各個職責拆開,讓未來如果要分成不同 package,不會太痛苦。整個流程從 main 函式開始。程式一啟動會先呼叫 loadEnv(),透過 github.com/joho/godotenv 將專案根目錄下的 .env 載入到環境變數裡。這一步是為了後面能夠用 os.Getenv 拿到 Telegram bot token 和 Webhook 相關設定。
接著,我用 signal.NotifyContext 建立了一個會在收到 os.Interrupt(也就是 Ctrl+C)時自動被取消的 context.Context。這個 context 會一路傳遞到整個 HTTP server 和 bot 的生命周期中,最後用來觸發優雅關機。然後是 newBot(),這個函式會讀取 TELEGRAM_BOT_TOKEN 和 TELEGRAM_WEBHOOK_SECRET_TOKEN,建立一個 *bot.Bot。同時,我在這裡把預設的 handler 設成自己的 handler 函式,也把 Webhook 的 secret token 傳給 SDK,讓它在處理 Webhook 請求時能進行驗證。
botClient.StartWebhook(ctx) 則是整個 Telegram 面向的啟動點。當這個 goroutine 跑起來之後,只要有請求打到 /telegram/webhook,由 SDK 提供的 WebhookHandler 就會把資料轉成 Update 結構,然後交給我們定義好的 handler 處理。HTTP 層的部分則是交給 newMux(botClient) 和 newServer(mux)。前者負責把 /、/telegram/webhook 和 /set-webhook 這三個路由都掛到同一個 http.ServeMux 上,後者則是回傳一個 *http.Server,將位址設定為 :8000,並且把 mux 傳給它。
真正啟動 server 的工作發生在 startServer(ctx, srv)。這個函式會在 goroutine 裡呼叫 ListenAndServe,同時印出 HTTP server listening on :8000 這句提示。另一邊它會卡在 <-ctx.Done(),一旦我們在終端機按下 Ctrl+C,context 被取消,就會進一步呼叫 srv.Shutdown,試著以優雅的方式把 HTTP server 關掉。整個結構沒有特別複雜,但足夠完整,讓我在未來要加上日誌、metrics 或是健康檢查時,都有地方可以安放。
實際的 main 函式長得大概是這樣,整個啟動流程都收斂在一起,讀起來也很直覺:
func main() {
loadEnv()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
botClient := newBot()
go botClient.StartWebhook(ctx)
mux := newMux(botClient)
srv := newServer(mux)
startServer(ctx, srv)
}整個 main 只做幾件事:先載入環境變數,建立會在 Ctrl+C 時結束的 context,初始化 Telegram bot client,啟動 Webhook,建立路由與 HTTP server,最後交給 startServer 去負責整個服務的生命週期。其他像是 loadEnv、newBot、newMux、newServer 等邏輯,則各自拆成小函式放在檔案裡,讓程式碼在視覺與心智負擔上都比較輕盈。
環境變數與設定檔
這個專案仰賴的環境變數其實不多,卻串起了整條路徑。TELEGRAM_BOT_TOKEN 來自 Telegram 的 @BotFather,是整個 bot 身分的根本。TELEGRAM_WEBHOOK_SECRET_TOKEN 則是用來驗證 Webhook 請求,用來增加一層安全性。最後的 WEBHOOK_BASE_URL 則代表整個服務對外的 base URL。當 /set-webhook 這個路由被呼叫時,程式會讀取這個 base URL,把尾端多餘的斜線去掉,然後在後面接上 /telegram/webhook,組合出要傳給 Telegram 的完整 Webhook URL。
如果 WEBHOOK_BASE_URL 沒有設定,程式會退回到預設值 http://localhost:8000。這個設計可以讓我在本機測試時比較方便,當然在正式部署到雲端時,還是會明確地把這個變數設成實際的服務網址。
在本機與雲端跑起來的樣子
當我在本機啟動 go-bot 時,它其實就和一個普通的 Go HTTP 服務沒有太大差別。打開瀏覽器連到 http://localhost:8000/,看到 Hello World 的那一刻,會很自然地感覺到「這個世界又多了一個用 Go 寫的小服務」。真正有趣的部分,是當我把 Webhook 設定好之後,在 Telegram 裡對著自己的 bot 打一句話,例如 Hello from Go,幾乎在同一時間,手機螢幕上就會出現一行新的訊息,上面寫著 Echo: Hello from Go。那個瞬間會很直覺地意識到:這個小專案已經不再只是一段在終端機裡跑來跑去的程式碼,而是一個能夠和真實世界互動的東西。
之後我把這個服務部署到雲端。部署的過程和一般 Go 應用沒有太大差別,重點就是確保環境變數都在服務端正確設定,包括 TELEGRAM_BOT_TOKEN、TELEGRAM_WEBHOOK_SECRET_TOKEN 和 WEBHOOK_BASE_URL。服務啟動後,只要在瀏覽器裡打開對應的 /set-webhook 路徑,讓程式幫忙呼叫 Telegram 的 setWebhook API,整個系統就算正式上線了。從那一刻開始,就算關掉本機,這隻 bot 依然會安靜地在雲端替你收訊息、回訊息。
Handler 作為未來擴充的入口
目前的 handler 只是做 Echo,非常單純,但也因為這樣,它成為一個很好的起點。只要願意,隨時可以在裡面加上更多分支,處理像是 /start、/help 這種指令,或者根據不同關鍵字觸發不同行為。也可以根據使用者、群組、時間等條件,做出更細膩的情境回應。對我來說,這個 handler 就像一個門,一開始只開了一條最簡單的通道,但整個架構已經準備好,等著未來的自己慢慢在上面堆東西。
現在的實作大概是這個樣子:
func handler(ctx context.Context, b *bot.Bot, update *models.Update) {
if update.Message == nil {
return
}
fmt.Println("Received update: ", update.Message.Text)
b.SendMessage(ctx, &bot.SendMessageParams{
ChatID: update.Message.Chat.ID,
Text: "Echo: " + update.Message.Text,
})
}一開始會先檢查這次的 Update 裡有沒有 Message,如果沒有就直接 return,避免處理不必要的事件。接著把收到的文字印在 log 裡,最後透過 SendMessage 回到同一個聊天空間,前面加上 Echo: 作為簡單的回應。未來如果要加上指令系統,只要從這裡開始往內拆邏輯就好,不需要動到底層的 Webhook 或 HTTP server 設定。
小結:一個用新技術完成的小里程碑
回頭看 go-bot 這個專案,它的功能其實不複雜,整個程式碼量也不算多,卻完整涵蓋了我想要練習的一切:用 Go 建立 HTTP 服務、處理環境變數與設定檔、透過第三方 SDK 串接 Telegram,再把這個服務部署到雲端,變成一個真正「在線上的」聊天機器人。對我來說,用 Go 做 chatbot 是一個非常剛好的實戰範例,既不會大到難以下手,又足夠讓人感受到從零到一的成就感。
接下來要怎麼長,其實就不再是「能不能做到」的問題,而是「想像力能走到哪裡」的選擇。go-bot 已經把基礎打好:它可以安全地接收訊息、穩定地回應、持續在雲端運作,隨時可以接上新的指令、串更多外部服務,甚至變成真正融入日常流程的工具。只要還有想像,總能找到更好的解法,把這個小小的 chatbot 推向下一個階段。對我來說,這不是一個畫下句點的專案,而是一個很清楚的逗點 —— 之後每一次想用 Go 試驗新點子、想把某個「也許可以這樣做」變成現實時,都可以從這裡出發,讓這隻 bot 和自己一起長大。
如果你有 AI 專案、網站開發或技術整合需求,或正在為團隊尋找工程師,歡迎來信交流: partner@calpa.me
歡迎訂閱 Calpa 的頻道,一同將想像力化為可能: