renderer是Go語言的一個簡單的、輕量的、快速響應的呈現(xiàn)包,它可以支持JSON、JSONP、XML、HYAML、HTML、File等類型的響應。在開發(fā)web應用或RESTFul API的時候,這個包是非常方便的toolkit。
本文繞開如何使用它,深入到代碼實現(xiàn)中研究它,同時也嘗嘗Go語言包的開發(fā)套路。
Go包基礎介紹
代碼結構
package pkgname
import (
"fmt"
...
)
const (
CONST1 typeX = xx
...
)
var (
VAR1 typeX = xxx
...
)
func Fn1() {
}
在Go語言中包名和目錄名保持一致,同一包內可共享命名空間。
- 包文件開頭除了注釋外,第一行,必須是package pkgname, 聲明包的名稱。
- 在包聲明之后,可以import標準庫中的包和其他外部包。
- 然后可以定義包常量、包變量(暴露變量和非暴露變量,以首字母大小寫來區(qū)分實現(xiàn))。
- 然后定義自定義類型、函數(shù)或方法。
import語句
import可以引入標準庫的包,也可以引入外部包。Go語言中一旦引入某個包,必須在程序中使用到這個包的命名空間,否則編譯報錯會告訴你引入了某個包,但代碼中未曾使用。
當然你也會有疑問,我如果需要引入包,但又不想使用怎么辦。這個Go語言有一個特殊的符號"_", 放在引入包名前面,就可以防止編譯報錯。為什么會有這種考慮呢? 因為有時候,我們只是希望引入一個包,然后執(zhí)行這個包的一些初始化設置。然后在代碼中暫時不使用該包的任何方法和變量。
import (
_ "gitHub.com/xxxxx/pkgname"
)
上面語句會引入pkgname命名空間,但是暫時不在代碼中使用這個命名空間。這樣引入之后,會在pkgname包中尋找init()函數(shù),然后在main()函數(shù)執(zhí)行之前先執(zhí)行它們,這點對于需要使用包之前做初始化非常有用。
暴露與非暴露的實現(xiàn)
我們在其他編程語言中,都接觸過private, protected, public之類的修飾符。 但是在Go語言中完全沒有這些,但是Go語言還是可以某些東西從包中暴露出去,而某些東西不暴露出去,它用的原則很簡單的,就是標識符如果以小寫字母開頭的,包外不可見; 而如果是標識符以大寫字符開頭的,包外可見,可訪問。
對于暴露變量和函數(shù)(方法)非常直觀簡單,但是如果是暴露的結構體,情況稍微復雜一點。 不過本質上也是差不多, 結構體外部如果小寫字母開頭,內部屬性大寫字母開頭。 則外部包直接不訪問,但如果通過函數(shù)或方法返回這個外部類型,那么可以通過:=得到這個外部類型,從而可以訪問其內部屬性。舉例如下:
// package pkgname
package pkgname
type admin struct {
Name string
Email String
}
func Admin() *admin {
return admin{
Name: "admin",
Email: "admin@email.com",
}
}
那么我們在外部包中,可以直接通過下面代碼訪問admin結構體內部的屬性:
admin := pkgname.Admin()
fmt.Println(admin.Name, admin.Email)
當然這種情況下,需要你事先知道admin的結構以及包含的屬性名。
內置類型和自定義類型
Go語言包含了幾種簡單的內置類型:整數(shù)、布爾值、數(shù)組、字符串、分片、映射等。除了內置類型,Go語言還支持方便的自定義類型。
自定義類型有兩種:
- 自定義結構體類型: type MyType struct {}這種形式定義,這種類似C語言中的結構體定義。
- 命名類型: type MyInt int。這種方式通過將內置類型或自定義類型命名為新的類型的方式來實現(xiàn)。 需要注意MyInt和int是不同的類型,它們之間不能直接互相賦值。
函數(shù)和方法
Go語言的函數(shù)和方法都是使用func關鍵詞聲明的,方法和函數(shù)的唯一區(qū)別在于,方法需要綁定目標類型; 而函數(shù)則無需綁定。
type MyType struct {
}
// 這是函數(shù)
func DoSomething() {
}
// 這是方法
func (mt MyType) MyMethod() {
}
// 或者另外一種類型的方法
func (mt *MyType) MyMethod2() {
}
對于方法來說,需要綁定一個receiver, 我稱之為接收者。 接收者有兩種類型:
- 值類型的接收者
- 指針類型的接收者
關于接收者和接口部分,有很多需要延伸的,后續(xù)有時間整理補充出來。
接口
代碼分析
常量部分
代碼分析部分,我們先跳過import部分, 直接進入到常量的聲明部分。
const (
// ContentType represents content type
ContentType string = "Content-Type"
// ContentJSON represents content type application/json
ContentJSON string = "application/json"
// ContentJSONP represents content type application/javascript
ContentJSONP string = "application/javascript"
// ContentXML represents content type application/xml
ContentXML string = "application/xml"
// ContentYAML represents content type application/x-yaml
ContentYAML string = "application/x-yaml"
// ContentHTML represents content type text/html
ContentHTML string = "text/html"
// ContentText represents content type text/plain
ContentText string = "text/plain"
// ContentBinary represents content type application/octet-stream
ContentBinary string = "application/octet-stream"
// ContentDisposition describes contentDisposition
ContentDisposition string = "Content-Disposition"
// contentDispositionInline describes content disposition type
contentDispositionInline string = "inline"
// contentDispositionAttachment describes content disposition type
contentDispositionAttachment string = "attachment"
defaultCharSet string = "utf-8"
defaultJSONPrefix string = ""
defaultXMLPrefix string = `?xml version="1.0" encoding="ISO-8859-1" ?>\n`
defaultTemplateExt string = "tpl"
defaultLayoutExt string = "lout"
defaultTemplateLeftDelim string = "{{"
defaultTemplateRightDelim string = "}}"
)
以上常量聲明了內容類型常量以及本包支持的各種內容類型MIME值。以及各種具體內容類型相關的常量,比如字符編碼方式、JSONP前綴、XML前綴,模版左右分割符等等一些常量。
類型聲明部分
這部分聲明了如下類型:
- M: 映射類型,描述代表用于發(fā)送的響應數(shù)據(jù)便捷類型。
- Options: 描述選項類型。
- Render: 用于描述renderer類型。
type (
// M describes handy type that represents data to send as response
M map[string]interface{}
// Options describes an option type
Options struct {
// Charset represents the Response charset; default: utf-8
Charset string
// ContentJSON represents the Content-Type for JSON
ContentJSON string
// ContentJSONP represents the Content-Type for JSONP
ContentJSONP string
// ContentXML represents the Content-Type for XML
ContentXML string
// ContentYAML represents the Content-Type for YAML
ContentYAML string
// ContentHTML represents the Content-Type for HTML
ContentHTML string
// ContentText represents the Content-Type for Text
ContentText string
// ContentBinary represents the Content-Type for octet-stream
ContentBinary string
// UnEscapeHTML set UnEscapeHTML for JSON; default false
UnEscapeHTML bool
// DisableCharset set DisableCharset in Response Content-Type
DisableCharset bool
// Debug set the debug mode. if debug is true then every time "VIEW" call parse the templates
Debug bool
// JSONIndent set JSON Indent in response; default false
JSONIndent bool
// XMLIndent set XML Indent in response; default false
XMLIndent bool
// JSONPrefix set Prefix in JSON response
JSONPrefix string
// XMLPrefix set Prefix in XML response
XMLPrefix string
// TemplateDir set the Template directory
TemplateDir string
// TemplateExtension set the Template extension
TemplateExtension string
// LeftDelim set template left delimiter default is {{
LeftDelim string
// RightDelim set template right delimiter default is }}
RightDelim string
// LayoutExtension set the Layout extension
LayoutExtension string
// FuncMap contain function map for template
FuncMap []template.FuncMap
// ParseGlobPattern contain parse glob pattern
ParseGlobPattern string
}
// Render describes a renderer type
Render struct {
opts Options
templates map[string]*template.Template
globTemplates *template.Template
headers map[string]string
}
)
New函數(shù)
// New return a new instance of a pointer to Render
func New(opts ...Options) *Render {
var opt Options
if opts != nil {
opt = opts[0]
}
r := Render{
opts: opt,
templates: make(map[string]*template.Template),
}
// build options for the Render instance
r.buildOptions()
// if TemplateDir is not empty then call the parseTemplates
if r.opts.TemplateDir != "" {
r.parseTemplates()
}
// ParseGlobPattern is not empty then parse template with pattern
if r.opts.ParseGlobPattern != "" {
r.parseGlob()
}
return r
}
用于創(chuàng)建Render類型的函數(shù)。它接受Options類型的參數(shù),返回一個Render類型。
我們一般通常不傳入Options類型變量調用renderer.New()來創(chuàng)建一個Render類型。
var opt Options
if opts != nil {
opt = opts[0]
}
r := Render{
opts: opt,
templates: make(map[string]*template.Template),
}
上面這段代碼實際上就是初始化了一個Render類型的r變量。opts為nil, templates為map類型,這里被初始化。
接下來調用r.buildOptions()方法。
buildOptions方法
func (r *Render) buildOptions() {
if r.opts.Charset == "" { // 沒有指定編碼方式,使用默認的編碼方式UTF-8
r.opts.Charset = defaultCharSet
}
if r.opts.JSONPrefix == "" { // 沒有指定JSON前綴,使用默認的
r.opts.JSONPrefix = defaultJSONPrefix
}
if r.opts.XMLPrefix == "" { // 沒有指定XML前綴,使用默認XML前綴
r.opts.XMLPrefix = defaultXMLPrefix
}
if r.opts.TemplateExtension == "" { // 模版擴展名設置
r.opts.TemplateExtension = "." + defaultTemplateExt
} else {
r.opts.TemplateExtension = "." + r.opts.TemplateExtension
}
if r.opts.LayoutExtension == "" { // 布局擴展名設置
r.opts.LayoutExtension = "." + defaultLayoutExt
} else {
r.opts.LayoutExtension = "." + r.opts.LayoutExtension
}
if r.opts.LeftDelim == "" { // 模版變量左分割符設置
r.opts.LeftDelim = defaultTemplateLeftDelim
}
if r.opts.RightDelim == "" { // 模版變量右分割符設置
r.opts.RightDelim = defaultTemplateRightDelim
}
// 設置內容類型屬性常量
r.opts.ContentJSON = ContentJSON
r.opts.ContentJSONP = ContentJSONP
r.opts.ContentXML = ContentXML
r.opts.ContentYAML = ContentYAML
r.opts.ContentHTML = ContentHTML
r.opts.ContentText = ContentText
r.opts.ContentBinary = ContentBinary
// 如果沒有禁用編碼集,那么就將內容類型后面添加字符集屬性。
if !r.opts.DisableCharset {
r.enableCharset()
}
}
該方法構建Render的opts屬性,并綁定默認的值。
我們看了New函數(shù),得到了一個Render類型,接下來就是呈現(xiàn)具體類型的內容。我們以JSON為例,看看它怎么實現(xiàn)的。
JSON方法
如果沒有renderer包,我們想要用Go語言發(fā)送JSON數(shù)據(jù)響應,我們的實現(xiàn)代碼大致如下:
func DoSomething(w http.ResponseWriter, ...) {
// json from a variable v
jData, err := json.Marshal(v)
if err != nil {
panic(err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write(jData)
}
原理很簡單,首先從變量中解析出JSON, 然后發(fā)送Content-Type為application/json, 然后發(fā)送狀態(tài)碼, 最后將json序列發(fā)送出去。
那么我們再詳細看看renderer中的JSON方法的實現(xiàn):
func (r *Render) JSON(w http.ResponseWriter, status int, v interface{}) error {
w.Header().Set(ContentType, r.opts.ContentJSON)
w.WriteHeader(status)
bs, err := r.json(v)
if err != nil {
return err
}
if r.opts.JSONPrefix != "" {
w.Write([]byte(r.opts.JSONPrefix))
}
_, err = w.Write(bs)
return err
}
大致看上去,和我們不使用renderer包的實現(xiàn)基本一樣。指定Content-Type, 發(fā)送HTTP狀態(tài)碼,然后看JSON前綴是否設置,如果設置,前綴也發(fā)送到字節(jié)流中。 最后就是發(fā)送json字節(jié)流。
唯一區(qū)別在于,我們使用encoding/json包的Marshal方法來將給定的值轉換成二進制序列,而renderer對這個方法進行了包裝:
func (r *Render) json(v interface{}) ([]byte, error) {
var bs []byte
var err error
if r.opts.JSONIndent {
bs, err = json.MarshalIndent(v, "", " ")
} else {
bs, err = json.Marshal(v)
}
if err != nil {
return bs, err
}
if r.opts.UnEscapeHTML {
bs = bytes.Replace(bs, []byte("\\u003c"), []byte(""), -1)
bs = bytes.Replace(bs, []byte("\\u003e"), []byte(">"), -1)
bs = bytes.Replace(bs, []byte("\\u0026"), []byte(""), -1)
}
return bs, nil
}
如果有設置JSONIndent, 即JSON縮進,那么使用json.MarshalIndent來將變量轉換為json字節(jié)流。 這個方法其實就是將JSON格式化,使得結果看起來更好看。
另外這個方法還會根據(jù)配置,進行html實體的轉義。
因此總體來說原理和開頭的代碼基本一樣。只不過多了一些額外的修飾修補。
JSONP方法
我們理解了JSON方法,理解起JSONP就更加簡單了。
JSONP全稱為JSON with Padding, 用于解決Ajax跨域問題的一種方案。
它的原理非常簡單:
// 客戶端代碼
var dosomething = function(data) {
// do something with data
}
var url = "server.jsonp?callback=dosomething";
// 創(chuàng)建 script> 標簽,設置其 src 屬性
var script = document.createElement('script');
script.setAttribute('src', url);
// 把 script> 標簽加入 body> 尾部,此時調用開始。
document.getElementsByTagName('body')[0].appendChild(script);
上面server.jsonp是一個后臺腳本,訪問后立即返回它的輸出內容, 這也就是renderer的JSONP要響應的內容。
func (r *Render) JSONP(w http.ResponseWriter, status int, callback string, v interface{}) error {
w.Header().Set(ContentType, r.opts.ContentJSONP)
w.WriteHeader(status)
bs, err := r.json(v)
if err != nil {
return err
}
if callback == "" {
return errors.New("renderer: callback can not bet empty")
}
w.Write([]byte(callback + "("))
_, err = w.Write(bs)
w.Write([]byte(");"))
return err
}
- 設置Content-Type為application/javascript, 非常關鍵的一點。 想一想html中嵌入的js文件的mime類型是不是也是這個值?
- 然后同樣的設置響應狀態(tài)碼, 這點沒有什么特殊的。
- 將值轉換為json字節(jié)序列。這個json字節(jié)序列還沒有向響應寫入進去。
- 這個時候我們檢查callback是否存在,不存在報錯出去。因為是JSONP, 必須要有callback, 這個callback是請求參數(shù)傳入的。
- 然后用"callbak("和")"將json字節(jié)序列包圍起來,一起輸出到響應流中。這樣jsonp響應就產生了。
那么回過頭結合我們開頭寫的一個前段jsonp代碼,我們知道請求了server.jsonp?callback=xxxx之后,一個application/javascript的內容被嵌入到body內。它是js文件。 而其內容將callback替換為傳入的dosomething, 我們得到類似的js內容:
dosomething({
// ....
});
這樣服務端產生數(shù)據(jù),并調用前端js的方法, 傳入這些數(shù)據(jù), jsonp就完成了。這樣的js一旦加載成功,它和當前訪問域名是同源的,不存在跨域問題。 這樣就解決了ajax跨域問題。
剩下的其他方法基本都是同樣的套路, 這里不再贅述, 有時間的話再重新整理下開頭的內容。
本文僅個人學習整理, 如有不對之處, 還望各位不吝指出。
在鏈接部分,有我自己對Go in Action英文書籍的翻譯, 英文比較差,再者也是初學Go語言,翻譯不到位, 有興趣的朋友可以一起翻譯此書,或者后續(xù)有其他好的技術書籍,一起翻譯學習。
引用鏈接
- renderer
- Go In Action
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。