最近開始學 Go,常聽人說最好的學習方法就是直接 build 一個簡單的小 feature,於是乎便想到了在我原本的 js side project 中建構一個簡單的 go cronjob 的方法。以下紀錄了這個小專案的背景、撰寫,與運行方式。
1. Background
根據彭博社報導,全世界近期受到通貨膨脹的衝擊,尤其是美國面臨了嚴峻的物價指數上漲,根據美國勞工統計局的數據分析,牛絞肉、薯片等基本物品如今的平均價格高於 Covid-19 前水準、汽油價格再次上漲,而電費等日常必需品的成本仍然維持高價位。
有鑑於此,台灣電商 Synoptic 為了可以跟上經濟趨勢、降低銷貨成本,因此希望趁著消費者分神時提高商品價格。方式即是透過打造一個 cronjob, 每分鐘偷偷調高各類商品的價格 1 塊錢。
2. Method
Go module in general
Go 的 repo 通常會由一個 module 與多個 packages 組成。
- module 在 Go 1.11 引入,用於管理多個 packages 與其依賴關係,並支援版本控制,存在一個 go.mod 紀錄版本、依賴項、替換規則等等。
- package 則是一組相關聯的代碼,用於代碼組織封裝與重新利用。
假設有一個 repo 架構如下:
|
|
可知:
- go.mod 文件位於 myapp 目錄中,定義了 “example.com/myapp” 這個 module。
- main.go、utils/util.go 和 models/model.go 都屬於同一個 module “example.com/myapp”。
- utils 和 models 是 package,但它們不需要各自擁有 go.mod 文件。它們共享位於根目錄的 go.mod 文件。
File Structure
先 top-down 的展示這個小專案的檔案架構:
|
|
Init a Go module
執行以下指令,init 我們的專案:
|
|
這時便會產出 go.mod
檔如下:
|
|
Install related packages
在這個專案中,我們需要額外兩個 packages:
godotenv
: 用於讀取寫於.env
的環境變數cron
: 用於執行 cronjob
執行安裝指令如下:
|
|
此時安裝內容便會被記錄於 go.sum
檔案中。每個安裝的 packages 會有兩個 hash value, 分別確保直接下載與紀錄於 module 的依賴是否可以通過校驗。
-u
flag 代表在安裝時,同時檢查是否有缺漏沒有安裝到的 dependencies,有的話就安裝。但他不會更新既有的 dependencies。
main.go
main.go 算是 module 的主要入口點。主要的檔案內容如下:
|
|
在 main.go 中,我們需要兩個函數: incrementProductPrices(), 與 main()。
- incrementProductPrices(): 負責與 db 互動,將每項商品的價格調漲一塊。
- main(): 載入環境變數,並設定 incrementProductPrices() 每分鐘執行一次。
incrementProductPrices()
|
|
這個函數主要有以下幾個重點:
-
這個 Go cronjob 會透過打 api 的方式,讓 PriceRepository (written in Javascript) 執行每個商品價格加ㄧ的動作。POST api 裡面連結到 js price service。這麼做有幾個優點:
- 讓 Go cronjob 調用 Javascript repo 的資源。
- 我們可以不用在 cronjob 中撰寫 db crud 相關指令,讓程式層級切割的更明確。
-
defer response.Body.Close()
:- 這個寫法通常見於 HTTP request / response, 是 Go 語言中的一種用法,用於確保在 HTTP 請求完成後關閉response body,以釋放相關資源。
- “defer” 確保了函數無論在什麼時候結束、如何結束,資源都可以被釋放。
- 如果不寫 defer, 僅僅在最後加上 response.Body.Close(), 可能導致某些 error handling 出口忘記釋放資源,讓資源被洩漏。
-
使用
io.ReadAll()
讓 response 成為一個 list:- 用途:有時候 response 長度會太長,這時我們可以指定最長 logging 的長度,再把 response 讀成一個 list, 借此讓 response 可以好看一點。
-
使用
log
而非fmt
: 讓輸入的狀態擁有時間戳、格式化輸出選擇。
main()
|
|
這個函數主要有以下幾個重點:
-
載入環境變數,並在無法順利載入時 log.Fatalf()
- 在 main() 而非 incrementProductPrices() 調用環境變數,可以確保調用的行為只會發生一次,降低不必要的開銷。
-
cron.New(cron.WithSeconds())
- 創建一個 cronjob, 並將精細度由預設的分鐘級調整為秒級
-
schedule.AddFunc("0 * * * * *", incrementProductPrices)
:- 將 “incrementProductPrices” 添加至 cronjob中,並設定每小時的第 0 秒(=每分鐘)執行一次。
-
schedule.Start()
: 開始 cronjob -
select {}
:- Go 語言用法,用於創建一個 infinite loop, 讓程式無限執行下去。
因此,當執行 go run main.go
時,這個 cronjob 就會固定於每分鐘將價格表上的所有商品價格調漲一塊了。
3. Conclusion
很好玩!之後可以看看怎麼結合 Render 的 job 功能,讓 cronjob 的運行腳本可以寫得更簡便、更著重於 service 本身就好。之後 Synoptic 也會因應世界上發生的大小事,再次用 Go 拯救公司營運(X