Go 命令行工具初體驗:使用標準包開發

最初是在社群活動時接觸到 Golang,前陣子換工作後,新的產品團隊也是使用 Golang 來開發產品,在接觸新語言時覺得有些地方很有意思,好像能看到某種程式語言的變遷,或者說開發方向之類?跟常見的物件導向語言不同,Golang 不強調物件,而且帶有一些函式編程的特色,如果說 C++ 是替 C 補上物件導向的環節,那 Golang 更像是 C 語言的現代版。

本篇會簡單介紹如何使用 Golang 來開發一個簡單的命令行工具。我們可以假設一個微服務開發的情境,開發者需要頻繁在開發環境中啟動或關閉微服務,這時它會需要一個工具,能依照需求啟動各個微服務,通常在 Linux,我們會使用 shell script 來做這件事;如果是 Windows,則會使用 power shell 或 batch file;更正式的生產環境,可能會採用容器調度工具。Golang 由於具有跨平台的特性,也可以用在這個情境中。

Install Golang

開發前,當然要先安裝囉,對於 Windows 的使用者,會建議使用 chocolatey 來安裝

choco install golang -y

安裝好後確認版本

go version

得到

go version go1.14 windows/amd64

由於 Golang 是發展快速的語言,對版本要特別注意,可能前個版本的功能或環境,到下個版本就不同了。

然後,建議開發者的專案目錄可以長這樣

project
├── cmd  # main applications for this project.
|   └── main.go
├── pkg  # code that's ok to use by external applications
|   ├── module 1
|   |   └── module.go
|   └── module 2
└── README.md

cmd 用來放主要的應用程式,pkg 用來放相關的 lib,更細部的 layout 可以參考相關連結。因為我們的程式不會很大,這邊先用到 cmd 就可以了。

Hello World

所有語言的入門款就是 Hello, World,在 cmd/main.go 下加入程式碼,印出第一行文字

package main

import "fmt"

func main() {
    fmt.Println("Hello, world")
}

package main 是這個 go file 所屬的 module,對於所有的 go file 而言,都會有專屬的模組,方便進行引用;import 則是引用其他的模組,fmt 是 format 的縮寫,是 Go 的標準庫,用來做一些格式化的輸入輸出,可以在相關的網站看到說明

Package fmt implements formatted I/O with functions analogous to C’s printf and scanf. The format ‘verbs’ are derived from C’s but are simpler.

main 是 Go 的保留關鍵字,只要是 func main,就會是程式的入口,概念跟 C 語言的 int main() 相同。

當敲好程式後,可以直接用 go run 來編譯並執行

go run cmd/main.go

將 command-line tool 整合到 language 中,這點就很有現代語言特色,讓開發者更專注心力於開發上。當然,如果只是想要 build 應用程式,也可以使用

go build -o micro-cli.exe ./cmd

執行完後可以看到專案目錄多了一個 micro-cli.exe,執行可以得到

D:\git\golang-introduction>micro-cli.exe
Hello, world

Exec System Command

接著,我們需要使用 Golang 來執行外部的程式,引入 os/exec 這個模組

func main() {
    fmt.Println("Hello, world")
    noteCmd := exec.Command("notepad")
    noteCmd.Start()
}

這樣就能用 Golang 來打開記事本了。

如果除了啟動程式,還需要重新導向該程式的標準輸出到現在的視窗,可以怎麼做?這邊先準備一個文字檔,裡面放要輸出的內容

# README

this is readme file

在 Windows 下,可以使用

type README.md

來輸出檔案內容。我們將 go file 改成

func main() {
    fmt.Println("Hello, world")
    noteCmd := exec.Command("cmd", "/c", "type README.md")
    buf := make([]byte, 1024)
    stdout, _ := noteCmd.StdoutPipe()
    noteCmd.Start()
    stdout.Read(buf)
    os.Stdout.Write(buf)
}

先建立一個 byte 的動態儲存陣列(Go 的專門用語叫 slice),大小是 1024,再將 noteCmd 的標準輸出建立 pipeline,連接到建好的陣列中。如此 noteCmd 的輸出就會像水管,源源不絕進到 buf,我們再由 buf 中取值,輸出到 micro-cli.exe 的標準輸出,完成串接。

Parse Arguments

因為命令行工具需要對應到不同的情境,比如說,有些時候希望輸出 A 檔案的內容,有些時候希望輸出 B 檔案的內容,因此最好有個 option 可以讓人做切換。Golang 的標準庫自帶 argument parser,名稱是 flag,說明可以看,用法是

var (
    help     bool
    filename string
)

func init() {
    flag.BoolVar(&help, "h", false, "this is help")
    flag.StringVar(&filename, "r", "", "select your file")
    flag.Usage = usage
}

func main() {
    flag.Parse()
    if help {
    flag.Usage()
    return
    }
    fmt.Println("Hello, world")
    noteCmd := exec.Command("cmd", "/c", "type README.md")
    buf := make([]byte, 1024)
    stdout, _ := noteCmd.StdoutPipe()
    noteCmd.Start()
    stdout.Read(buf)
    os.Stdout.Write(buf)
}

func usage() {
    fmt.Println("Usage: micro-cli [-h] [-r filename]")
    flag.PrintDefaults()
}

先看第一部分

var (
    help     bool
    filename string
)

func init() {
    flag.BoolVar(&help, "h", false, "this is help")
    flag.StringVar(&filename, "r", "", "select your file")
    flag.Usage = usage
}

用 var 建立兩個全域變數 help 跟 filename,用來儲存 flag 的值,接著在 init 設定 flag。func init 跟 func main 同樣是保留字,當程式進入時,會先執行 init 的內容,之後才進行 main,這用在一些初始化設定很方便。

這邊做了三個初始化設定:(1) 看 h 這個選項是否存在,如果存在,賦值給 help,預設是 false,最後的文字是說明;(2) 看 r 這個選項是否存在,如果存在,賦值給 filename;(3) 將 Usage 這個函數指給 flag.Usage。

再來看第二部分

func main() {
    flag.Parse()
    if help {
    flag.Usage()
    return
    }
    ...
}

func usage() {
    fmt.Println("Usage: micro-cli [-h] [-r filename]")
    flag.PrintDefaults()
}

使用 flag.Parse 來解析選項,並實際賦值給前面設定好的變數;使用後,就能運用 help 跟 filename 了,這邊定義,當使用者用了 h 參數,就印出使用說明。使用說明看函式 usage,會先印出使用方法,再用 PrintDefaults 顯示細部設定。

來看看實際執行結果,先看 help

go run cmd/main.go -h

Usage: micro-cli [-h] [-r filename]
    -h this is help
    -r string
       select your file

簡單將使用說明與程式結合起來。

接著,修改程式,來讀讀看不同的檔案

func main() {
    flag.Parse()
    if help {
    flag.Usage()
    return
    }
    fmt.Println("Hello, world")
    noteCmd := exec.Command("cmd", "/c", "type "+filename)
    buf := make([]byte, 1024)
    stdout, _ := noteCmd.StdoutPipe()
    noteCmd.Start()
    n, _ := stdout.Read(buf)
    os.Stdout.Write(buf[:n])
}

執行

go run cmd/main.go -r file1
Hello, world
This is file 1

go run cmd/main.go -r file2
Hello, world
This is file 2

只要後面帶不同的 filename,就能讀到不同的檔案了

Read Config

因為每次要讀檔案,都要重新再輸入一次 option,對某些情境實在有點麻煩,想想,如果只有一兩個 option 就算了,假設現在 option 有 10 個,每次啟動程式都要輸入,很容易出現 typo,最好的辦法是將不常更改的 option 放在 config file,使用 config 來設定。

先在專案結構中建立一個 configs 資料夾,用來放設定檔,設定檔格式可以使用 json,但不限制,這邊用 json 格式相對單純而且我比較熟

project
├── cmd  # main applications for this project.
|   └── main.go
├── configs
|   └── config.json
├── pkg  # code that's ok to use by external applications
|   ├── module 1
|   |   └── module.go
|   └── module 2
└── README.md

內容是

{
    "filename": "file1"
}

為讀取 config,要先建立一個對應 config 結構的 struct,好讓程式知道該如何將 config 翻譯成物件

type config struct {
    Filename string `json:"filename"`
}

包在反引號 ` 中的文字是 Go 的 tag,它的功用是讓編譯器知道這個 struct 可以對應到 json,在這個例子中,struct config 的 field Filename 對應到 config file 的 filename 欄位。

修改主程式來讀取設定檔

func main() {
    data, _ := ioutil.ReadFile("configs/config.json")
    var fileConfig config
    json.Unmarshal(data, &fileConfig)
    fmt.Println("Hello, world")
    noteCmd := exec.Command("cmd", "/c", "type "+fileConfig.Filename)
    buf := make([]byte, 1024)
    stdout, _ := noteCmd.StdoutPipe()
    noteCmd.Start()
    n, _ := stdout.Read(buf)
    os.Stdout.Write(buf[:n])
}

使用 ioutil 來讀取檔案,將讀取到的 byte 資訊用 json.Unmarshal 反序列化,轉成人眼能看懂的結構,或者講更明白,賦值給 fileConfig 這個變數。接著,就能使用 fileConfig 內的 Filename 來讀取檔案了。

觀察執行結果

go run cmd/main.go
Hello, world
This is file 1

好的,不用每次都帶 option 了。

小結

用簡單的命令行工具,來當 Golang 的入門熱身,可以看到 Golang 跟 C 語言些相同的地方,例如它們都是靜態語言,可讀性跟可維護性較腳本語言更好,適合開發大型程式。但是 Golang 相對於 C,有幾項優點

  • 支援多重回傳值,有效解決 C 語言函式輸入輸出語意模糊的問題
  • 標準庫更強大,例如 flags 或是 ioutil,讓開發者能更專注於開發
  • 支援垃圾回收,同樣也讓開發者更專注於應用
  • 跨平台,這對當前的應用環境很重要,你絕對不希望換個作業系統要重寫一次程式碼
  • 工具齊全,有時候有點太齊全了,例如強制性的語法靜態檢查
  • 編譯速度快如閃電

作為 Google 力推的程式語言,Golang 可以挖掘的地方還有很多,像是它最重要的賣點 Goroutine,很適合開發高併發程式;它的精簡語法也適合開發微服務。我們可以想像它是因應雲端世代而產生的新工具。

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