renderer是Go語言的一個簡單的、輕量的、快速響應(yīng)的呈現(xiàn)包,它可以支持JSON、JSONP、XML、HYAML、HTML、File等類型的響應(yīng)。在開發(fā)web應(yīng)用或RESTFul API的時候,這個包是非常方便的toolkit。
10年的無為網(wǎng)站建設(shè)經(jīng)驗(yàn),針對設(shè)計(jì)、前端、開發(fā)、售后、文案、推廣等六對一服務(wù),響應(yīng)快,48小時及時工作處理。全網(wǎng)營銷推廣的優(yōu)勢是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動調(diào)整無為建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。成都創(chuàng)新互聯(lián)公司從事“無為網(wǎng)站設(shè)計(jì)”,“無為網(wǎng)站推廣”以來,每個客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。
本文繞開如何使用它,深入到代碼實(shí)現(xiàn)中研究它,同時也嘗嘗Go語言包的開發(fā)套路。
Go包基礎(chǔ)介紹
代碼結(jié)構(gòu)
package pkgname import ( "fmt" ... ) const ( CONST1 typeX = xx ... ) var ( VAR1 typeX = xxx ... ) func Fn1() { }
在Go語言中包名和目錄名保持一致,同一包內(nèi)可共享命名空間。
import語句
import可以引入標(biāo)準(zhǔn)庫的包,也可以引入外部包。Go語言中一旦引入某個包,必須在程序中使用到這個包的命名空間,否則編譯報錯會告訴你引入了某個包,但代碼中未曾使用。
當(dāng)然你也會有疑問,我如果需要引入包,但又不想使用怎么辦。這個Go語言有一個特殊的符號"_", 放在引入包名前面,就可以防止編譯報錯。為什么會有這種考慮呢? 因?yàn)橛袝r候,我們只是希望引入一個包,然后執(zhí)行這個包的一些初始化設(shè)置。然后在代碼中暫時不使用該包的任何方法和變量。
import ( _ "gitHub.com/xxxxx/pkgname" )
上面語句會引入pkgname命名空間,但是暫時不在代碼中使用這個命名空間。這樣引入之后,會在pkgname包中尋找init()函數(shù),然后在main()函數(shù)執(zhí)行之前先執(zhí)行它們,這點(diǎn)對于需要使用包之前做初始化非常有用。
暴露與非暴露的實(shí)現(xiàn)
我們在其他編程語言中,都接觸過private, protected, public之類的修飾符。 但是在Go語言中完全沒有這些,但是Go語言還是可以某些東西從包中暴露出去,而某些東西不暴露出去,它用的原則很簡單的,就是標(biāo)識符如果以小寫字母開頭的,包外不可見; 而如果是標(biāo)識符以大寫字符開頭的,包外可見,可訪問。
對于暴露變量和函數(shù)(方法)非常直觀簡單,但是如果是暴露的結(jié)構(gòu)體,情況稍微復(fù)雜一點(diǎn)。 不過本質(zhì)上也是差不多, 結(jié)構(gòu)體外部如果小寫字母開頭,內(nèi)部屬性大寫字母開頭。 則外部包直接不訪問,但如果通過函數(shù)或方法返回這個外部類型,那么可以通過:=得到這個外部類型,從而可以訪問其內(nèi)部屬性。舉例如下:
// package pkgname package pkgname type admin struct { Name string Email String } func Admin() *admin { return &admin{ Name: "admin", Email: "admin@email.com", } }
那么我們在外部包中,可以直接通過下面代碼訪問admin結(jié)構(gòu)體內(nèi)部的屬性:
admin := pkgname.Admin() fmt.Println(admin.Name, admin.Email)
當(dāng)然這種情況下,需要你事先知道admin的結(jié)構(gòu)以及包含的屬性名。
內(nèi)置類型和自定義類型
Go語言包含了幾種簡單的內(nèi)置類型:整數(shù)、布爾值、數(shù)組、字符串、分片、映射等。除了內(nèi)置類型,Go語言還支持方便的自定義類型。
自定義類型有兩種:
函數(shù)和方法
Go語言的函數(shù)和方法都是使用func關(guān)鍵詞聲明的,方法和函數(shù)的唯一區(qū)別在于,方法需要綁定目標(biāo)類型; 而函數(shù)則無需綁定。
type MyType struct { } // 這是函數(shù) func DoSomething() { } // 這是方法 func (mt MyType) MyMethod() { } // 或者另外一種類型的方法 func (mt *MyType) MyMethod2() { }
對于方法來說,需要綁定一個receiver, 我稱之為接收者。 接收者有兩種類型:
關(guān)于接收者和接口部分,有很多需要延伸的,后續(xù)有時間整理補(bǔ)充出來。
接口
代碼分析
常量部分
代碼分析部分,我們先跳過import部分, 直接進(jìn)入到常量的聲明部分。
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 = "}}" )
以上常量聲明了內(nèi)容類型常量以及本包支持的各種內(nèi)容類型MIME值。以及各種具體內(nèi)容類型相關(guān)的常量,比如字符編碼方式、JSONP前綴、XML前綴,模版左右分割符等等一些常量。
類型聲明部分
這部分聲明了如下類型:
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類型變量調(diào)用renderer.New()來創(chuàng)建一個Render類型。
var opt Options if opts != nil { opt = opts[0] } r := &Render{ opts: opt, templates: make(map[string]*template.Template), }
上面這段代碼實(shí)際上就是初始化了一個Render類型的r變量。opts為nil, templates為map類型,這里被初始化。
接下來調(diào)用r.buildOptions()方法。
buildOptions方法
func (r *Render) buildOptions() { if r.opts.Charset == "" { // 沒有指定編碼方式,使用默認(rèn)的編碼方式UTF-8 r.opts.Charset = defaultCharSet } if r.opts.JSONPrefix == "" { // 沒有指定JSON前綴,使用默認(rèn)的 r.opts.JSONPrefix = defaultJSONPrefix } if r.opts.XMLPrefix == "" { // 沒有指定XML前綴,使用默認(rèn)XML前綴 r.opts.XMLPrefix = defaultXMLPrefix } if r.opts.TemplateExtension == "" { // 模版擴(kuò)展名設(shè)置 r.opts.TemplateExtension = "." + defaultTemplateExt } else { r.opts.TemplateExtension = "." + r.opts.TemplateExtension } if r.opts.LayoutExtension == "" { // 布局?jǐn)U展名設(shè)置 r.opts.LayoutExtension = "." + defaultLayoutExt } else { r.opts.LayoutExtension = "." + r.opts.LayoutExtension } if r.opts.LeftDelim == "" { // 模版變量左分割符設(shè)置 r.opts.LeftDelim = defaultTemplateLeftDelim } if r.opts.RightDelim == "" { // 模版變量右分割符設(shè)置 r.opts.RightDelim = defaultTemplateRightDelim } // 設(shè)置內(nèi)容類型屬性常量 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 // 如果沒有禁用編碼集,那么就將內(nèi)容類型后面添加字符集屬性。 if !r.opts.DisableCharset { r.enableCharset() } }
該方法構(gòu)建Render的opts屬性,并綁定默認(rèn)的值。
我們看了New函數(shù),得到了一個Render類型,接下來就是呈現(xiàn)具體類型的內(nèi)容。我們以JSON為例,看看它怎么實(shí)現(xiàn)的。
JSON方法
如果沒有renderer包,我們想要用Go語言發(fā)送JSON數(shù)據(jù)響應(yīng),我們的實(shí)現(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ā)送出去。
那么我們再詳細(xì)看看renderer中的JSON方法的實(shí)現(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包的實(shí)現(xiàn)基本一樣。指定Content-Type, 發(fā)送HTTP狀態(tài)碼,然后看JSON前綴是否設(shè)置,如果設(shè)置,前綴也發(fā)送到字節(jié)流中。 最后就是發(fā)送json字節(jié)流。
唯一區(qū)別在于,我們使用encoding/json包的Marshal方法來將給定的值轉(zhuǎn)換成二進(jìn)制序列,而renderer對這個方法進(jìn)行了包裝:
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 }
如果有設(shè)置JSONIndent, 即JSON縮進(jìn),那么使用json.MarshalIndent來將變量轉(zhuǎn)換為json字節(jié)流。 這個方法其實(shí)就是將JSON格式化,使得結(jié)果看起來更好看。
另外這個方法還會根據(jù)配置,進(jìn)行html實(shí)體的轉(zhuǎn)義。
因此總體來說原理和開頭的代碼基本一樣。只不過多了一些額外的修飾修補(bǔ)。
JSONP方法
我們理解了JSON方法,理解起JSONP就更加簡單了。
JSONP全稱為JSON with Padding, 用于解決Ajax跨域問題的一種方案。
它的原理非常簡單:
// 客戶端代碼 var dosomething = function(data) { // do something with data } var url = "server.jsonp?callback=dosomething"; // 創(chuàng)建