模板中的方法

107 阅读8分钟

 模板中是能够使用该结构体的方法

在 Go 语言的 html/template 里,要是传递的数据是结构体,那么在模板中是能够使用该结构体的方法的。

package main

import (
    "html/template"
    "log"
    "os"
)

// Person 定义一个结构体
type Person struct {
    Name string
    Age  int
}

// GetGreeting 定义结构体的方法,返回问候语
func (p Person) GetGreeting() string {
    return "Hello, my name is " + p.Name + " and I'm " +
        func() string {
            if p.Age < 18 {
                return "a minor"
            }
            return "an adult"
        }() + "."
}

func main() {
    // 定义模板内容
    tmplStr := `{{define "personTemplate"}}{{.GetGreeting}}{{end}}`

    // 创建并解析模板
    tmpl, err := template.New("personTemplate").Parse(tmplStr)
    if err != nil {
        log.Fatalf("解析模板时出错: %v", err)
    }

    // 创建结构体实例
    person := Person{
        Name: "Alice",
        Age:  25,
    }

    // 执行模板
    err = tmpl.ExecuteTemplate(os.Stdout, "personTemplate", person)
    if err != nil {
        log.Fatalf("执行模板时出错: %v", err)
    }
}
    

转存失败,建议直接上传图片文件

当你把一个结构体实例传递给模板时,模板引擎能够访问该结构体的导出方法(即方法名首字母大写)。在模板里,可通过 . 操作符来调用这些方法。

注意事项

  • 只有导出的方法(方法名首字母大写)才能在模板中被调用。
  • 方法不能有参数(除非通过自定义函数的方式处理),因为模板语法本身不支持带参数的方法调用。若方法需要参数,可考虑使用自定义函数来封装该方法调用。

 向模板传递自定义函数

要向模板传递自定义函数,可以使用 template.FuncMap 类型,它是一个字符串到函数的映射。下面是一个示例代码,展示了如何定义和传递自定义函数

package main

import (
    "html/template"
    "log"
    "os"
)

// 自定义函数:将字符串转换为大写
func toUpper(s string) string {
    return "HELLO: " + s
}

func main() {
    // 定义模板内容
    tmplStr := `{{define "example"}}{{toUpper .}}{{end}}`

    // 创建一个自定义函数映射
    funcMap := template.FuncMap{
        "toUpper": toUpper,
    }

    // 创建一个新的模板并添加自定义函数
    tmpl, err := template.New("example").Funcs(funcMap).Parse(tmplStr)
    if err != nil {
        log.Fatalf("解析模板时出错: %v", err)
    }

    // 定义要传递给模板的数据
    data := "world"

    // 执行模板
    err = tmpl.ExecuteTemplate(os.Stdout, "example", data)
    if err != nil {
        log.Fatalf("执行模板时出错: %v", err)
    }
}    

转存失败,建议直接上传图片文件

  1. 自定义函数定义toUpper 函数接收一个字符串参数并将其转换为大写。
  2. 创建 FuncMaptemplate.FuncMap 是一个字符串到函数的映射,键是函数名,值是对应的函数。
  3. 添加自定义函数到模板:使用 Funcs 方法将 FuncMap 添加到模板中。
  4. 解析和执行模板:使用 Parse 方法解析模板字符串,然后使用 ExecuteTemplate 方法执行模板。

html/template 包中的内置函数

比较函数

  • eq:判断两个值是否相等。
  • ne:判断两个值是否不相等。
  • lt:判断左边的值是否小于右边的值。
  • le:判断左边的值是否小于等于右边的值。
  • gt:判断左边的值是否大于右边的值。
  • ge:判断左边的值是否大于等于右边的值。

逻辑函数

  • and:逻辑与操作,所有参数为真时返回真。
  • or:逻辑或操作,只要有一个参数为真就返回真。
  • not:逻辑非操作,对参数取反。

字符串处理函数

  • len:返回字符串、切片、数组、映射等的长度。
  • print:格式化输出值,类似于 Go 语言中的 fmt.Sprint
  • printf:格式化输出值,类似于 Go 语言中的 fmt.Sprintf
  • println:格式化输出值并换行,类似于 Go 语言中的 fmt.Sprintln

其他函数

  • index:用于访问切片、数组、映射或字符串的元素。
  • slice:用于创建切片。
package main

import (
    "html/template"
    "log"
    "os"
)

func main() {
    // 定义模板内容,使用多种内置函数
    tmplStr := `
{{define "example"}}
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Built-in Functions Example</title>
    </head>
    <body>
        <!-- 使用 len 函数获取切片长度 -->
        <p>Slice length: {{len .Numbers}}</p>
        <!-- 使用 index 函数访问切片元素 -->
        <p>First number: {{index .Numbers 0}}</p>
        <!-- 使用 eq 函数进行比较 -->
        <p>Is 1 equal to the first number? {{eq (index .Numbers 0) 1}}</p>
        <!-- 使用 and 函数进行逻辑与操作 -->
        <p>Is 1 equal to the first number and 2 equal to the second number? {{and (eq (index .Numbers 0) 1) (eq (index .Numbers 1) 2)}}</p>
        <!-- 使用 printf 函数格式化输出 -->
        <p>Formatted string: {{printf "The first number is %d" (index .Numbers 0)}}</p>
    </body>
    </html>
{{end}}`

    // 创建并解析模板
    tmpl, err := template.New("example").Parse(tmplStr)
    if err != nil {
        log.Fatalf("解析模板时出错: %v", err)
    }

    // 定义要传递给模板的数据
    data := struct {
        Numbers []int
    }{
        Numbers: []int{1, 2, 3, 4, 5},
    }

    // 执行模板
    err = tmpl.ExecuteTemplate(os.Stdout, "example", data)
    if err != nil {
        log.Fatalf("执行模板时出错: %v", err)
    }
}
    

转存失败,建议直接上传图片文件

综合案例:博客文章列表页

我们模拟一个博客系统,展示文章列表,每篇文章有标题、内容、发布时间、作者。
模板中将使用:

  • 自定义函数:formatTime(格式化时间)、truncate(截断字符串)
  • 内置函数:slice(切片前3篇文章)
  • 结构体方法:Article.Summary() 获取摘要、Article.IsNew() 判断是否为新文章

1. Go 代码 (main.go)

package main

import (
	"html/template"
	"log"
	"net/http"
	"strings"
	"time"
)

// Article 文章结构体
type Article struct {
	Title    string
	Content  string
	Author   string
	PubTime  time.Time
}

// Summary 获取文章摘要(结构体方法)
func (a Article) Summary() string {
	if len(a.Content) > 100 {
		return a.Content[:100] + "..."
	}
	return a.Content
}

// IsNew 判断是否为7天内发布的新文章(结构体方法)
func (a Article) IsNew() bool {
	return time.Since(a.PubTime) < 7*24*time.Hour
}

// 自定义模板函数
var funcMap = template.FuncMap{
	"formatTime": func(t time.Time) string {
		return t.Format("2006-01-02 15:04")
	},
	"truncate": func(s string, n int) string {
		if len(s) <= n {
			return s
		}
		return s[:n] + "..."
	},
}

func main() {
	// 创建模板,注册自定义函数
	tmpl := template.Must(template.New("blog.html").Funcs(funcMap).ParseFiles("blog.html"))

	// 模拟数据
	articles := []Article{
		{
			Title:   "Go语言入门指南",
			Content: strings.Repeat("Go是一门强大的编程语言。", 10),
			Author:  "张三",
			PubTime: time.Now().Add(-2 * 24 * time.Hour), // 2天前
		},
		{
			Title:   "深入理解Goroutine",
			Content: strings.Repeat("并发是Go的核心特性。", 15),
			Author:  "李四",
			PubTime: time.Now().Add(-10 * 24 * time.Hour), // 10天前
		},
		{
			Title:   "Go Template完全指南",
			Content: strings.Repeat("模板引擎非常实用。", 20),
			Author:  "王五",
			PubTime: time.Now().Add(-1 * time.Hour), // 1小时前
		},
		{
			Title:   "Go与微服务架构",
			Content: strings.Repeat("微服务是现代架构趋势。", 12),
			Author:  "赵六",
			PubTime: time.Now().Add(-5 * 24 * time.Hour), // 5天前
		},
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		data := struct {
			Title    string
			Articles []Article
		}{
			Title:    "我的博客",
			Articles: articles,
		}

		err := tmpl.Execute(w, data)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	})

	log.Println("Server running on http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

2. HTML 模板 (blog.html)

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>{{ .Title }}</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        .article { border: 1px solid #ccc; margin: 20px 0; padding: 20px; border-radius: 5px; }
        .new-badge { background: #ff6b6b; color: white; padding: 3px 8px; border-radius: 3px; font-size: 12px; }
        .summary { color: #555; margin: 10px 0; }
        .meta { font-size: 14px; color: #888; }
    </style>
</head>
<body>
    <h1>{{ .Title }}</h1>
    <h2>最新三篇文章(使用 slice)</h2>

    {{ $top3 := slice .Articles 0 3 }}
    {{ range $top3 }}
        <div class="article">
            <h3>{{ .Title }}
                {{ if .IsNew }} <!-- 调用结构体方法 -->
                    <span class="new-badge">NEW</span>
                {{ end }}
            </h3>
            <div class="summary">
                {{ .Summary }} <!-- 调用结构体方法 -->
                <!-- 或者用自定义函数截断 -->
                <!-- {{ truncate .Content 80 }} -->
            </div>
            <div class="meta">
                作者:{{ .Author }} | 发布:{{ .PubTime | formatTime }} <!-- 自定义函数 -->
            </div>
        </div>
    {{ else }}
        <p>暂无文章。</p>
    {{ end }}

    <hr>
    <h2>全部文章(使用自定义 truncate 函数)</h2>
    {{ range .Articles }}
        <div class="article">
            <h3>{{ .Title }}</h3>
            <div class="summary">
                {{ truncate .Content 60 }} <!-- 自定义函数截断内容 -->
            </div>
            <div class="meta">
                作者:{{ .Author }} | 发布:{{ .PubTime | formatTime }}
            </div>
        </div>
    {{ end }}
</body>
</html>

3. 运行方式

  1. 将 Go 代码保存为 main.go
  2. 将 HTML 代码保存为 blog.html(与 main.go 同目录)
  3. 终端运行:
go run main.go
  1. 打开浏览器访问:http://localhost:8080

效果说明

  • 页面显示“最新三篇文章”,使用 slice .Articles 0 3
  • 每篇文章调用结构体方法 .IsNew() 判断是否显示“NEW”标签
  • 调用 .Summary() 方法显示摘要(也可替换为 truncate 自定义函数)
  • 使用 formatTime 自定义函数格式化时间
  • 使用 truncate 自定义函数在第二部分截断内容展示

知识点总结

类型示例说明
自定义函数formatTimetruncate通过 FuncMap 注册
内置函数slice .Articles 0 3截取数组/切片
结构体方法.IsNew().Summary()在模板中直接调用
管道操作符{{ .PubTime | formatTime }}将值传给函数
条件判断{{ if .IsNew }} ... {{ end }}控制显示逻辑
循环{{ range ... }}遍历切片

❓ {{ .PubTime | formatTime }} 能不能写成 {{ formatTime .PubTime }}

答案:可以!两者完全等价,功能一模一样。


两种写法的区别(仅语法风格不同)

写法名称说明
{{ .PubTime | formatTime }}管道风格(Pipeline)把 .PubTime 作为参数“管道”传给 formatTime 函数,类似 Unix shell 的管道 x | f
{{ formatTime .PubTime }}函数调用风格直接调用函数,把参数写在函数名后面,类似常规编程语言

示例对比

// 模板中这两种写法效果完全相同:
{{ .PubTime | formatTime }}
{{ formatTime .PubTime }}

都会调用:

funcMap["formatTime"] = func(t time.Time) string {
    return t.Format("2006-01-02 15:04")
}

传入 .PubTime,返回格式化后的时间字符串。


什么时候用哪种风格?

👉 推荐使用 管道风格 {{ .Value | func }} 的场景:

  • 链式操作(多个函数依次处理):

    {{ .Content | truncate 50 | title }}
    

    等价于:

    {{ title (truncate .Content 50) }}
    

    → 管道写法更清晰、可读性更强!

  • 和内置动作结合:

    {{ if .Title | len }} ... {{ end }}
    

👉 推荐使用 函数调用风格 {{ func .Value }} 的场景:

  • 只调用一次函数,无链式操作

  • 函数有多个参数(管道只能传第一个参数):

    {{ truncate .Content 30 }} ✅ 正确
    {{ .Content | truncate 30 }} ❌ 错误!管道只能传第一个参数
    

⚠️ 注意:管道操作符只能传递一个参数(作为函数的第一个参数) ,其余参数必须用函数调用风格!