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

從個人貢獻者到管理者:關於領導的反思

從個人貢獻者到管理者:關於領導的反思

某個下雪天,我拖著病體,組裝一套供使用者簡報之用的破爛系統,莎朗進來發現我在操控台前勉強支撐,她便離開了,幾分鐘後,她端著一鍋湯回來,為我倒了一杯,我的精神為之一振。我問她要做的管理工作那麼多,怎麼會有空做這種事,她向我展露她的招牌微笑,說:「湯姆,這就是管理。」 -《Peopleware: Productive Projects and Teams》 有次跟一名職涯顧問聊天。我提到:「我希望透過打造產品來替別人創造價值,如果有很棒的團隊,我相信自己能辦到。」她問:「團隊是必須的嗎?」我愣住了,隨口說:「因為打造產品需要很多不同的職能……還需要可持續性的運作,對,我想團隊是必要的。」事後回想,她的問題很有趣,現代社會好像把「團隊」和「領導」當成是成功的標配,人力市場也一堆團隊主管的職缺,這是一則現代神話嗎?還是某種工業革命時代的遺產? 身為個人貢獻者的管理者 不是說團隊不重要,只是在現代,你會用不同的角度審視完成目標需要的條件。你想想,如果你是個開發者,自己架網站、

By Ken Chen
CDN 的快取失效設計:內容平台場景

CDN 的快取失效設計:內容平台場景

Phil Karlton 有句名言:「計算機科學中只有兩件難事:快取失效和命名。」 想像你在管理網站,因為傳輸速度與伺服器效能問題,網站讀取速度很差,特別是當你的使用者來自地球另外一端,常常需要等待幾秒才能看到畫面,這讓他們的使用體驗大打折扣。身為一名重視使用體驗的開發者,你肯定知道該如何解決這問題,沒錯,答案就是 CDN(內容傳遞網路)。 CDN 可以看成是服務商在全球各地建置伺服器,當你的網站內容(例如圖片、CSS、JavaScript、影片等)流經這些伺服器時,它會保留一份複本(稱為快取),等到下次有人讀取同樣的內容,CDN 會拿出複本給使用者。因為全球各地都有 CDN 節點,美國的使用者可以由美國節點提供,日本的使用者可以由日本節點提供。這樣既加速網路傳遞效率,也降低來源伺服器的效能壓力,可謂一舉兩得。 當然這是有條件的。CDN 會使用網址來判斷快取是否是相同檔案,假設你的內容以圖片為主,通常來說,當你更換圖片,新舊兩張圖片會有不同網址,被當成兩個不同的檔案,新圖使用新快取,舊快取留著也沒差;但如果你的內容是文字,新舊版文字很可能有相同網址,

By Ken Chen
收拾行李搬家去:從 Medium 到 Ghost

收拾行李搬家去:從 Medium 到 Ghost

想搬家想很久,連身邊的朋友都搬完了,我還沒動工。 原因是我懶,我討厭麻煩,每次有人問我吃什麼,我都回答麥當勞。搬家是一件麻煩事,我已經有一份很讚的工作了,全副精神都放在工作上,偶爾才會想起來,反正家什麼時候都能搬,一點也不急,有什麼好急的呢對吧。這樣一拖,就拖到現在。 繼續用 Medium 不好嗎? 跟男女朋友分手一樣,通常被問到:「對方不好嗎?」得到回答是:「也沒有不好啦,只是……(以下開放填空)。」 從優點開始講吧!Medium 的編輯器很棒,它是 WYSIWYG(所見即所得)類型的編輯器,能讓創作者快速發佈內容,也因為它讓內容發佈更容易了,它開始吸引一批優秀的創作者,這批創作者持續創作內容,又吸引來更多讀者,更多讀者激勵創作者產出內容,內容又再吸引讀者……這形成一個增強迴圈。Medium 還能支援多人協作,拜它時尚簡約的風格所賜,科技公司會使用 Medium 來打造品牌形象,例如我前公司的 Tech Blog

By Ken Chen
OpenTelemetry 的可觀察性工程:以 Sentry 為例

OpenTelemetry 的可觀察性工程:以 Sentry 為例

點進 OpenTelemetry 的官方文件,它最先映入眼中的句子是「什麼是 OpenTelemetry」。例如,它是套可觀察性框架,用於檢測、蒐集與導出遙測數據;它是開源且供應商中立,能搭配其他的開源工具,像 Jaeger 或 Prometheus;它能將應用程式與系統儀表化,無關是用 Go 還是 .NET 開發,也無關部署在 AWS 還是 GCP 上。 但是身為一名開發者,當下我們想的是:「公司常開發一些沒人要用的功能,聽說 OpenTelemetry 可以提高可觀察性,也許我們應該放棄開發功能,轉頭建立更好的開發環境。」「AWS 常常要不到需要的數據,也許我們應該改用另一套工具,像是 OpenTelemetry,來解決這件事。」我們想像 OpenTelemetry 「應該」要能解決目前面臨到的一些問題,就像在技術的鏡像中尋找願望一樣。 如果已經有在用 Sentry,還需要導入 OpenTelemetry

By Ken Chen