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 上阅读或评论(发表评论需要登录)