去提问»
提问背景 Question:
按下面的来改,先不要管代码高亮的事情了。你看看下面的代码。 // 2. 百科详情页 Handler // 路由配置:r.GET("/baike/article/:id", ai.Baike_detail) func Baike_detail(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { articleIDStr := ps.ByName("id") articleID, err := strconv.Atoi(articleIDStr) if err != nil { http.NotFound(w, r) return } // 1. 查询文章主体 var a Article var contentRaw string var createdAtInt int64 row := fun.DB.QueryRow( "SELECT id, question, title, description, content, click_count, like_count, created_at FROM cms_article WHERE id = ? AND status = 1", articleID, ) err = row.Scan(&a.ID, &a.Question, &a.Title, &a.Description, &contentRaw, &a.ClickCount, &a.LikeCount, &createdAtInt) if err != nil { if err == sql.ErrNoRows { http.NotFound(w, r) } else { http.Error(w, "数据库错误: "+err.Error(), http.StatusInternalServerError) } return } //a.Content = template.HTML(contentRaw) // 转换为非转义 HTML,直接渲染 a.Content = ConvertMarkdown(contentRaw) //解析Markdown并转义template.HTML a.CreatedAt = time.Unix(createdAtInt, 0).Format("2006-01-02 15:04") // 2. 异步增加浏览量(避免阻塞主渲染) go func(id int) { _, _ = fun.DB.Exec("UPDATE cms_article SET click_count = click_count + 1 WHERE id = ?", id) }(articleID) // 3. 查询当前文章的 Tags var tags []Tag tagRows, err := fun.DB.Query(` SELECT t.id, t.name FROM cms_tag t INNER JOIN cms_article_tag at ON t.id = at.tag_id WHERE at.article_id = ?`, articleID, ) if err == nil { for tagRows.Next() { var t Tag if err := tagRows.Scan(&t.ID, &t.Name); err == nil { tags = append(tags, t) } } tagRows.Close() } data := BaikeDetailData{ Article: a, Tags: tags, } // 4. 渲染模板 path := filepath.Join(fun.AppPath, "/templates/ai/baike_detail.html") mb, err := fun.TManager.Get(path) if err != nil { http.Error(w, "详情模板渲染失败: "+err.Error(), http.StatusInternalServerError) return } err = mb.Execute(w, data) if err != nil { fun.WriteLog("渲染百科详情页模板报错: %s", err) } } func ConvertMarkdown(mdContent string) template.HTML { var buf bytes.Buffer if err := goldmark.Convert([]byte(mdContent), &buf); err != nil { return template.HTML(mdContent) // 如果解析失败,回退返回原始文本 } return template.HTML(buf.String()) } 感觉是\n没有处理好,在交给md解析之前,是不是应该先处理什么东西。换行吗?

解决Markdown字面量\n导致解析失效方案

发布时间:2026-08-18 14:28 👁️ 阅读:34

你的直觉非常精准!这确实是导致 Markdown 渲染完全失效的根本原因。

1. 为什么会这样?

在数据传输(特别是 JSON 格式传输)、API 响应或从某些不规范的数据库存储中读取时,真正的换行符(如回车 )往往会被转义为字面量的两个字符:\(反斜杠)和 n(字母n)

  • 真正的换行符:在计算机中是一个不可见的控制字符(ASCII 码 10)。
  • 字面量 :是两个普通的可见字符。

Goldmark 作为标准的 Markdown 解析器,必须依赖真正的换行符来区分行。例如,只有在换行后紧跟 ###-,它才会将其解析为标题或列表。如果中间夹杂的是字面量的 ,解析器会认为整篇文档只有一条极长的“单行文本”,从而将所有 Markdown 标记直接原文输出,导致排版彻底崩溃。


2. 后端解决方案:归一化处理

在将文本交给 Goldmark 之前,我们应当在 Go 后端对字面量 进行预处理,将其替换为真正的换行符。同时,为了保险起见,建议引入 strings 包,并同时兼容处理 Windows 的回车换行符(`

`)。

请修改你的 ConvertMarkdown 函数,代码如下:

import (
	"bytes"
	"html/template"
	"strings"

	"github.com/yuin/goldmark"
)

func ConvertMarkdown(mdContent string) template.HTML {
	// 1. 将字面量的 "\
" (反斜杠+n) 替换为真正的换行符 "
"
	mdContent = strings.ReplaceAll(mdContent, "\
", "
")
	
	// 2. 兼容性处理:将字面量的 "\
\
" 替换为真正的 "

"
	mdContent = strings.ReplaceAll(mdContent, "\
\
", "
")
	
	// 3. 兼容性处理:将 Windows 换行符统一替换为 Linux 换行符(可选,防患未然)
	mdContent = strings.ReplaceAll(mdContent, "

", "
")

	var buf bytes.Buffer
	if err := goldmark.Convert([]byte(mdContent), &buf); err != nil {
		return template.HTML(mdContent) // 如果解析失败,回退返回原始文本
	}
	return template.HTML(buf.String())
}

3. 修改后的效果对照

做出此修改后:

  1. `

核心食材将会变成物理换行,Goldmark 会正确识别###并将其渲染为核心食材`。

  1. `
  • 优质皮蛋将会换行并被正确识别为列表优质皮蛋`。
  1. 原来夹杂在文本中的强转义标签如 <strong> 也将正常生效,或者你可以直接使用标准的 Markdown 语法 **粗体**,两者均能完美呈现。