Wednesday, August 31, 2016

vim 的插件 vim-go 的 tutorial

vim-go 的 tutorial  裡頭有很多有趣、有用的技巧。取出一些我覺得比較好用的。

Import 的技巧
:GoImportAs ff fmt
:GoImport strings

Edit 的技巧
if 表示函數的內部 inside a function
dif = delete inside a function
yif = yank   inside a function
vif = visual mode inside a function

af 表示整個函數 -> a function
daf = delete a function
yaf = yank a function
vaf = visual mode a function


Trace 程式碼的技巧
對 channel 的理解
 :GoChannelPeers  選擇某個channel,顯示它的send/receive/def

對 variable 的理解
 :GoReferrers     選擇某個變數,顯示所有被使用的地方。

對於 type 的理解
 :GoImplements    選擇某個類別,顯示它實作的介面。(一個有搭配函數的類別就很可能是有實作介面的類別)
 :GoDescribe      選擇某個類別,顯示所有使用它的函數。(用使用範例來"描述"一個類別)

對於 error 的理解
 :GoWhicherrs     選擇某個錯誤,顯示它可能包含的錯誤資訊。

對 func 的理解
 :GoCallees       選擇某個區域變數(該變數的型態是函數),顯示它可以對應到的函數。
 :GoCallstack     選擇某個函數顯示它被呼叫時的call stack
 :GoCallers       選擇某個函數的定義,顯示它被呼叫的位置

Refactor 的技巧
將區段的程式碼抽取出來,變成函數
 :GoFreevars      區塊選擇一段程式碼之後,顯示這段程式碼裡的輸入變數(input variables)
 :GoRename        修改一個變數的命名,自動修改多個位置。


註:在新版的 vim-go ,已經用 guru 取代了 oracle 。所以如果沒有看到 oracle ,不用再去找了。

log 與 error handling --- 讓錯誤看得出來是錯誤

公司的同事 mike 前幾天跟我講了幾句話,讓我受用無窮:

  1. 很多程式設計師寫出了幽靈程式碼,總是抓不出錯。這是因為在撰寫程式時,就不夠嚴謹。
  2. 要如何寫出嚴謹的程式碼呢? 遇到異常(exception)或是錯誤(error),有辦法處理,就要設法處理。沒有辦法處理時,也要寫在規格(也就是註解),至少讓後來維護的人知道這邊有個「坑」


於是我歸納了一些要寫出嚴謹的 golang 程式,可以注意的重點:

  1. 不要亂用 log level ,要小心地使用 log level 。比方說,在 golang 裡, log.Fatal 就會呼叫 exit(1) ,這個就絕對不可以亂用。有時候,網路上找到的 code snippet 就用了 log.Fatal。如果直接不加思索的照抄。那程式就會異常終止了。
  2. log level 和 log message 要好好地搭配使用: 比方說,如果引入了 logrus 這套 log level 的函式庫。對於不會影響程式正常運作的 log level 就有 info, warn, error 三種。既然已經用了 warn 的 log level ,其實 log message 就該避開使用 error  這個關鍵字。
  3. 儘量在程式裡處理所有的函數傳回的 error 。要做到這件事,有一個 golang tool 可以使用: errcheck 。這個工具可以檢查出,所有傳回錯誤,錯誤卻沒有被處理的函數。如果有用 vim-go 的話,可以下指令 :GoErrCheck  

Thursday, August 18, 2016

依賴注入 - dependency injection

寫單元測試時,常常會遇到一問題,原始的程式碼本身的結構,難以加入單元測試。

最近我遇到了一個問題,我寫的程式會呼叫 git clone ,而 git clone 在網路環境不佳時,甚至會執行超過24小時,將程式的 go routine 整個卡住。所以解法就是需要將 golang 的外部指令呼叫,改成有 timeout 的。

要加上這個 timeout 並不難,困難點是,要如何測試這個功能?因為其實要模擬出 git clone 長時間執行的環境,並不是很簡單的事。

解法是這樣子:
加上這個功能時,必須將這個「為指令加上 timeout 功能」實作成一個獨立的函數,使它的功能獨立,並不「依賴」於 git clone 。換言之,「 git clone 」對應的指令,會成為這個函數的輸入變數(input argument),由外部「注入」。於是這個功能就可以獨立地來寫單元測試來測。測試它的時候,就可以用「 sleep 500 」對應的指令,來做為它的輸入變數(input argument)

原始程式碼的修改
// Original -> New
cmd := exec.Command("git", "clone", gitRemoteAddr, file.Basename(pluginDir))
cmd.Dir = parentDir
- err2 := cmd.Run()
+ err2 := RunCmdWithTimeout(cmd, 600)

新增的程式碼和單元測試
func RunCmdWithTimeout(cmd *exec.Cmd, timeout int64) (err error) {
if err = cmd.Start(); err != nil {
log.Println("Start shell command error:", err)
}
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
d := time.Duration(time.Duration(timeout) * time.Second)
select {
case err = <-done:
case <-time.After(d):
// timed out
if err = cmd.Process.Kill(); err != nil {
log.Printf("failed to kill: %s, error: %s", cmd.Path, err)
}
err = fmt.Errorf("Command %s time out", cmd.Path)
}
return
}
// Unit test of RunCmdWithTimeout
func TestRunCmdWithTimeout(t *testing.T) {
cmd := exec.Command("sleep", "500")
t1 := time.Now()
RunCmdWithTimeout(cmd, 3)
t2 := time.Now()
t.Log("Time spent should less than 4 second: ", t2.Sub(t1))
}

除錯的利器 - tcpdump

開發程式,我最常用來除錯的技巧主要就是幾種:

  1. 寫單元測試 
  2. 查 log 檔

然而,上述兩種技巧都有一些限制。

單元測試的話,如果原本的程式寫的很差,我自己新寫的部分有時也很難測,要先重構舊的程式才能寫單元測試。查 log 檔的話,如果一些 bug 是在 production 環境才出現,也很難取得完整的 log ,因為在 production 環境, loglevel 通常是設定成 info 而已。

也因此,在 production 環境之下,用 tcpdump 有時可以取得比 log 還更完整的資訊。
  sudo tcpdump -i eth1 port 8433 -X
  sudo tcpdump -nei eth4 port 8433 -X > ./dump.pcap
view raw gistfile1.md hosted with ❤ by GitHub

Wednesday, August 17, 2016

例外處理

package g
import (
"bytes"
"fmt"
"github.com/toolkits/file"
"os/exec"
"strings"
)
func GetCurrPluginVersion() string {
if !Config().Plugin.Enabled {
return "plugin not enabled"
}
pluginDir := Config().Plugin.Dir
if !file.IsExist(pluginDir) {
return "plugin dir not existent"
}
cmd := exec.Command("git", "rev-parse", "HEAD")
cmd.Dir = pluginDir
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return fmt.Sprintf("Error:%s", err.Error())
}
return strings.TrimSpace(out.String())
}
view raw bad_example.go hosted with ❤ by GitHub



寫程式有一段時間了,有一些習慣問題,卻一直沒有深刻的認識。直到真的寫出 bug 以後,才會理解。

上方的例子,就是一個不好的例子。 GetCurrPluginVersion() 的傳回值,其實應該要有兩個資料型態,一個應該要用 string ,另一個應該要用 error 。而上頭的例子,只用一個 string 就裝了兩個不同資料型態的資料。這樣子讓使用 GetCurrPluginVersion() 的使用者,很容易潛意識地主觀認為,這個函數是一定會成功的,不會有 error 。於是,真的有 error 時,就一直穿透,直到很後期才發現。