這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
go 工具來了,集大成,全整合。沒了 Makefile 還真有點不習慣。此文甚好,早就想翻譯了,無奈最近焦頭爛額……不管怎麼樣,還是動手了。
原文要翻牆,訪問請謹慎:The go tool
—————-翻譯分隔線—————-
線上介紹了 go 命令的最新 weekly 發布後,我決定寫一些關於它的內容。我得承認,在第一次聽說統一 go 工具的時候,我滿是懷疑並對此非常恐懼。我擔心它會像大多數其他語言特定的包管理器一樣混亂。個人認為多數這種包管理器都是在重新發明輪子,並且與作業系統的包管理器發生衝突,讓系統管理員生活的更加艱辛。另外,我確實喜歡 makefile,它們簡單並且直接,工作得也很好。幸運的是,新的 go 工具驅散了我的恐懼!
拒絕重複…
最近在 go nuts 郵件清單上有大量的關於新的 go 工具的資訊。官方的 go 文檔也包含了一些關於如何使用 go 工具編寫 go 代碼的短文。
無論如何,我覺得當前在這些文檔中還存在一些缺漏,因此這成為了撰寫關於新的 go 工具的快速指南,並對一些技巧加以示範的理由。
配置的約定
這是我恐懼最大的來源,多半是因為我在 Ruby on Rails 上的經曆。所有熟悉 Rails 的開發人員都會同意,每當你嘗試做一些微小的改進,一點點小技巧,一些不遵循規則的東西,它就……
不過還是來談談最佳實務吧。首先,每個 go 工具只做一件事情,並且把這件事情做得很好。例如,我們有:
go build
– 編譯包,
go get
– 解析並安裝依賴,
go test
– 執行測試案例和效能測試,
go install
– 安裝包,
go doc
– 產生文檔,
go fmt
– 格式化代碼,
go run
– 構建並執行應用,
go tool
– 調用擴充工具,
- 等等……
go 包沒有任何的構建配置。沒有 makefile,沒有依賴描述等等。那麼它如何工作的?所有都是從代碼中擷取。為了讓這個魔法生效,首先有一件事情要做。需要指定 go 那些七七八八都存在哪裡。環境變數 GOPATH 定義了 go 代碼樹的路徑。例如,下面是在 ~/.bashrc 中的:
GOPATH="/home/nu7/gocode"
……告訴 go 工具你的 go 代碼樹存放在指定的目錄中。但是你可能想知道 go 代碼樹到底是什麼?簡單說,它就是所有 go 資源、包和命令存放的地方。例如:
$ ls /home/nu7/gocode/bin pkg src
所有的代碼會放到 src 目錄。我所說的所有的代碼意味著包含你的應用、包和依賴。pkg 目錄包含編譯和安裝後的包,而 bin 存放命令。
GOPATH 變數的工作方式與 PATH 類似,可以隨意設定若干個 go 路徑。不過務必記得其中的第一個是主要路徑,因此使用 go install 安裝的內容將全部存放於此。
解決依賴
沒有用來描述依賴關係的設定檔……那麼高明的 go 工具是如何判斷安裝什麼,以及從哪裡下載呢!你認為在某個地方有一個倉庫?不對,沒有這個!Go 帶來了叫做 importpath 的東西,來看看:
import "github.com/nu7hatch/gouuid"
匯入路徑是變形平板的。它是代碼倉庫 URL 和包將安裝的本地路徑。go get 工具只需要看看匯入路徑就知道從哪裡獲得依賴,而 go build 也能知道在本地從哪裡匯入它們。
為了在系統中安裝依賴,必須這樣使用 go get 工具:
$ go get package-name
等等,等等……這裡的 package-name 是什嗎?這是希望安裝的依賴的包的名字。假設在 go 代碼中有一個叫做 foo 的包,調用 go get foo 將會安裝其所有的依賴。同樣可以在包中直接運行這個工具:
$ cd ~/gocode/src/foo$ go get .
其他所有的 go 工具都是類似的方式工作,並且可以在包中直接調用,或指定匯入路徑。同時也可以在一組嵌套的包上使用 …(三個點)通配執行命令。如果 foo 包包含一些嵌套的包,為了一次安裝所有需要的依賴,只需要執行:
$ go get ./...
如果指定的依賴已經安裝在了 go 代碼樹中,除非明確指定,否則就不會進行更新。在執行 go get 進行依賴安裝的時候,增加 -u 標識進行更新:
$ go get -u package-name
很簡單,不是嗎?
依賴的地獄!
go tool 有一個約定是我特別喜歡的,不過恐怕與此同時……go 工具通過檢出程式碼程式庫的 HEAD 版本來解決依賴。這強制包保持向下相容,並且……
綠色主線政策是我在工作中一直堅持的。預設分支總是首先被檢出的,因此它應當保持乾淨,或者至少它應當可以工作!一旦官方發布,或者已經成熟,它應當同樣有向下相容的能力——總不能在補丁或者小版本上推翻或者修改 API 吧。
不過我們都知道在實踐的時候是怎麼回事。許多人像把向下相容當作一坨屎,或把預設分支當作遊樂場,等等。對於那些人,以及所有期望同新的 go 工具和平共處的開發人員,我有一些建議……
在該死的程式員生涯中應當遵循的規則:
- 保持該死的主線乾淨!
- 在該死的獨立分支上進行該死的新功能的開發!
- 一旦你公布了代碼,並且某些人使用了它,那就 TMD 不要修改 API!
- 如果想要或者必須修改 API,修改 TMD 主要版本號 並且 在獨立於原程式碼程式庫的新的程式碼程式庫上進行該死的開發!
- 如果有必要,非常有必要使用某些特別的標籤,做分支或提交作為依賴,你得 TMD 用自己 fork 出來的程式碼程式庫進行 TMD 所需要的提交!
- 哦,還有保持簡單,狗娘養的!
別讓我給你讀以西結書(譯註:《希伯來聖經》中的一部Crowdsourced Security Testing書)來提醒你這些……
構建和安裝
好,讓我們回到 go 命令。go build 命令用於編譯包。它僅僅編譯指定包,而不會進行安裝。重要的是,它要求包已經檢出在本地代碼樹中。要安裝遠程包應當使用 go get 來代替:
$ go get github.com/nu7hatch/gouuid
安裝當地套件當然使用 go install 工具。(如果有必要的話)它首先構建包然後將其安裝在 $GOPATH/pkg 或者/以及 $GOPATH/bin。
go 工具在構建的時候也能忽略檔案,無需顯式的指定額外的參數或者特別的配置。對於要忽略的檔案唯一要做的,就是讓其名字前有一個底線:
$ ls_bar.go foo.go$ go build .
在上面的例子中,構建時 _bar.go 將會被忽略。
唔,就這樣吧……我想關於這些也沒什麼好說的了,讓我們繼續下面的內容。
用 CGO 的 C 擴充
go 通過 cgo 命令獲得了構建 C 擴充的良好支援。實際上,編譯大多數 C 支援的應用,甚至無需瞭解 cgo,go build 工具足夠了。
實話說,關於 cgo 真沒什麼好講的,文檔和 go user wiki 中的文章已經涵蓋了大部分內容。
首先要說說的是那些我確實不喜歡的東西,也就是在官方例子中展示的將 C 代碼寫在注釋中的做法。你得知道,這些例子主要是為了減少代碼的尺寸並讓每個例子都在同一個檔案中示範。在實際的應用中 C 代碼不應當放在注釋部分!go build 工具能夠聰明的處理包中的 .h 和 .c 檔案。
需要一些例子?來看一個將所有參數都列印到螢幕上的簡單的 echo 命令,不過要使用來自 stdio.h 的 printf 函數。與 wiki 上所述一致,go 不允許調用變長參數的 C 函數,因此需要對 printf 函數編寫一個簡單的包裹。代碼看起來大約是這樣(同樣可在 github 上瀏覽到):
echo.h:
#ifndef _ECHO_H_#define _ECHO_H_#include <stdio.h>void echo(char*);#endif /* _ECHO_H_ */
echo.c:
#include "echo.h"void echo(char* s){ printf("%s\n", s);}
echo.go:
package main/*#include <stdlib.h>#include "echo.h"*/import "C"import ( "flag" "unsafe" "strings")func main() { flag.Parse() cs := C.CString(strings.Join(flag.Args(), " ")) C.echo(cs) C.free(unsafe.Pointer(cs))}
現在可以用 go build 工具無縫的編譯所有的東西。它能識別出在包中所有的 C 檔案。簡單來說這就能工作了!
特定平台的構建
go build 另一個很酷卻又有趣的地方是處理特定平台的檔案。它能通過名字來識別(必須是像 file_GOOS_GOARCH.go 或者 file_GOARCH.go ):
foo_darwin_amd64.gofoo_386.gofoo.go
這個功能對 C 檔案同樣適用:
foo_amd64.cfoo_386.cfoo.hfoo.go
就像文檔裡說的那樣,你可能永遠都用不到這個功能,不過我還是像給大家展示一下 go 工具在如此簡單的基礎上做到了多麼的靈活。
好,但是你們當中可能有人會問,如果要做一些棘手的事情需要特殊的編譯參數或者一些配置,例如……
救世主 Makefile
是的,不要害怕使用 makefile!這是最簡便的方法來做額外的配置,一些預請求等等。makefile 不但對於 C 擴充很有協助,對於具有多個包的應用也同樣適用(例如,在 webrocket 中使用了一個頂級的 makefile 來讓任務更加輕鬆)。
更加清楚的例子……設想一個包含了核心包和基於此的命令列工具的應用。可以套用 echo 的例子,不過更加模組化:
echo/ pkg/ echo/ echo.c echo.h echo.go cmd/ echo/ echo.go
希望 pkg/echo 包提供一個可重用的 C printf 函數的包裹,它的代碼看起來跟前面的例子中的一樣。cmd/echo 命令是一個使用了核心包來列印到螢幕的可執行檔。cmd/echo 命令是這樣:
package mainimport ( "github.com/nu7hatch/cgoecho2/pkg/echo" "flag")func main() { flag.Parse() echo.Echo(flag.Args()...)}
注意:對於那些不瞭解 slice 的人來說 … 意味著將一個 slice 映射到變長參數,就像 Ruby 中的 *args。
回到主題,為了讓對包的工作更加簡單,需要一些 Makefile。看起來大約是這樣:
all: echo-pkg echo-cmdecho-pkg: go build ./pkg/echoecho-cmd: go build ./cmd/echo
現在就可以通過調用 make 來快速的編譯包和命令兩個部分,還有一個很重要的,可以使用 go 命令遠程安裝:
$ go get github.com/nu7hatch/cgoecho2/cmd/echo
當然這時非常簡單的一個例子,完全可以使用通配來進行快速的構建,而不必這麼小題大作:
$ go build ./...
但是面對更大的應用,包含許多包和/或命令就會是王道了。那時有各種理由使用 makefile、shell 指令碼或者任何你喜歡的構建工具。
總結
我不得不明確表態,我 TMD 超級喜歡新的 go 工具!在第一次用的時候,我遇到了一大堆麻煩,不過大多數都是因為我從其他包管理工具上養成的壞習慣。哈,最近我在 go-nuts IRC 上問了許多愚蠢的問題,而我想要的答案是如此的顯而易見並且簡單……
現在回顧用過的全部工具,例如 easy_install、rubygems 或者 bundler,我對它們只有一個印象……哦,我還是不要說出來了,免得許多人恨我 :)。取而代之,我可以向你展示我是如何看待新的 go 工具的……
看到 Go 正在向好的方向發展,我很欣慰!今天就到這裡吧,好運,Gopher 們!