# Week 6: 5GC development - 1
## 課程目標
- Concurrent programming in Go
- Design Pattern
- NF 專案架構與相關套件 [free5GC Upgrade R17 Suggestion Step](https://hackmd.io/@free5gc-dev/B1HzZ7pBkx)
## Concurrent programming
A goroutine is a lightweight thread managed by the Go runtime.
```go=
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
```

> *M-P-G Model,圖片來源:https://alanzhan.dev/post/2022-01-24-golang-goroutine/*
- **G**oroutine:表示 goroutine,每個 goroutine 都有自已的 stack 與定時器,初始化時 stack 大小在 2KB 左右,其大小是會被 golang runtime 自動縮放的。
- **M**achine:代表 kernel thread,紀錄 thread 對應的 stack 資訊。當 goroutine 被調度到 machine 時會使用 goroutine 本身的 stack。
- **P**rocessor:表示調度器,負責調度 goroutine ,維護一個 local goroutine queue,並且將 queue 與 M 綁定,使 M 能夠從 P 上獲得 goroutine 並執行,同時還負責部分記憶體管理機制。
從 M-P-G 模型可以了解,基本上 golang runtime 就是一個排程器,它為使用者建立的 goroutine 分配背後的 memory 以及 cpu 資源。
同時,golang runtime 還實作了 sleep 機制,避免資源上的浪費(這部分我們在討論 mutex lock 時再談)。
:::info
💡補充:
- **GOMAXPROCS** 預設是 CPU 的 CORE 數,這樣的設計讓 golang runtime 盡可能地使用機器上的 CPU 資源。
- Golang 的優勢之一是其內建易於使用的 goroutine,然而,如果你的應用程式部署在 Kubernetes 平台,其實有可能會因為 CPU limit 與 GOMAXPROCS 的設定,導致應用程式因為錯誤的 GOMAXPROCS,在 runtime 嘗試 GC 時觸發系統的限流。 -- [ref](https://go.dev/blog/container-aware-gomaxprocs)
- 閥值 = 上次 GC 記憶體分配量 * 記憶體增長量。 -- [ref](https://tip.golang.org/doc/gc-guide)
- 看記憶體增長量觸發 GC:由變數 GOGC 控制,預設值為 100,即每當記憶體使用量擴大一倍(100%)時啟動 GC。
- 默認情況下,每兩分鐘觸發一次 GC。
- 使用 `runtime.GC()` 也能觸發 GC
:::
### Mutex

> *Critical Section 示意圖,圖片來源:https://medium.com/@punyatoya213/multithreading-can-be-fun-too-part-2-e27f1841c8ca*
Mutex Lock 是常見用來處理 synchronization 的手段之一,Mutext Lock 有以下特性:
- 解鎖的人必須是上鎖的人
- 排隊等鎖的人會進入休眠
```go=
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
c.v[key]++
c.mu.Unlock()
}
// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
```
休眠這件事情就需要仰賴系統的支援:
- 如果在 RTOS 上,那就是由 RTOS 來掌管 Thread 的狀態
- 對於 Golang 來說,使用原生的 Mutext 上鎖的話,其餘等鎖的 goroutine 就會由 Golang runtime 將其放入下圖的 sudog(pseudo-g)之中

> *M-P-G Model Overview,圖片來源:https://alanzhan.dev/post/2022-01-24-golang-goroutine/*
:::info
💡補充:
- Golang 沒有提供 SpinLock,如果需要,可使用 atomic operation 實作出類似的效果。
- Golang 提供 `sync.RWMutex`,可以將讀操作與寫操作分開對待,進一步提升效能。
:::
### Inter-communication methods
#### Unbuffered channel
:::spoiler
以下內容取自 [A Tour of Go](https://go.dev/tour/concurrency/2):
Channels are a typed conduit through which you can send and receive values with the channel operator, <-.
```go=
ch <- v // Send v to channel ch.
v := <-ch // Receive from ch, and
// assign value to v.
```
Like maps and slices, channels must be created before use:
```go=
ch := make(chan int)
```
By default, sends and receives block until the other side is ready. This allows goroutines to synchronize without explicit locks or condition variables.
:::
#### Buffered channel
A Tour of Go 對 Unbuffered channel 的形容是這樣的:
> By default, sends and receives block until the other side is ready. This allows goroutines to synchronize without explicit locks or condition variables.
這意味著,在高度並行的情況下,goroutine 之間使用 Unbuffered channel 會出現阻塞。如果希望 sender 不要因為等待 receiver,那麼我們可以在宣告時為 channel 多分配一些空間:
```go=
ch := make(chan int, <SIZE>)
```
#### Semaphore
Unbuffer channel 會阻斷 sender 或是 receiver 的執行,所以它一般會被用在流程控制,而不是訊息的傳遞。
實際上,我們也可以用 unbuffered channel 實作出 semaphore:

> *Semaphore 示意圖,圖片來源:https://medium.com/happytech/limiting-goroutines-with-semaphore-b8ccc248e5f0*
```go=
package semaphore
import (
"context"
"time"
)
type Semaphore struct {
sem chan struct{} // use unbuffered channel as semaphore
}
func New(size int) *Semaphore {
return &Semaphore{
sem: make(chan struct{}, size),
}
}
func (s *Semaphore) Acquire(ctx context.Context) error {
select {
case s.sem <- struct{}{}:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (s *Semaphore) Release() {
<-s.sem
}
```
### Workflow control
如果一個應用程式會啟動多個 goroutine,如何保證當系統收到 SIGNAL 時,能夠等待所有 goroutine 都執行完畢才結束 process 的生命週期呢?
#### WaitGroup
Golang 實作了 WaitGroup,用於保證 goroutine 的結束順序:
```go=
package main
import (
"fmt"
"sync"
"time"
)
// worker simulates a task performed by a goroutine.
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Decrement the WaitGroup counter when the goroutine finishes.
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second) // Simulate some work
fmt.Printf("Worker %d finished\n", id)
}
func main() {
var wg sync.WaitGroup // Declare a WaitGroup.
// Launch several goroutines.
for i := 1; i <= 3; i++ {
wg.Add(1) // Increment the WaitGroup counter for each goroutine.
go worker(i, &wg) // Pass a pointer to the WaitGroup to the goroutine.
}
wg.Wait() // Block until all goroutines have called wg.Done().
fmt.Println("All workers finished")
}
```
#### Context
參考 [[開發技巧]在 Golang 中使用 context](https://medium.com/@ianchen0119/%E9%96%8B%E7%99%BC%E6%8A%80%E5%B7%A7-%E5%9C%A8-golang-%E4%B8%AD%E4%BD%BF%E7%94%A8-context-6aa1e3630ce0)。
#### Graceful shutdown
對於一個任意的網路應用程式,在應用程式收到 SIGTERM 以後都應該在寬限期(Graceful Period)內處理完已經接受的客戶端請求,以避免服務品質不佳。

> *Graceful shutdown in k8s,圖片來源:https://learnk8s.io/graceful-shutdown*
假設一個使用 Golang 開發的網路程式,它將接收封包與處理封包的部分拆開來,使用 goroutine 實現並行化處理,那麼該程式至少會有:
- 執行 `main()` 的 goroutine
- 處理封包接收的 goroutine(s)
- 處理封包的 goroutine(s)
當應用程式進入寬限期時,執行 `main()` 的 goroutine 負責發送 cancel 訊號,並且等待其他 goroutine 完成任務:
```go=
// handle SIGTERM
cancel() // 發送 cancel 訊號,其他 goroutine 會從 ctx.Done() 收到該事件
wg.Wait() // 等待 n 個 goroutine 執行 wg.Done()
log.Println("graceful shutdown")
```
其他 goroutine:
```go=
for {
select {
case <- ctx.Done():
for _, req := range reqCh {
handleReq(req)
}
wg.Done()
return
case req := <- reqCh:
handleReq(req)
// ...
}
}
```
## Design Patterns for Concurrency
### Single Threaded Execution Pattern
確保 critical section 在任意時間至多只會有一個 thread 執行:
```go=
package main
import (
"fmt"
"sync"
"time"
)
var (
counter int
mutex sync.Mutex
)
func increment() {
mutex.Lock() // Acquire the lock
defer mutex.Unlock() // Release the lock when done
counter++
fmt.Println("Counter:", counter)
}
func main() {
for i := 0; i < 5; i++ {
go increment()
}
time.Sleep(time.Second) // Allow goroutines to complete
}
```
### Read-Write-Lock Pattern
多讀單寫:
- 讀操作不會影響結果,所以多個讀操作可以同時進行
- 寫操作會影響結果,所以同時只能有一個寫操作
```go=
package main
import (
"fmt"
"sync"
"time"
)
var (
counter int
mutex sync.RWMutex
)
func increment() {
mutex.Lock() // Acquire the lock
defer mutex.Unlock() // Release the lock when done
counter++
}
func read() int {
mutex.RLock() // Acquire the read lock
defer mutex.RUnlock() // Release the read lock when done
fmt.Println("Counter:", counter)
}
func main() {
for i := 0; i < 5; i++ {
go increment()
go read()
}
time.Sleep(time.Second) // Allow goroutines to complete
}
```
### 一個案例看多種 design pattern
```go=
// ...
func newWorker(
// ...
) {
for {
select {
case <- ctx.Done():
for req := range reqChan { // Two-phase Termination Pattern
handleReq(req)
}
wg.Done()
return
case req := <- reqChan // Guarded Suspension Pattern
handleReq(req)
}
}
}
func main() {
var wg sync.WaitGroup
signalChannel := make(chan os.Signal, 2)
signal.Notify(signalChannel, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
ctx, cancel := context.WithCancel(context.Background())
reqChan := make(chan Req{}, 20)
// worker pool pattern
for i := 0; i < 5; i++ { // consumer
wg.Add(1)
go newWorker(ctx, reqChan, wg)
}
wg.Add(1)
go func(){ // producer
for {
select {
case: <- ctx.Done():
wg.Done()
return
default: // !!!IMPORTANT!!!
}
// prepare net connection
reqChan <- req
}
}()
<- signalChannel
cancel()
wg.Wait()
}
```
- Guarded Suspension Pattern:滿足先決條件,thread 才能向下執行。
- Two-phase Termination Pattern:先結束程式邏輯,再結束 goroutine。
- Producer-Consumer Pattern:將生產者與消費者分開執行。
- Worker Pool Pattern:使用 Worker Pool 避免 goroutine 濫開。
## Creational Design Pattern
專注於高效的管理物件
### Simple Factory Pattern
Simple Factory Pattern 是一種管理物件創建的模式,隨著輸入的參數不同,Factory 會提供不同的物件,使用者取得物件的時候只要在意傳入的參數,不需要去理解物件本身:
```go=
package main
import "fmt"
// Product Interface
type Animal interface {
Speak() string
}
// Concrete Product: Dog
type Dog struct{}
func (d *Dog) Speak() string {
return "Woof!"
}
// Concrete Product: Cat
type Cat struct{}
func (c *Cat) Speak() string {
return "Meow!"
}
// Simple Factory
func NewAnimal(animalType string) Animal {
switch animalType {
case "dog":
return &Dog{}
case "cat":
return &Cat{}
default:
return nil // Or handle error
}
}
func main() {
// Client interacts with the factory to get animals
dog := NewAnimal("dog")
if dog != nil {
fmt.Println("Dog says:", dog.Speak())
}
cat := NewAnimal("cat")
if cat != nil {
fmt.Println("Cat says:", cat.Speak())
}
unknown := NewAnimal("bird")
if unknown == nil {
fmt.Println("Unknown animal type.")
}
}
```
### Factory Method Pattern
將複雜的生產邏輯再拆分至特定工廠,由使用者來決定使用哪個工廠生產產品:
```go=
package main
import "fmt"
// Product Interface
type Vehicle interface {
Drive()
}
// Concrete Products
type Car struct{}
func (c *Car) Drive() {
fmt.Println("Driving a car.")
}
type Truck struct{}
func (t *Truck) Drive() {
fmt.Println("Driving a truck.")
}
// Creator Interface (Factory Method)
type VehicleFactory interface {
CreateVehicle() Vehicle
}
// Concrete Creators
type CarFactory struct{}
func (cf *CarFactory) CreateVehicle() Vehicle {
return &Car{}
}
type TruckFactory struct{}
func (tf *TruckFactory) CreateVehicle() Vehicle {
return &Truck{}
}
func main() {
var carFactory VehicleFactory = &CarFactory{}
car := carFactory.CreateVehicle()
car.Drive()
var truckFactory VehicleFactory = &TruckFactory{}
truck := truckFactory.CreateVehicle()
truck.Drive()
}
```
### Abstract Factory Pattern
使用者不需要知道具體要建立哪個產品,只要使用「工廠介面」即可產生對應的一整組物件。
假設我們要開發一個跨平台的應用程式,
會需要根據作業系統(Windows / MacOS)產生不同風格的按鈕(Button)與核取方塊(Checkbox)。我們希望做到:
- 當在 Mac 上時 → 產生 Mac 風格的按鈕與核取方塊
- 當在 Windows 上時 → 產生 Windows 風格的按鈕與核取方塊
- 「客戶端程式」不需要知道這些差異
#### 1. 定義抽象產品介面(Abstract Products)
```go=
type Button interface {
Render()
}
type Checkbox interface {
Check()
}
```
#### 2. 實作具體產品(Concrete Products)
```go=
import "fmt"
// MacOS 實作
type MacButton struct{}
func (b *MacButton) Render() {
fmt.Println("渲染 MacOS 風格的按鈕")
}
type MacCheckbox struct{}
func (c *MacCheckbox) Check() {
fmt.Println("勾選 MacOS 風格的核取方塊")
}
// Windows 實作
type WinButton struct{}
func (b *WinButton) Render() {
fmt.Println("渲染 Windows 風格的按鈕")
}
type WinCheckbox struct{}
func (c *WinCheckbox) Check() {
fmt.Println("勾選 Windows 風格的核取方塊")
}
```
#### 3. 定義抽象工廠介面(Abstract Factory)
```go=
type GUIFactory interface {
CreateButton() Button
CreateCheckbox() Checkbox
}
```
#### 4. 實作具體工廠(Concrete Factories)
```go=
type MacFactory struct{}
func (f *MacFactory) CreateButton() Button {
return &MacButton{}
}
func (f *MacFactory) CreateCheckbox() Checkbox {
return &MacCheckbox{}
}
type WinFactory struct{}
func (f *WinFactory) CreateButton() Button {
return &WinButton{}
}
func (f *WinFactory) CreateCheckbox() Checkbox {
return &WinCheckbox{}
}
```
#### 5. 客戶端程式(Client Code)
```go=
func renderUI(factory GUIFactory) {
button := factory.CreateButton()
checkbox := factory.CreateCheckbox()
button.Render()
checkbox.Check()
}
func main() {
var factory GUIFactory
osType := "mac" // 模擬偵測到系統為 macOS
if osType == "mac" {
factory = &MacFactory{}
} else {
factory = &WinFactory{}
}
renderUI(factory)
}
```
### Singleton Pattern
確保整個程式在執行期間,某個類別只會被建立一個實例(instance),並且提供一個全域存取點(global access point) 讓其他地方能取得該實例。
```go=
package main
import (
"fmt"
"sync"
)
// Config 是我們要共用的單例物件
type Config struct {
AppName string
Version string
}
var (
instance *Config
once sync.Once // 確保只初始化一次
)
// GetConfig 回傳唯一的 Config 實例
func GetConfig() *Config {
once.Do(func() {
fmt.Println("🔧 初始化 Config 實例...")
instance = &Config{
AppName: "Gthulhu",
Version: "1.0.0",
}
})
return instance
}
func main() {
// 模擬多次呼叫,但只會建立一次
cfg1 := GetConfig()
cfg2 := GetConfig()
fmt.Println(cfg1.AppName, cfg1.Version)
// 驗證兩者是同一個實例
if cfg1 == cfg2 {
fmt.Println("✅ cfg1 和 cfg2 是同一個實例")
} else {
fmt.Println("❌ cfg1 和 cfg2 是不同實例")
}
}
```
### Prototype Pattern
透過複製(clone)現有物件,而不是直接建立新的物件,來產生新的實例,避免重複初始化或複雜的建立流程。
#### 1. 定義 Prototype 介面
```go=
package main
import "fmt"
// Prototype 介面:定義 Clone() 方法
type Prototype interface {
Clone() Prototype
GetInfo()
}
```
#### 2. 定義具體原型(Concrete Prototype)
```go=
type Character struct {
Name string
Class string
Attack int
Defense int
}
func (c *Character) Clone() Prototype {
// 這裡做淺層複製(Go 的 struct 是值型別,可直接複製)
clone := *c
return &clone
}
func (c *Character) GetInfo() {
fmt.Printf("角色名稱: %s | 職業: %s | 攻擊: %d | 防禦: %d\n",
c.Name, c.Class, c.Attack, c.Defense)
}
```
#### 3. 範例
```go=
func main() {
// 建立原型角色
baseKnight := &Character{
Name: "Arthur",
Class: "Knight",
Attack: 80,
Defense: 100,
}
// 使用原型複製出新角色
knight2 := baseKnight.Clone().(*Character)
knight2.Name = "Lancelot" // 修改部分屬性
knight3 := baseKnight.Clone().(*Character)
knight3.Name = "Gawain"
knight3.Attack = 90
// 顯示結果
baseKnight.GetInfo()
knight2.GetInfo()
knight3.GetInfo()
}
```
## Structural Design Patterns
專注於低耦合物件的設計
### Adapter Pattern
透過定義統一介面的方式,讓多種物件可以用同樣的方式管理。
舉例:UPF 的 Data Plane 可以用多種方式實作出來,假設我希望實作一個支持多種 Data Plane 實作的 UPF,我可以套用 Adapter Pattern!
```go=
type Driver interface {
Close()
CreatePDR(uint64, *ie.IE) error
UpdatePDR(uint64, *ie.IE) error
RemovePDR(uint64, *ie.IE) error
CreateFAR(uint64, *ie.IE) error
UpdateFAR(uint64, *ie.IE) error
RemoveFAR(uint64, *ie.IE) error
CreateQER(uint64, *ie.IE) error
UpdateQER(uint64, *ie.IE) error
RemoveQER(uint64, *ie.IE) error
CreateURR(uint64, *ie.IE) error
UpdateURR(uint64, *ie.IE) ([]report.USAReport, error)
RemoveURR(uint64, *ie.IE) ([]report.USAReport, error)
QueryURR(uint64, uint32) ([]report.USAReport, error)
CreateBAR(uint64, *ie.IE) error
UpdateBAR(uint64, *ie.IE) error
RemoveBAR(uint64, *ie.IE) error
HandleReport(report.Handler)
}
```
- Related PR: https://github.com/free5gc/go-upf/compare/main...feat/dummy
- 單看程式碼會有點像是 Simple Factory Pattern,我認為 Adapter Pattern 關注的是物件的統一方法,而 Simple Factory Pattern 則是專注在取得物件上。
### Decorator Pattern
用包裝(wrapping)取代繼承(inheritance)。
假設我們有一個簡單的訊息系統:
原本的功能是:傳送純文字訊息。
現在想動態增加功能:
- 可以加上「加密」
- 可以加上「壓縮」
- 可以組合使用(例如先壓縮再加密)
這時候「Decorator Pattern」就派上用場了:
#### 1. 定義介面
```go=
package main
import "fmt"
// Component 介面:所有訊息傳送者都要實作 Send()
type Sender interface {
Send(msg string)
}
```
#### 2. 具體實作
```go=
// BasicSender 是最原始的訊息傳送者
type BasicSender struct{}
func (b *BasicSender) Send(msg string) {
fmt.Println("傳送訊息:", msg)
}
```
#### 3. 定義 Decorator
```go=
// BaseDecorator 內部包含一個 Sender,讓子類別能包裝其他 Sender
type BaseDecorator struct {
wrapped Sender
}
func (d *BaseDecorator) Send(msg string) {
if d.wrapped != nil {
d.wrapped.Send(msg)
}
}
```
#### 4. 實作 Decorator
```go=
// EncryptionDecorator:為訊息加密
type EncryptionDecorator struct {
BaseDecorator
}
func (e *EncryptionDecorator) Send(msg string) {
encrypted := "[加密]" + msg + "[/加密]"
fmt.Println("加密處理中...")
e.wrapped.Send(encrypted)
}
// CompressionDecorator:為訊息壓縮
type CompressionDecorator struct {
BaseDecorator
}
func (c *CompressionDecorator) Send(msg string) {
compressed := "[壓縮]" + msg + "[/壓縮]"
fmt.Println("壓縮處理中...")
c.wrapped.Send(compressed)
}
```
#### 5. 使用範例
```go=
func main() {
// 建立基本 sender
var sender Sender = &BasicSender{}
// 加上壓縮功能
sender = &CompressionDecorator{BaseDecorator{sender}}
// 再加上加密功能
sender = &EncryptionDecorator{BaseDecorator{sender}}
// 傳送訊息(順序:先壓縮,再加密)
sender.Send("Hello Decorator Pattern")
}
```
:::info
文章推薦:https://blog.messfar.com/page/design-pattern/
:::