Go Language and Templates

Last update: 2022-05-03 10:12:40 +0800 +0800
tags: Go | Templates
Available translations: 日本語 / 中文(简体) / 中文(繁體)

Go organizes source into packages and modules, reports recoverable failures as values, and provides reflection-based text and HTML templates in its standard library. The same design principle connects these areas: make dependencies and control flow explicit at the API boundary.

Modules, Packages, and Builds #

A package is the compilation and namespace boundary formed by non-test .go files with the same package declaration in one directory. A module is a versioned collection of packages rooted at go.mod. The module path supplies the import-path prefix:

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 changes module requirements; go install package@version installs a command without changing the current module. In current Go releases, module mode is the standard dependency model, and a module can live outside GOPATH. Nested modules are separate dependency boundaries and are excluded from the parent module’s package pattern.

The build cache stores compiled packages and test results under GOCACHE. Downloaded module source is stored under GOMODCACHE, commonly $GOPATH/pkg/mod. Inspect actual locations with:

sh
go env GOCACHE GOMODCACHE GOPATH

Do not point GOCACHE and GOMODCACHE at the same directory because they have different layouts and lifecycle rules. CI can cache both, keyed by the Go version, target platform, and dependency files, while still treating go.sum as the authenticated module-content record.

A package main containing func main() builds an executable and is not an importable library boundary. Put reusable behavior in another package; keep flag parsing and process exit decisions in main.

Errors, Panic, and Cleanup #

The built-in error interface contains Error() string. Functions return an error when a caller can make a local decision:

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

Wrap with %w when callers should retain access to the cause through errors.Is or errors.As. A panic indicates an unrecoverable invariant failure inside the current abstraction. Libraries should not use panic as a substitute for ordinary error results. A boundary such as an HTTP server may recover a panic to isolate one request, log a stack, and convert the failure into an error response; recovery does not make partially mutated state valid.

defer evaluates the function value and arguments immediately, then executes deferred calls in last-in, first-out order when the surrounding function returns or panics. A deferred closure can inspect named return values. Cleanup errors still need explicit handling, especially for writable files:

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 can report delayed write or metadata errors. Read-only close errors are less likely to affect the operation’s result, but applications can still record them where the resource contract requires it.

Declarations, Shadowing, and Comma-Ok Forms #

:= declares at least one new variable in the current lexical block. A nested declaration can shadow an outer error:

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

Using message, err := readMessage() inside the loop would declare a new err; the outer value would remain unchanged. Keep declarations close to use but use assignment when a result must escape the block.

Three language operations have comma-ok forms:

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

Without the boolean, a failed type assertion panics, while a missing map entry and a receive from a closed channel produce zero values. The boolean therefore separates a real zero value from absence or closure.

Within a constant declaration, iota is the zero-based constant index:

go
type Permission uint8

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

Insertions can change later values, so persist or transmit an iota-based enumeration only when every value is assigned deliberately or compatibility is otherwise controlled.

Defined Types, Aliases, and Reference-Like Values #

An alias introduces another spelling for the same type; a definition creates a distinct named type with the specified underlying type:

go
type Bytes = []byte
type UserID string

Bytes and []byte are identical. UserID and string require conversion when neither assignment rule applies, and methods can be declared on UserID.

Slices, maps, channels, functions, and interfaces contain references to runtime state and are inexpensive descriptors to copy. Passing a slice by value copies its pointer, length, and capacity, not its elements. Copying a map or channel value likewise does not duplicate its backing state. A pointer to one of these values is needed only when the function must replace the caller’s descriptor or represent an additional state.

Go has no class-subtyping relation. A concrete type satisfies an interface by having its methods, and embedding promotes selected fields and methods without turning one struct into a subtype of another. Method sets differ between T and *T: methods with a value receiver belong to both method sets, while a pointer-receiver method belongs to *T.

Template Parsing and Execution #

text/template generates arbitrary text. html/template implements the same core language but contextually escapes untrusted values for HTML, attributes, URLs, JavaScript, and CSS. Use html/template for HTML output; converting untrusted text to a trusted content type such as template.HTML bypasses the protection.

Parse templates, register functions before parsing expressions that name them, and check execution errors:

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
}

An action appears between {{ and }}. Dot (.) is the current argument. Pipelines pass one command’s final value to the next command. if, with, and range execute conditionally; range changes dot for each element. define, template, and block create and invoke named templates. Variables such as {{$root := .}} preserve a value when dot changes.

A template invocation accepts one pipeline argument:

gotemplate
{{template "card" .}}

Pass several logical values by wrapping them in a struct, map, slice, or helper result. Prefer a typed view-model struct because field names and ownership stay visible in Go:

go
type CardData struct {
    User    User
    Enabled bool
}

Template.Execute may already have written partial output when an evaluation fails. Render into a bytes.Buffer first when the caller requires all-or-nothing output. After parsing, a template can execute concurrently provided callers do not mutate it and writers are independent.

Editor and Tooling Boundaries #

The conventional filename suffix is .tmpl, .gotmpl, or a content suffix such as .html. Editor support depends on the extension and embedded-language grammar; no filename changes Go’s template semantics. Format and test the Go code with gofmt, go vet, and go test, then execute representative template data in tests because the Go compiler cannot type-check reflected field and method names inside template source.

The authoritative references are the Go language specification, the go command, and the package documentation for text/template and html/template.

Comments

Read or comment on GitHub (sign-in required to comment)