一次核心模組的重構經驗

前言 在軟體開發的漫漫長路中,我們時常會接手一些充滿「歷史印記」的專案。這些專案的核心模組,往往因為業務的快速迭代與時間的無情沖刷,逐漸演化成難以觸碰的「史前巨獸」。近期,我便有幸(或許該說是不幸地)參與了一次這樣核心模組的重構之旅,其核心是我們產品線廣泛使用的 Golang gRPC 認證攔截器 (Interceptor)。這段經歷充滿挑戰,但也收穫良多,希望能藉此分享一些心得。 歷史的塵埃:核心模組的演進悲歌 我接手的這個核心認證模組,在專案初期或許設計簡潔明瞭,但隨著產品線的不斷擴展和需求的堆疊,其複雜度已然失控。追溯其演進的脈絡,彷彿能看到一部小型技術債的形成史。 最初的起點:單純的 gRPC Interceptor 可以想見,專案伊始,對於 gRPC 服務的認證需求相對單純。一個通用的攔截器或許就能滿足所有需求,程式碼結構清晰可見: package main func SimpleAuthInterceptor(...) { log.Println("Performing basic authentication via SimpleAuthInterceptor") return handler(ctx, req) } func SimpleStreamAuthInterceptor(...) error { log.Println("Performing basic stream authentication via SimpleStreamAuthInterceptor") return handler(srv, ss) } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer( grpc.UnaryInterceptor(SimpleAuthInterceptor), grpc.StreamInterceptor(SimpleStreamAuthInterceptor), ) log.Println("gRPC server listening on :50051") if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } } 在這個階段,一切看起來是那麼的美好與純粹。 ...

Golang Iterator 簡介與 samber/lo 比較

Golang Iterator 簡介與 samber/lo 比較

自從 Golang 1.18 版本引入泛型(Generics)後,Go 語言的生態系統迎來了許多令人興奮的變化。其中,Golang 1.23 版本對 Iterator(迭代器)的標準化,以及 iter 套件的加入,無疑是近期改動中相當重要的一環。本文將淺談 Golang Iterator 的基本概念,深入探討 Pure Iterator 與 Impure Iterator 之間的區別與設計考量,並與社群中流行的 samber/lo 工具庫進行比較。 什麼是 Iterator? Iterator Pattern(迭代器模式)是一種常見的設計模式,它提供了一種循序存取集合物件中各個元素的方法,而又無需暴露該物件的內部表示。簡單來說,Iterator 就像一個指針,可以依序指向集合中的下一個元素,直到遍歷完所有元素為止。 Golang 中的 Iterator 在 Golang 1.23 之前,我們通常透過 for-range 迴圈來迭代 array、slice、string、map、channel 等內建資料結構。然而,對於自訂的資料結構或複雜的序列生成邏輯,缺乏一個統一的迭代標準。 Golang 1.23 版本正式將 Iterator 標準化,並在標準庫中加入了 iter 套件。同時,slices 和 maps 套件也增加了一些回傳 Iterator 的工廠函數(Iterator Factories)。到了 Golang 1.24,更有如 strings.SplitSeq 等函數加入,進一步豐富了 Iterator 的應用場景。 // strings.SplitSeq 回傳一個迭代器,用於遍歷由 sep 分隔的 s 子字串。 // 此迭代器產生的字串與 Split(s, sep) 回傳的相同,但不會建構整個 slice。 // 它回傳一個單次使用的迭代器。 func SplitSeq(s, sep string) iter.Seq[string] 如果對 Golang 1.23+ 中 Iterator 的語法和語義還不熟悉,建議可以閱讀 Ian Lance Taylor 在 Go 官方部落格發表的介紹文章 ...

建構多平台的 container image

建構多平台的 container image

前言 最近在搞 side project 時,常常需要在不同的 CPU 架構上跑我的應用程式。例如,開發用的 MacBook 是 ARM 架構 (Apple Silicon),開發用的 Desktop 是 x86 架構(Ryzen 5900X),而部署的伺服器可能是 x86 (AMD64),有時候甚至想在 Raspberry Pi (ARM) 上跑些小東西。每次都要為不同平台分別建構 image 實在有點麻煩,而且 Registry 上一堆 xxxapp-amd64, xxxapp-arm64 的 tag 看了也很礙眼。經過一番研究與嘗試,是時候接觸 Docker Buildx 了。 為什麼需要多平台映像檔 在 wintel 的商業策略下,以及大家對高性能伺服器的普遍認知,主要用 x86/amd64,但現在 ARM 架構越來越普及,從 Apple Silicon 的 Mac、AWS Graviton 處理器、各種 IoT 設備到你的 Raspberry Pi,ARM 無所不在。如果你的 container image 只支援 amd64,那它就無法在這些 ARM 設備上原生運行 (需要模擬,效能差)。為了Build Once, Run Anywhere,多平台映像檔 (Multi-platform images) 就是 meta。 OCI 多平台映像檔架構簡述 其實不複雜。傳統的單一平台 image,它的 manifest 指向一組設定檔和一堆 layer。而多平台 image 則是透過一個 manifest list (索引) 指向多個特定平台的 manifest。每個特定平台的 manifest 才各自指向該平台的設定檔和 layer。 ...

使用 go work 在本地開發解決同時開發 module 的問題

在 Golang 1.18 中 go workspace 的提案釋出後,golang 的官方文件或多或少也提到應該要怎麼做 multi module 的開發。相較於過去需要不斷的替換 go.mod 內的 replace 指令,go work 大幅改善了 multi module 的開發體驗。 為什麼需要 go work 專案逐漸變大 當你在維護一個小工具 side project 時,單一的 module 就能夠滿足所有需求,但當專案逐漸變大,會需要將專案拆分成多個 module。 可以透過一個例子來理解: 假設我們有一個專案,拆分成以下兩個 module: common-lib-golang: 存放所有專案都會用到的 function,例如 retry, logger, tracer 等等 backend: 實際提供 http api 的程式碼 最開始的檔案架構為: . ├── backend │ ├── go.mod │ ├── go.sum │ └── main.go └── common-lib-golang ├── go.mod ├── go.sum └── util.go 隨著專案變大,我們在開發 backend 時,時常會需要修改 common-lib-golang 內的程式碼,這時候就需要 go work 來協助我們。 ...

在習慣 go mod 後重新學習 git submodule

前言 使用 Golang 作為主要開發已經有五年的時間。最近因工作需要接觸到 Python 專案,並且該專案使用 git submodule 的方式來引用共同函式庫 common-lib-python。對於長久使用 Golang 的 go mod 的我來說,submodule 是一個相對陌生的概念。藉這個機會撰寫一篇文章,整理並紀錄一下 git submodule 的用法,也作為未來的參考。 這篇文章會假設已經對 git 的基本操作有一定程度的了解,並著重在 submodule 的概念、使用情境以及與 Golang 的 go mod 的差異比較。文章內容會以下列流程來呈現: 建立一個新的 Python 專案 my-python-repo 建立一個 Python 模組 my-python-module 作為 submodule 在 my-python-repo 中使用 my-python-module 作為 submodule 模擬需求變更,同時修改 my-python-repo 與 my-python-module,並分別發送 PR 與 Golang 的 go mod 進行比較 建立 Python 專案與模組 先建立兩個新的 git repo,分別是 my-python-repo 與 my-python-module。 # 建立 my-python-repo mkdir my-python-repo cd my-python-repo git init touch main.py git add . git commit -m "Initial commit" # 建立 my-python-module cd .. mkdir my-python-module cd my-python-module git init touch my_module.py git add . git commit -m "Initial commit" # 將兩個專案 push 到 Github/Gitlab 上 my-python-repo 與 my-python-module 都已經是一個獨立的 git repo。 ...

Golang Composition over Inheritance

Golang Composition over Inheritance

Golang 是一門簡潔有力的程式語言,相較於其他程式語言,更傾向於使用組合(composition)而不是繼承(inheritance),語言設計之初更是沒有提供繼承的關鍵字,這種設計哲學讓 Golang 在現代軟體開發中脫穎而出。 繼承固然有其優點,但在建構複雜的物件關係時,容易產生過於龐大的繼承層級結構。這使得程式碼難以閱讀和維護,就像是一棵盤根錯節的大樹,牽一髮而動全身。 過深的繼承層級會導致知名的「脆弱基類問題」(fragile base class problem),使得程式碼難以修改和擴展。 組合則不同,它鼓勵建立小型、專注的 struct,然後像樂高積木一樣,將這些 struct 組合成更大的結構。這種方式讓程式碼模組化,更容易理解和修改。 彈性 Golang 的 type system 支援我們靈活地組合各種 struct。可以建立一個新的 struct,並在其中「嵌入」其他 struct 作為其欄位。 type Car struct { make string model string year int } type Driver struct { name string car Car } func main() { myCar := Car{"Toyota", "Camry", 2020} driver := Driver{"John", myCar} fmt.Println(driver.name) // 輸出: John fmt.Println(driver.car.make) // 輸出: Toyota } 在這個例子中,Driver 透過組合 Car 來建立更豐富的資料結構。Driver 「has-a」 Car,而不是 「is-a」 Car,這提供了更高的彈性,讓 Driver 可以更專注在自身的邏輯。 程式碼複用 組合促進了程式碼的複用。可以建立許多小型、可複用的 struct,然後將它們組合成各種不同的結構。 繼承也能實現程式碼複用,但它也可能導致不必要的耦合,使得程式碼難以修改,因為對父類別的修改可能會影響到所有子類別。 type Engine struct { power int fuelType string } type Wheels struct { count int material string } type Vehicle struct { engine Engine wheels Wheels brand string } func (v Vehicle) getBrand() string { return v.brand } func (v Vehicle) getEnginePower() int { return v.engine.power } 在這個例子中,Vehicle 透過組合 Engine 和 Wheels 來複用這兩個 struct 的欄位和功能。Vehicle 「has-a」 Engine and 「has-a」 Wheels,並可以新增自己的欄位和方法,例如 brand、getBrand() 和 getEnginePower()。這種方式讓 Vehicle 可以專注於自身的邏輯,同時又能複用 Engine 和 Wheels 的功能。 ...

Golang 1.22 中 http routing 的改進

Golang 作為一個偏向 server 應用的程式語言,一般的 web server 並不會直接使用原生的 package net/http,而更多的使用 gin-gonic/gin 或是 gorilla/mux,後來也有 labstack/echo 以及 go-chi/chi 等等選擇,在效能、輕量、好維護、好擴充中,都能找到對應的 third party package,其中的原因不外乎是原生的 package 提供的功能過於簡潔。 好在 1.22 中,官方改進了 net/http 中對於多工器、路由,甚至出了一篇部落格,現在更可以「大膽的」直接使用 standard library。 Path Parameter 若要將應用的 Web API 定義成 RESTful,我們會使用 /資源/{資源唯一識別符}/子資源/{子資源唯一識別符} 來定義路徑。假如要獲取一個使用者的訂單,則會使用 GET /users/1/orders 來獲取。在 1.22 以前,我們只能定義到 /users,再自行解析往後的 path: http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { subPath := strings.TrimPrefix(req.URL.Path, "/users/") if len(subPath) == 0 { xxx } else { ooo } ... }) 而在 1.22 中新增了 net.http 對 path parameter 的支持,我們可以直接使用 (*http.Request).PathValue("xxx") 來獲取: http.HandleFunc("/users/{user_id}", func(w http.ResponseWriter, r *http.Request) { userID := r.PathValue("user_id") ... }) 不過也帶來一些限制 ...

使用 TinyGo 與 Raspberry pi pico 實現溫濕度感測器

前言 作為嵌入式系統學習的微小專案,我決定使用 TinyGo 來實現一個簡單的溫濕度感測器。過去我大多使用 Arduino 作為微控制器(MCU)開發平台,但這次想嘗試使用 TinyGo 來進行單片機的學習。 TinyGo 是 Go 語言的一個子集,專門針對小型設備和微控制器進行了優化,使得我們可以在資源受限的硬體上運行 Go 程式。 全部的程式碼都可以到 omegaatt36/pico-bme280 中找到。 硬體選擇 Raspberry Pi Pico 我選擇了 Raspberry Pi Pico 作為本專案的主控板。Pico 是一款基於 RP2040 晶片的微控制器開發板,具有以下特點: 雙核 ARM Cortex-M0+ 處理器,時脈可達 133 MHz 264KB 的 SRAM 和 2MB 的板載閃存 支援 USB 1.1 主機和設備功能 低功耗睡眠和休眠模式 可編程 I/O(PIO)狀態機 30 個 GPIO 引腳 便宜 價格僅需要 5 美元,後繼款的 Pico 2 仍然維持 5 美元,還額外增加了 RISC-V 架構的支援,可以一次玩到兩種處理器架構。 Pico 的這些特性使其非常適合用於各種嵌入式專案,包括我們的溫濕度感測器。 BME280 感測器 最初我購買了 BMP280 感測器,但後來發現它只能測量溫度和氣壓,無法測量濕度。因此,我轉而選擇了 BME280 感測器,它可以同時測量溫度、濕度和氣壓。 BME280 是一款精確度高、體積小、待機功耗低的環境感測器。但當連續偵測時,通過的電流會導致 sensor 功耗上升,進而導致測量到過高的溫度。 ...

透過內建的 pprof 工具來分析 Golang 發生的 memory leak

前言 某天下午,公司的 cronjob daemon 無預警的被 GCP OOM Kill 了,且程式碼沒有看出明顯的原因。 根據過去的經驗,local 開發時會使用 go tool pprof 來分析 CPU profile 或是 memory 與 trace 的問題,詳細可以參考 Go 官方文件。 由於我們的程式碼是一個基於 gin 的 http service,因此可以使用 gin 提供的 pprof 來快速建立 endpoint。 gin pprof gin 的 pprof package 提供了數個基於 net/http/pprof 的 endpoint,可以分別為: /: 基本的 pprof 的 static page,可以分析 CPU 與 memory 的問題。 /cmdline: 分析 command line 的問題。 /profile: 分析 CPU profile 的問題,可以透過 query string 來指定 CPU profile 的 duration。 /symbol: 分析 symbol table 的問題。 /trace: 分析 trace 的問題。 /goroutine: 分析 goroutine 的問題,這也是本文中重點查看的 endpoint。 /heap: 分析 heap 的問題。 可以在 http server 中加入以下程式碼來啟用 pprof: ...

基於 Golang 的 Grafana Dashboard 與 JWT 認證的前後端實作

在工作上有一個需求是需要做一些 OLAP,原訂計畫是使用 Google Looker(ver. Google Cloud Core),礙於量小不符合經濟效益,決定用 Grafana 這個較熟悉的開源套件來幫助我們做視覺化的處理。 這篇文章的範例可以在 omegaatt36/grafana-embed-example 中找到所有的 source code 我的 Use Case 為已經有一組 SHA512 產生的 Key,以下的內容為使用 HS512 進行簽名與認證。 流程 sequenceDiagram autonumber participant U as User participant B as Browser participant I as iframe participant S as Server participant G as Grafana rect rgb(236,239,244) U->>B: Open Web Page B->>S: Request JWT Token S->>B: Return JWT Token B->>I: Load iframe I->>G: Request Dashboard with JWT Token G->>I: Return Dashboard I->>B: Display Dashboard in iframe B->>U: Show Dashboard end Grafana 配置 主要是針對 grafana.ini 做修改 ...