Go后端用regexp在渲染前做高亮是推荐路径:先html.EscapeString()转义原文本,关键词用regexp.QuoteMeta()处理,再以(?i)模式匹配替换为<mark>标签,最后通过template.HTML输出。

gin 框架本身不处理搜索高亮,它只负责把数据传给模板;真正做高亮的是 Go 代码层或前端 JS。直接在 html/template 里用正则替换关键词并加 <mark> 是最轻量、可控性最强的做法。
Go 后端用 regexp 在渲染前做高亮
这是推荐的默认路径:避免 XSS、语义清晰、模板干净。关键点是提前转义原始文本,再对关键词做正则匹配替换。
- 必须先用
html.EscapeString()处理原文本,否则用户输入的<script>会被执行 - 关键词要过
regexp.QuoteMeta(),否则.、+、(等字符会破坏正则逻辑 - 大小写不敏感匹配用
(?i)前缀,别漏掉 - 替换时用
ReplaceAllStringFunc而非ReplaceAllString,确保只替换完整匹配词(避免“go”匹配到“golang”里)
keywords := []string{"golang", "redis", "cache"}
quoted := make([]string, len(keywords))
for i, k := range keywords {
quoted[i] = regexp.QuoteMeta(k)
}
pattern := "(" + strings.Join(quoted, "|") + ")"
re := regexp.MustCompile(`(?i)` + pattern)
highlighted := re.ReplaceAllStringFunc(html.EscapeString(text), func(m string) string {
return "<mark>" + m + "</mark>"
})
c.HTML(http.StatusOK, "search.html", gin.H{"content": template.HTML(highlighted)})
模板里直接写 range + replace 不可靠
html/template 的 {{ replace .text "foo" "<mark>foo</mark>" | safe }} 看似简单,但有硬伤:
- 不支持正则,无法大小写忽略,也无法避免子串误匹配(如把 “is” 替成
<mark>is</mark>导致 “this” 变成 “th<mark>is</mark>”) - 关键词含特殊字符(如
c++)会触发模板语法错误或静默失败 - 多次调用
replace会产生嵌套标签,比如redis和red同时存在时 -
safe是信任所有内容,若.text来自用户输入且未预转义,XSS 风险直接暴露
前端 JS 高亮适合动态搜索场景
当页面已加载、用户在输入框实时打字搜索时,后端全量高亮反而冗余。此时用 JS 更合适:
- 用
textContent获取原始段落文本,避免解析 HTML 标签干扰 - 构造
RegExp时显式传入g和i标志,不要依赖字符串拼接漏掉 - 替换用
String.prototype.replaceAll()(注意 IE 不支持,需降级为replace(/.../gi, ...)) - 别直接 innerHTML 整段重写,应只替换文本节点,保留原有 DOM 结构和事件绑定
function highlightText(node, keywords) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
const escaped = keywords.map(k => k.replace(/[.*+?^${}()|[]\]/g, '\$&')).join('|');
const re = new RegExp(`(${escaped})`, 'gi');
const highlighted = text.replace(re, '<mark>$1</mark>');
if (highlighted !== text) {
const span = document.createElement('span');
span.innerHTML = highlighted;
node.replaceWith(span);
}
} else {
node.childNodes.forEach(n => highlightText(n, keywords));
}
}
为什么不用 template.FuncMap 封装高亮函数
有人想把高亮逻辑封装进模板函数,比如 {{ highlight .text .keywords }},但实际踩坑多:
- 模板函数参数类型受限:
.keywords是 interface{},需强制断言为[]string,失败就 panic - 无法在函数内调用
html.EscapeString再返回template.HTML,因为模板引擎会二次转义 - 每次渲染都重新编译正则,无缓存,QPS 上千时 CPU 明显抖动
- 调试困难:错误堆栈指向模板行号,而非真实 Go 文件位置
真正需要复用时,不如写个工具方法放在 util/highlight.go,显式传参、显式错误处理、显式缓存正则实例——比模板函数更稳、更易测。


















