自動生成重複代碼:使用 Go 的 Template

開發軟體時,常常會發現有些函式或方法很類似,例如對 Callback Function 來說,開發者都需要註冊回調函式,並在適當的時機,將資料交給回調函式處理,我們可以將這兩個動作,稱為 OnAction 跟 EmitAction。儘管繼承或組合能讓程式碼重複使用類似的組件,幫助開發者節省時間,但對於較複雜的情況,像是不同類(Class)的函式名稱也要不同時,仍需要仰賴開發者自行編寫。

試著想想,如果開發者僅僅寫設定檔(Config File),就有程式能根據設定檔來自動產生程式碼,不是很美好的事嗎?這是有的,在實務上,這類用於產生程式的程式被稱為 Code Generator,例如前面介紹過的 genny。Golang 有內建 generate 這個命令行工具,能幫助開發者將 Code Generator 跟編譯更密切結合在一起。

本文會用 Callback 的 Generator 當例子,講解如何開發並使用一套 Code Generator。需要 Clone 程式碼的,可以到這裡

Design a Config File

我們的目標是設計一套程式,可以依照 Config 來產生 Callback Function,Callback Function 能根據 Config,而有不同的名字跟引數型別。對像 Golang 這類強型別又沒有泛型的語言來說,這是很實用的功能。

先看專案結構

.
├── Makefile
├── cmd
│   ├── codegen
│   │   └── main.go
│   └── example
├── config
│   └── callback.json
└── go.mod

codegen 內的 main.go 是主要程式碼,也就是 Code Generator;而 config 用於放置需要的設定檔。

Config 應該長怎樣呢?我們希望它是一個陣列,這樣就能將每個元素對應到不同的 Callback,元素應該是個物件,包含 Name 跟 Arg 兩個不同的鍵,如果用 JSON 格式來表達,它會長

[
    {
        "EventName": "Click",
        "CallbackArg": "int"
    },
    {
        "EventName": "Move",
        "CallbackArg": "uint32"
    }
]

EventName 是 Callback 的名稱;CallbackArg 是 Callback 的引數型別。

來看主程式的部分,在 main.go 中讀進 config,並 Parse 成 Struct

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "os"
    "strings"
    "text/template"
)

type schema struct {
    EventName   string
    CallbackArg string
}

func main() {
    var schemas []schema
    data, err := ioutil.ReadFile("../../config/callback.json")
    if err != nil {
        panic(err)
    }
    err = json.Unmarshal(data, &schemas)
    if err != nil {
        panic(err)
    }
}

先建立個 Struct 來對應 config

type schema struct {
    EventName   string
    CallbackArg string
}

再來讀檔案

data, err := ioutil.ReadFile("../../config/callback.json")
if err != nil {
    panic(err)
}

接著反序列化 JSON,放進 array 中

var schemas []schema
err = json.Unmarshal(data, &schemas)
if err != nil {
    panic(err)
}

完成對 config 的讀取。

Design Template

接著來設計 template,可以把它想像成是個餅乾模具,只要將麵團塞進模具放入烤箱烘烤,等待出爐後就是熱騰騰的餅乾。如果要換口味,也只需要調整麵團的配方,不需要動到模具。對照到程式碼,它可以看成是一段重複的程式碼原型,某些段落可以塞進變數,模板引擎會根據模板來渲染,達到客制化效果。

讓人驚奇的是,Golang 有內建 template,這再次讓人感受到 Golang 在應用開發上的優勢。

在原本的專案下,加入 template

.
├── Makefile
├── cmd
│   ├── codegen
│   │   └── main.go
│   └── example
├── config
│   └── callback.json
├── go.mod
└── tmpl
    ├── callbackTemplate.tmpl
    ├── contextTemplate.tmpl
    └── main.tmpl

main.tmpl 用於產生主程式;contextTemplate.tmpl 用以產生一個 Struct,內有需要的 callback 函式;callbackTemplate.tmpl 用來產生 callback 的註冊及調用。

先來看要怎麼調用模板引擎,修改 main.go

t := template.Must(template.New("main.tmpl").ParseFiles("../../tmpl/main.tmpl"))
t = template.Must(t.ParseFiles("../../tmpl/callbackTemplate.tmpl"))
t = template.Must(t.ParseFiles("../../tmpl/contextTemplate.tmpl"))
err = t.Execute(os.Stdout, schemas)
if err != nil {
    fmt.Println(err)
}

使用 text/template 函式庫,在 ParseFiles 由檔案引入模板,然後用 Execute 來渲染模板,輸出的結果先導到 stdout,顯示於終端機畫面,如果開發者有需要,可以將它再重新導向到檔案中。

要注意 Execute 的第二個引數是 schemas ,這是由 config 中讀出的數據,也是要傳給模板的值。

來看看模板的內容,main.tmpl 是

package main

{{ template "contextTemplate.tmpl" . }}
{{ template "callbackTemplate.tmpl" . }}

用 {{ action }} 框起來的是模板的 action,可以當成是模板語法,{{ template }} 意思是引入其他模板,在 main.tmpl 引入其他兩個模板; . 是調用程式傳入的參數。

main.tmpl 內的 contextTemplate.tmpl 是另一個子模板,用於產生 struct

// Context can be used to callback
type Context struct {
{{- range . }}
    {{ .EventName }}Callback func({{ .CallbackArg }})
{{- end }}
}

其中 range 是模板語言的迴圈,類似 Golang 的 for range; - 是省略 {{}} 前的空白,避免 action 干擾到模板。.EventName 跟 .CallbackArg 是傳入結構底下的欄位,也就是 config 中 EventName 跟 CallbackArg 的值。

經過 contextTemplate.tmpl 後,預期可以生成

// Context can be used to callback
type Context struct {
    ClickCallback func(int)
}

Click 跟 int 都是依照傳入參數而建立。

main.tmpl 內的另一個子模板 callbackTemplate.tmpl 用於產生 function

{{- range . }}
// On{{ .EventName }} register a callback function
func (c *Context) On{{ .EventName }}(callback func(arg {{ .CallbackArg }})) {
    c.{{ .EventName }}Callback = callback
}

// Emit{{ .EventName }} emit a callback event
func (c *Context) Emit{{ .EventName }}(arg {{ .CallbackArg }}) {
    c.{{ .EventName }}Callback(arg)
}
{{ end }}

經過 callbackTemplate.tmpl 後,可以生成

// OnClick register a callback function
func (c *Context) OnClick(callback func(arg int)) {
    c.clickCallback = callback
}

// EmitClick emit a callback event
func (c *Context) EmitClick(arg int) {
    c.clickCallback(arg)
}

如此一來,就完成模板的設計了。

Add Template Action

儘管 Golang 模板有內建很多 action,但還是可能找不到想要的功能,例如,在 Golang 語法中,Struct Field 的名字首字母如果是大寫,意謂該 Field 是 Public,但另一方面,開發者又會希望 Function Name 要 Follow Camal Method。對應到需求是,如果 EventName 有時能大寫,有時能小寫,那就兩全其美了。

這時就是自定義 action 派上用場的時機了,要加入自定義 action,可以回去修改 main.go,加入

funcLowerCase := template.FuncMap{"lower": strings.ToLower}
t := template.Must(template.New("main.tmpl").Funcs(funcLowerCase).ParseFiles("../../tmpl/main.tmpl"))

用 FuncMap 建立一個鍵值對,key 是 action 的名字,value 是 action 的執行內容,這邊使用 strings 下的 ToLower 幫忙做大小寫轉換。

建立後的 func 可以用 Funcs 帶進模板中。

再來修改模板 contextTemplate.tmpl

{{ .EventName | lower }}Callback func({{ .CallbackArg }})

| 是 pipeline,可以將前項的輸出跟後項的輸入用管道連接起來,放在這邊,意思是將 EventName 傳給 lower,而 lower 正是剛剛建立的模板 action。

於是渲染效果變成

// Context can be used to callback
type Context struct {
    clickCallback func(int)
}

處理好 scope 的問題了。

Try It

開發完成後,來看看如何使用。

一開始,先用 go run 來驗證程式是否正確

ken@DESKTOP-2R08VK6:~/git/medium-example-golang/codegen/cmd/codegen$ go run main.go

得到

package main

// Context can be used to callback
type Context struct {
        clickCallback func(int)
        moveCallback func(uint32)
}

// OnClick register a callback function
func (c *Context) OnClick(callback func(arg int)) {
        c.clickCallback = callback
}
...

內容正確!將輸出導出到 context.go 的檔案中

go run main.go > ./context.go

在專案中新增使用的程式

.
├── Makefile
├── cmd
│   ├── codegen
│   │   └── main.go
│   └── example
│       └── main.go
├── config
│   └── callback.json
├── go.mod
└── tmpl
    ├── callbackTemplate.tmpl
    ├── contextTemplate.tmpl
    └── main.tmpl

example/main.go 的內容是

package main

import "fmt"

func main() {
    printNum := func(num int) {
        fmt.Println(num)
    }
    context := &Context{}
    context.OnClick(printNum)
    context.EmitClick(5)
}

使用 Context,並將 Println 註冊為 OnClick 的 callback,使用 EmitClick 將資料發送給 callback,OnClick 收到後就會將資料印出。

在本例中,發送的資料是 5

接著在檔案中加入 go:generate,讓 go generate 可以執行註解內容,產生 context.go

//go:generate bash -c "go run ../codegen/main.go > ./context.go"

go generate ./...

context.go 就被生出來了。

接著編譯並執行 example

ken@DESKTOP-2R08VK6:~/git/medium-example-golang/codegen$ ./bin/example 5

成功印出 5。

小結

Go 命令行工具的 generate 原本是為了方便整合外部工具,讓 Golang 編譯更順利,搭配 template 使用後,變成 Code Generator 的利器。

對於希望簡化程式碼,讓程式碼更具彈性,只要修改 config 即可完成擴充的人來說,Code Generator 會是個不錯的工具。實際上,以 Golang 目前沒有泛型的狀況來看,Code Generator 應該是一條開發捷徑;要注意的是,Code Generator 的用途是面向開發者,如果今天目的是要讓其他人容易使用,應該也要試試看用 Reflect 開發。

Reference

Read more

OAuth 2.0 的身份認證:OpenID Connect

OAuth 2.0 的身份認證:OpenID Connect

OAuth 2 讓網路服務可以存取第三方的受保護資源,因此,有些開發者會進一步利用 OAuth 2 來進行使用者認證。但這中間存在著一些語義落差,因為 OAuth 2 當初設計目的是「授權」而不是「認證」,兩者關注的焦點會有些不同。OpenID Connect 是基於 OAuth 2 的一套身份認證協定,讓開發者可以在 OAuth 2 授權的基礎上,再加入標準的認證流程。在這篇文章中,我會說明授權跟認證的場景有何差異,並講解 OpenID Connect 如何滿足認證需求。 因為 OpenID Connect 是建構在 OAuth 2 的基礎上,我會假設這篇文章的讀者已經知道 OAuth 2 的組件與流程,如果你不熟悉,可以先閱讀另外兩篇文章 * OAuth 2.0:

By Ken Chen
更好的選擇?用 JWT 取代 Session 的風險

更好的選擇?用 JWT 取代 Session 的風險

因為 HTTP 是無狀態協定,為了保持使用者狀態,需要後端實作 Session 管理機制。在早期方式中,使用者狀態會跟 HTTP 的 Cookie 綁定,等到有需要的時候,例如驗證身份,就能使用 Cookie 內的資訊搭配後端 Session 來進行。但自從 JWT 出現後,使用者資訊可以編碼在 JWT 內,也開始有人用它來管理使用者身份。前些日子跟公司的資安團隊討論,發現 JWT 用來管理身份認證會有些風險。在這篇文章中,我會比較原本的 Session 管理跟 JWT 的差異,並說明可能的風險所在。 Session 管理 Session 是什麼意思?為什麼需要管理?我們可以從 HTTP 無狀態的特性聊起。所謂的無狀態,翻譯成白話,就是後面請求不會受前面請求的影響。想像現在有個朋友跟你借錢,

By Ken Chen

Goroutine 的併發治理:掌握生命週期

從併發的角度來看,Goroutine 跟 Thread 的概念很類似,都是將任務交給一個執行單元來處理。然而不同的是,Goroutine 將調度放在用戶態,因此更加輕量,也能避免多餘的 Context Switch。我們可以說,Go 的併發處理是由語言原生支援,有著更好的開發者體驗,但也因此更容易忘記底層仍存在著輕量成本,當這些成本積沙成塔,就會造成 Out of Memory。這篇文章會從 Goroutine 的生命週期切入,試著說明在併發的情境中,應該如何保持 Goroutine 的正常運作。 因為這篇講的內容會比較底層,如果對應用情境不熟的人,建議先看過同系列 * Goroutine 的併發治理:由錯誤處理談起 * Goroutine 的併發治理:值是怎麼傳遞? * Goroutine 的併發治理:管理 Worker Pool 再回來看這篇,應該會更容易理解。 Goroutine 的資源使用量 讓我們看個最簡單的例子,假設現在同時開

By Ken Chen

Goroutine 的併發治理:管理 Worker Pool

併發會需要多個 Goroutine 來同時執行任務,Goroutine 雖然輕量,也還是有配置成本,如果每次新的任務進來,都需要重新建立並配置 Goroutine,一方面不容易管理 Goroutine 的記憶體,一方面也會消耗 CPU 的運算效能。這時 Worker Pool 就登場了,我們可以在執行前,先將 Goroutine 配置好放到資源池中,要用時再調用閒置資源來處理,藉此資源回收重複利用。這篇文章會從 0 開始建立 Work Pool,試著丟進不同的場景需求,看看如何實現。 基本的 Worker Pool Worker Pool 的概念可以用這張圖來解釋 Job 會放在 Queue 中送給 Pool 內配置好的 Worker,Worker 處理完後再將結果送到另一個 Queue 內。因為這是很常見的併發模式,

By Ken Chen