Go语言标准库的text/template和html/template提供了基础的模板渲染能力,但其语法需要显式的标签闭合和较多的冗余字符,对于习惯Haml或Slim风格的开发者来说不够友好。Haml和Slim是两种以缩进为基础、语法极度简洁的模板语言,通过减少冗余标签和符号,大幅提升模板的编写效率。探索在Go语言中实现这类风格的模板引擎,既能满足特定开发场景的需求,也能加深对模板编译与渲染流程的理解。

Haml与Slim的核心语法特点
Haml和Slim的设计理念都是通过缩进表示层级关系,省略冗余的闭合标签,用简洁的符号表示不同的模板元素。两者的核心差异在于符号的使用习惯,但整体思路一致:
- 元素标签通过百分号加标签名表示,比如
%div对应<div>标签 - 类名通过英文点连接,比如
%div.container对应<div class="container"> - ID通过井号连接,比如
%div#header对应<div id="header"> - 属性通过括号包裹,比如
%a(href="/index")对应<a href="/index"> - 文本内容可以直接跟在标签后,或者用管道符
|换行表示 - 支持嵌入代码逻辑,比如变量输出、条件判断、循环等
Go语言实现的核心思路
要实现Haml/Slim风格的模板引擎,核心流程分为三个步骤:模板解析、AST构建、渲染执行。下面分别介绍每个环节的实现要点。
1. 模板解析
首先需要读取模板字符串,按照行和缩进拆分内容,识别出每一行的元素类型、标签名、属性、文本内容等信息。Go语言中可以通过bufio.Scanner逐行读取模板,再通过正则匹配或者逐字符解析的方式提取元素信息。需要注意缩进的计算,通常用空格数或者Tab数来确定当前元素的层级关系。
2. AST构建
解析完成后,需要将解析得到的行信息构建成抽象语法树(AST),树的结构对应模板的层级嵌套关系。每个节点可以包含标签名、属性集合、文本内容、子节点列表、逻辑类型(比如是否为条件判断、循环节点)等信息。构建AST时需要处理层级的嵌套,当下一行的缩进大于当前行时,将其作为当前行的子节点。
3. 渲染执行
渲染阶段需要遍历AST,根据节点类型生成对应的HTML内容。对于普通的元素节点,生成对应的开放标签、属性、文本内容和闭合标签;对于逻辑节点,根据传入的上下文数据执行对应的逻辑,再渲染子节点。最终将所有生成的内容拼接成完整的HTML字符串返回。
基础实现示例
下面是一个简化的Haml风格模板引擎的实现示例,仅支持基础的标签、类名、ID和文本渲染,帮助理解核心流程:
package main
import (
"bytes"
"fmt"
"strings"
)
// 模板节点结构
type Node struct {
Tag string // 标签名,为空时表示文本节点
ID string // ID属性
Classes []string // 类名列表
Attrs map[string]string // 其他属性
Text string // 文本内容
Children []*Node // 子节点
Indent int // 缩进层级
}
// 解析模板行,返回节点信息
func parseLine(line string) *Node {
line = strings.TrimSpace(line)
node := &Node{
Attrs: make(map[string]string),
Children: []*Node{},
}
// 处理标签、ID、类名
if strings.HasPrefix(line, "%") {
line = line[1:]
// 提取标签名
tagEnd := strings.IndexAny(line, ".#(")
if tagEnd == -1 {
tagEnd = len(line)
}
node.Tag = line[:tagEnd]
line = line[tagEnd:]
// 提取类名和ID
for len(line) > 0 {
if line[0] == '.' {
// 类名
line = line[1:]
classEnd := strings.IndexAny(line, ".#(")
if classEnd == -1 {
classEnd = len(line)
}
node.Classes = append(node.Classes, line[:classEnd])
line = line[classEnd:]
} else if line[0] == '#' {
// ID
line = line[1:]
idEnd := strings.IndexAny(line, ".#(")
if idEnd == -1 {
idEnd = len(line)
}
node.ID = line[:idEnd]
line = line[idEnd:]
} else {
break
}
}
// 简化属性解析,仅处理简单的文本属性
if strings.HasPrefix(line, "(") {
line = line[1:]
attrEnd := strings.Index(line, ")")
if attrEnd != -1 {
attrStr := line[:attrEnd]
// 简单分割属性,实际场景需要更复杂的解析
kvs := strings.Split(attrStr, " ")
for _, kv := range kvs {
parts := strings.Split(kv, "=")
if len(parts) == 2 {
node.Attrs[parts[0]] = strings.Trim(parts[1], """)
}
}
line = line[attrEnd+1:]
}
}
// 提取文本内容
node.Text = strings.TrimSpace(line)
} else {
// 纯文本行
node.Text = line
}
return node
}
// 渲染节点为HTML
func renderNode(node *Node, buf *bytes.Buffer) {
if node.Tag != "" {
// 开放标签
buf.WriteString(fmt.Sprintf("<%s", node.Tag))
// ID属性
if node.ID != "" {
buf.WriteString(fmt.Sprintf(" id="%s"", node.ID))
}
// 类名属性
if len(node.Classes) > 0 {
buf.WriteString(fmt.Sprintf(" class="%s"", strings.Join(node.Classes, " ")))
}
// 其他属性
for k, v := range node.Attrs {
buf.WriteString(fmt.Sprintf(" %s="%s"", k, v))
}
buf.WriteString(">")
// 文本内容
if node.Text != "" {
buf.WriteString(node.Text)
}
// 子节点
for _, child := range node.Children {
renderNode(child, buf)
}
// 闭合标签
buf.WriteString(fmt.Sprintf("</%s>", node.Tag))
} else if node.Text != "" {
// 纯文本节点
buf.WriteString(node.Text)
}
}
// 渲染模板
func RenderHaml(template string) string {
// 简单按行拆分,实际场景需要处理缩进层级
lines := strings.Split(template, "n")
var rootNodes []*Node
var currentNodes []*Node
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
node := parseLine(line)
// 简化层级处理,仅按行顺序添加
currentNodes = append(currentNodes, node)
}
rootNodes = currentNodes
// 渲染所有根节点
var buf bytes.Buffer
for _, node := range rootNodes {
renderNode(node, &buf)
}
return buf.String()
}
func main() {
// 示例Haml风格模板
tpl := `%div.container
%h1.title 欢迎使用Haml风格模板
%p 这是一个简化的实现示例`
result := RenderHaml(tpl)
fmt.Println(result)
}
实践中的优化方向
上述示例是非常基础的实现,实际生产使用的模板引擎还需要完善很多功能:
- 支持完整的属性解析,包括动态属性、布尔属性等
- 支持代码嵌入,比如变量输出、条件判断
if、循环each等逻辑 - 支持模板继承、局部模板、宏定义等高级特性
- 增加缓存机制,避免每次渲染都重新解析模板
- 处理HTML转义,防止XSS攻击,类似Go标准库html/template的转义逻辑
目前Go社区也有不少成熟的Haml/Slim风格模板引擎开源项目,比如haml、slim等包,可以直接在项目中集成使用。如果是学习目的,参考本文的思路自行实现基础版本,能更深入理解模板引擎的工作原理。