Go 語言與範本

最後更新: 2022-05-03 10:12:40 +0800 +0800
tags: Go | Templates
原文以英文撰寫。您正在閱讀AI翻譯版本。

Go 將原始碼組織為套件與模組,將可復原的失敗以值回報,並在標準函式庫中提供以反射為基礎的文字範本與 HTML 範本。這些領域由同一項設計原則連結:在 API 邊界明確表達相依性與控制流程。

模組、套件與建置 #

套件是由同一目錄中具有相同套件宣告的非測試 .go 檔案所形成的編譯與命名空間邊界。模組是以 go.mod 為根的版本化套件集合。模組路徑提供匯入路徑前綴:

sh
mkdir example
cd example
go mod init example.com/team/example
go get golang.org/x/sync@v0.17.0
go mod tidy
go test ./...
go build ./cmd/server

go get 會變更模組需求;go install package@version 會安裝命令,但不會變更目前的模組。在目前的 Go 發行版本中,模組模式是標準的相依性模型,模組可以位於 GOPATH 外部。巢狀模組是獨立的相依性邊界,並且會從父模組的套件模式中排除。

建置快取會將已編譯的套件與測試結果儲存在 GOCACHE 下。下載的模組原始碼會儲存在 GOMODCACHE 下,通常位於 $GOPATH/pkg/mod。使用以下指令檢查實際位置:

sh
go env GOCACHE GOMODCACHE GOPATH

不要將 GOCACHE 與 GOMODCACHE 指向同一個目錄,因為兩者具有不同的配置與生命週期規則。CI 可以依 Go 版本、目標平台與相依性檔案為兩者設定快取鍵,同時仍將 go.sum 視為經過驗證的模組內容紀錄。

包含 func main() 的 package main 會建置可執行檔,而不是可匯入的函式庫邊界。將可重用行為放在另一個套件中;將旗標剖析與程序結束決策保留在 main 中。

錯誤、panic 與清理 #

內建的 error 介面包含 Error() string。當呼叫端可以在本地做出決策時,函式會回傳錯誤:

go
f, err := os.Open(path)
if err != nil {
    return fmt.Errorf("open %q: %w", path, err)
}
defer f.Close()

當呼叫端應能透過 errors.Is 或 errors.As 保留對原因的存取時,使用 %w 包裝錯誤。panic 表示目前抽象內發生無法復原的不變量失敗。函式庫不應使用 panic 取代一般錯誤結果。HTTP 伺服器等邊界可以復原 panic,以隔離單一請求、記錄堆疊,並將失敗轉換為錯誤回應;復原不會使部分變更的狀態變得有效。

defer 會立即評估函式值與引數,然後在外圍函式回傳或發生 panic 時,依後進先出的順序執行延遲呼叫。延遲閉包可以檢查具名回傳值。清理錯誤仍需明確處理,尤其是可寫入的檔案:

go
func writeFile(path string, data []byte) (err error) {
    f, err := os.Create(path)
    if err != nil {
        return err
    }
    defer func() {
        if closeErr := f.Close(); err == nil {
            err = closeErr
        }
    }()

    _, err = f.Write(data)
    return err
}

Close 可能回報延遲發生的寫入或中繼資料錯誤。唯讀關閉錯誤較不可能影響操作結果,但如果資源契約要求,應用程式仍可記錄這些錯誤。

宣告、遮蔽與逗號 ok 形式 #

:= 會在目前的詞法區塊中宣告至少一個新變數。巢狀宣告可能遮蔽外層錯誤:

go
var err error
for {
    var message []byte
    message, err = readMessage()
    if err != nil {
        break
    }
    _ = message
}
return err

在迴圈內使用 message, err := readMessage() 會宣告新的 err;外層值將保持不變。讓宣告靠近使用位置,但當結果必須離開區塊時使用指派。

三種語言操作具有逗號 ok 形式:

go
value, present := m[key]       // map lookup
value, open := <-ch            // channel receive
value, matches := x.(Target)   // type assertion

省略布林值時,失敗的型別斷言會觸發 panic;缺少的 map 項目與從已關閉通道接收則會產生零值。因此,布林值能將真正的零值與不存在或關閉區分開來。

在常數宣告中,iota 是從零開始的常數索引:

go
type Permission uint8

const (
    Read Permission = 1 << iota
    Write
    Execute
)

插入內容可能改變後續值,因此只有在每個值都經過刻意指派,或以其他方式控制相容性時,才應持久化或傳送以 iota 為基礎的列舉。

定義型別、別名與類參照值 #

別名為相同型別引入另一種寫法;定義則建立具有指定基礎型別的獨立具名型別:

go
type Bytes = []byte
type UserID string

Bytes 與 []byte 完全相同。當兩種指派規則都不適用時,UserID 與 string 需要轉換,且可以在 UserID 上宣告方法。

切片、map、channel、函式與介面包含對執行時狀態的參照,是複製成本低的描述元。以值傳遞切片時,複製的是其指標、長度與容量,而不是元素。複製 map 或 channel 值同樣不會複製其底層狀態。只有當函式必須替換呼叫端的描述元,或表示額外狀態時,才需要指向這些值的指標。

Go 沒有類別子型別關係。具體型別透過擁有相應方法來滿足介面;嵌入會提升選定的欄位與方法,但不會將一個結構變成另一個結構的子型別。T 與 *T 的方法集不同:值接收者方法屬於兩個方法集,而指標接收者方法屬於 *T。

範本剖析與執行 #

text/template 會產生任意文字。html/template 實作相同的核心語言,但會根據 HTML、屬性、URL、JavaScript 與 CSS 的情境,對不受信任的值進行逸出。HTML 輸出應使用 html/template;將不受信任的文字轉換為 template.HTML 等受信任內容型別會繞過這項保護。

剖析範本,在剖析命名這些函式的運算式前先註冊函式,並檢查執行錯誤:

go
const source = `{{define "row"}}<li>{{.Name}}</li>{{end}}
<ul>{{range .Items}}{{template "row" .}}{{else}}<li>empty</li>{{end}}</ul>`

tmpl, err := template.New("page").Option("missingkey=error").Parse(source)
if err != nil {
    return err
}
if err := tmpl.Execute(output, data); err != nil {
    return err
}

動作出現在 {{ 與 }} 之間。點號(.)是目前的引數。管線會將一個命令的最終值傳遞給下一個命令。if、with 與 range 會有條件地執行;range 會針對每個元素變更點號。define、template 與 block 會建立並呼叫具名範本。當點號變更時,{{$root := .}} 等變數可以保留某個值。

範本呼叫接受一個管線引數:

gotemplate
{{template "card" .}}

若要傳遞多個邏輯值,請將它們包裝在結構、map、切片或輔助函式結果中。優先使用具型別的檢視模型結構,因為欄位名稱與擁有權在 Go 程式碼中保持可見:

go
type CardData struct {
    User    User
    Enabled bool
}

評估失敗時,Template.Execute 可能已經寫入部分輸出。當呼叫端要求全有或全無的輸出時,請先渲染至 bytes.Buffer。剖析後,只要呼叫端不修改範本且寫入器彼此獨立,範本即可並行執行。

編輯器與工具邊界 #

慣用的檔名後綴是 .tmpl、.gotmpl,或 .html 等內容後綴。編輯器支援取決於副檔名與嵌入語言文法;檔名不會改變 Go 範本的語意。使用 gofmt、go vet 與 go test 格式化及測試 Go 程式碼,接著在測試中執行具代表性的範本資料,因為 Go 編譯器無法對範本原始碼中的反射欄位名稱與方法名稱進行型別檢查。

權威參考資料包括 Go 語言規格、go 命令,以及 text/template 和 html/template 的套件文件。

評論

在 GitHub 上閱讀或評論(發表評論需要登入)