Featured image of post Use Go to develop a price increaser cronjob in a Javascript project

Use Go to develop a price increaser cronjob in a Javascript project

最近開始學 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 架構如下:

1
2
3
4
5
6
7
myapp/
├── go.mod
├── main.go
├── utils/
│   └── util.go
└── models/
    └── model.go

可知:

  1. go.mod 文件位於 myapp 目錄中,定義了 “example.com/myapp” 這個 module。
  2. main.go、utils/util.go 和 models/model.go 都屬於同一個 module “example.com/myapp”。
  3. utils 和 models 是 package,但它們不需要各自擁有 go.mod 文件。它們共享位於根目錄的 go.mod 文件。

File Structure

先 top-down 的展示這個小專案的檔案架構:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.
└── server/
    ├── cronjob/
    │   └── product-price-stealthy-increaser/
    │       ├── .env
    │       ├── go.mod
    │       ├── go.sum
    │       └── main.go
    ├── services/
    │   └── prices.js
    └── routes/
        └── priceRouter.js

Init a Go module

執行以下指令,init 我們的專案:

1
go mod init product-price-stealthy-increaser

這時便會產出 go.mod 檔如下:

1
2
3
module product-price-stealthy-increaser

go 1.22.3

在這個專案中,我們需要額外兩個 packages:

  1. godotenv: 用於讀取寫於 .env 的環境變數
  2. cron: 用於執行 cronjob

執行安裝指令如下:

1
2
go get -u github.com/joho/godotenv
go get -u github.com/robfig/cron/v3

此時安裝內容便會被記錄於 go.sum 檔案中。每個安裝的 packages 會有兩個 hash value, 分別確保直接下載與紀錄於 module 的依賴是否可以通過校驗。

-u flag 代表在安裝時,同時檢查是否有缺漏沒有安裝到的 dependencies,有的話就安裝。但他不會更新既有的 dependencies。

main.go

main.go 算是 module 的主要入口點。主要的檔案內容如下:

1
2
3
4
5
6
7
8
9
package main

import (
    ...
)

func main() {
    ...
}

在 main.go 中,我們需要兩個函數: incrementProductPrices(), 與 main()。

  • incrementProductPrices(): 負責與 db 互動,將每項商品的價格調漲一塊。
  • main(): 載入環境變數,並設定 incrementProductPrices() 每分鐘執行一次。

incrementProductPrices()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func incrementProductPrices() {
	nodeServerURL := os.Getenv("BACKEND_URL")

	resp, err := http.Post(nodeServerURL+"/increasePriceToFightInflation", "application/json", nil)
	if err != nil {
		log.Fatalf("Failed to increate price product: %v", err)
	}

	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
        log.Fatalf("Failed to read response body: %v", err)
    }

	if resp.StatusCode != http.StatusOK {
        log.Fatalf("Failed to update prices, status code: %d", resp.StatusCode)
    }
	maxBodyLength := 500
	bodyToPrint := string(body)
	if len(bodyToPrint) > maxBodyLength {
        bodyToPrint = bodyToPrint[:maxBodyLength] + "..."
    }
	log.Printf("Response Body: %v", bodyToPrint)
	log.Println("Product prices incremented successfully.")
}

這個函數主要有以下幾個重點:

  1. 這個 Go cronjob 會透過打 api 的方式,讓 PriceRepository (written in Javascript) 執行每個商品價格加ㄧ的動作。POST api 裡面連結到 js price service。這麼做有幾個優點:

    • 讓 Go cronjob 調用 Javascript repo 的資源。
    • 我們可以不用在 cronjob 中撰寫 db crud 相關指令,讓程式層級切割的更明確。
  2. defer response.Body.Close()

    • 這個寫法通常見於 HTTP request / response, 是 Go 語言中的一種用法,用於確保在 HTTP 請求完成後關閉response body,以釋放相關資源。
    • “defer” 確保了函數無論在什麼時候結束、如何結束,資源都可以被釋放。
    • 如果不寫 defer, 僅僅在最後加上 response.Body.Close(), 可能導致某些 error handling 出口忘記釋放資源,讓資源被洩漏。
  3. 使用 io.ReadAll() 讓 response 成為一個 list:

    • 用途:有時候 response 長度會太長,這時我們可以指定最長 logging 的長度,再把 response 讀成一個 list, 借此讓 response 可以好看一點。
  4. 使用 log 而非 fmt: 讓輸入的狀態擁有時間戳、格式化輸出選擇。

main()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func main() {
	if err := godotenv.Load(); err != nil {
		log.Fatalf("Error loading .env file: %v", err)
	}

	schedule := cron.New(cron.WithSeconds())

	_, err := schedule.AddFunc("0 * * * * *", incrementProductPrices)
	if err != nil {
		log.Fatalf("Error scheduling cron job: %v", err)
	}

	schedule.Start()

	select {}
}

這個函數主要有以下幾個重點:

  1. 載入環境變數,並在無法順利載入時 log.Fatalf()

    • 在 main() 而非 incrementProductPrices() 調用環境變數,可以確保調用的行為只會發生一次,降低不必要的開銷。
  2. cron.New(cron.WithSeconds())

    • 創建一個 cronjob, 並將精細度由預設的分鐘級調整為秒級
  3. schedule.AddFunc("0 * * * * *", incrementProductPrices):

    • 將 “incrementProductPrices” 添加至 cronjob中,並設定每小時的第 0 秒(=每分鐘)執行一次。
  4. schedule.Start(): 開始 cronjob

  5. select {}:

    • Go 語言用法,用於創建一個 infinite loop, 讓程式無限執行下去。

因此,當執行 go run main.go時,這個 cronjob 就會固定於每分鐘將價格表上的所有商品價格調漲一塊了。

3. Conclusion

很好玩!之後可以看看怎麼結合 Render 的 job 功能,讓 cronjob 的運行腳本可以寫得更簡便、更著重於 service 本身就好。之後 Synoptic 也會因應世界上發生的大小事,再次用 Go 拯救公司營運(X

Written By Elaine Hsieh, Taipei Taiwan
Built with Hugo
Theme Stack designed by Jimmy