打造世界最快的go模板引擎gorazor 2.0
自2014年與 @於康
等小伙伴發布 gorazor后,我其實沒有想過還會再給它做更新,因為近些年,網站的開發基本朝前后端分離的方向發展,一個供后端使用的模板引擎其實使用場景不多。
gorazor
應該是go語言的第一個支持將模板編譯成為go代碼的“預編譯式”模板引擎。
采用預編譯一個顯而易見的好處當然是渲染速度;沒想一晃五年過去,go的后端模板引擎居然層出不窮,而相比起后來出現的這些模板,gorazor的渲染速度,赫然是最慢的一個:

QuickTemplate
也有在其FAQ中說gorazor
相比起QuickTemplate
少了性能優化。
QuickTemplate的作者也是fasthttp的作者Aliaksandr Valialkin,后者是go語言中非常有名http服務器實現,比go官方內置的net/http
庫快了近10倍。
在研究過QuickTemplate之后,我對其作者valyala采用的優化手段嘆為觀止,試列舉幾項如下:
- 使用bytebufferpool做字符串輸出的緩存池
- 模板渲染過程中實現了zero alloc 零內存分配
- 使用unsafe包來實現對string / bytes的相互轉換
- 獨立實現了writer庫以優化對不同類型數據(int / bytes / float64等等)的轉換與寫入
這些優化我感覺其實已經超乎了其模板引擎本身,而是借鑒在其它很多go項目中。
感嘆之余,我也不禁在想,gorazor能否借鑒這些來做優化呢?
gorazor渲染慢的主要原因,是因為我提供的全部都是基於string的輸入、輸出接口;那么,在壓測(當然還有實際運行)的時候,必然會有大量的字符串對象被創建、銷毀;內存申請多,性能肯定也就慢。
增加StringWriter接口的話,應該可以顯著提高性能:
// string 輸入/輸出接口 func Msg(u *models.User) string { ... } // StringWriter接口 func RenderMsg(_buffer io.StringWriter, u *models.User) { ... } // 后者相對於前者,無需創建並返回 string 對象;而是將數據寫入 _buffer 中 // 寫入 _buffer的話,就可能利用緩存池來避免內存分配
當然,整體接口的改動也意味着需要對gorazor內部的代碼生成引擎做大幅修改,特別是goraozr支持layout / helper的嵌套調用;所有內部謝謝嵌套的調用邏輯都需要改。
但,那就改吧~既然是整體接口的大幅修改,那么版本號是可以升級到2.0的。
經過幾個月斷斷續續的修改,2.0版終於完成。因為支持了Writer接口,我實際上也可以直接使用QuickTemplate的bytebufferpool等方式來進行優化性能測試結果,這只需要幾行代碼:
type quickStringWriter struct { bb *quicktemplate.ByteBuffer } func (q *quickStringWriter) WriteString(s string) (i int, e error) { return q.bb.Write(unsafeStrToBytes(s)) }
便可直接將quickStringWriter
傳遞給gorazor模板。
這樣一來,gorazor的性能實際上已經不遜於QuickTemplate了,畢竟,壓測中性能的主要損耗是在字符串對象的轉換以及緩存池的復用。
但gorazor依舊做不到內存的零分配,唯一的一次內存分配是發生在這里:
// HTMLEscape wraps template.HTMLEscapeString func HTMLEscape(m interface{}) string { switch v := m.(type) { case int: return strconv.Itoa(v) case string: return template.HTMLEscapeString(v) } s := fmt.Sprint(m) return template.HTMLEscapeString(s) }
當在模板中寫入變量時,變量的類型很可能是不同的;最常見的當然是有int以及string;不同的類型也需要做不同的處理,可就是這么一句switch v := m.(type) {
類型判斷會產生新變量以造成內存分配。
咋看這幾乎無解,因為我不可能在模板中限制需要渲染的數據類型;"類型轉換"是必不可免的。
回頭去看QuickTemplate,它對此問題的解決方法,讓我不禁莞爾:
<li>ID={%d row.ID %}, Message={%s row.Message %}</li>
QuickTemplate要求使用者在編寫模板的時候,就直接指定插入變量的類型,如果是int,那么就使用 <%d %>
的標簽,如果是字符串,就使用<%s %>
。
這需要引入新的語法不說,我認為也是會對模板使用者造成一定的心智負擔,那么,有沒有辦法解決呢?
同樣的模板代碼,在gorazor中是這么表示:
<li>ID=@row.ID, Message=@row.Message</li>
而生成出來的代碼是:
_buffer.WriteString("<li>ID=") _buffer.WriteString(gorazor.HTMLEscape(row.ID)) _buffer.WriteString(", Message=") _buffer.WriteString(gorazor.HTMLEscape(row.Message)) _buffer.WriteString("</li>")
經過一番嘗試,我發現可以使用go/types庫,對生成出來的代碼做類型分析,當通過編譯器知道row.ID
以及row.Message
的具體類型后,我自然可以將調用函數自動替換成為具體類型的版本,也就是說,重新生成以下的代碼:
_buffer.WriteString("<li>ID=") _buffer.WriteString(gorazor.HTMLEscInt(row.ID)) _buffer.WriteString(", Message=") _buffer.WriteString(gorazor.HTMLEscStr(row.Message)) _buffer.WriteString("</li>")
這么一來,gorazor模板,也就能夠做到零內存分配了!
壓測的結果非常讓人鼓舞:
- 同樣使用QuickTemplate自身的緩存池以及unsafe轉換下,gorazor可以比QuickTemplate快80%
- 因為razor從語法上就是要去除標簽間的一些空格;我實際上是手動修改了gorazor生成出來的代碼,增加不必要的空格輸出,以確保壓測時輸出的跟QuickTemplate是嚴格一致的數據;如果不做這些額外的空格輸出,gorazor還會更快。
- 即便不使用緩存池,也不會比QuickTemplate慢太多

若按比QuickTemplate快80%的結果看,gorazor 2.0應該是目前世界最快的go模板引擎了。
我其實覺得這樣的性能測試意義並不大;因為很容易為壓測跑分做優化,實際使用中,模板的方便性應該會更加重要;我會特別希望在gorazor 3.0中,增加Language Server Protocol的支持,這樣就可以在VS Code等編輯器中,對模板也實現完整的智能補全。
就不知道3.0版本是否需要再等個五年了 :)