go語言renderer包代碼分析

renderer是Go語言的一個簡單的、輕量的、快速響應的呈現包,它能夠支持JSON、JSONP、XML、HYAML、HTML、File等類型的響應。在開發web應用或RESTFul API的時候,這個包是很是方便的toolkit。javascript

本文繞開如何使用它,深刻到代碼實現中研究它,同時也嚐嚐Go語言包的開發套路。html

Go包基礎介紹

代碼結構

package pkgname

import (
    "fmt"
    ...
)
const (
    CONST1 typeX = xx
    ...
)

var (
    VAR1 typeX = xxx
    ...
)

func Fn1() {
}

在Go語言中包名和目錄名保持一致,同一包內可共享命名空間。前端

  • 包文件開頭除了註釋外,第一行,必須是package pkgname, 聲明包的名稱。
  • 在包聲明以後,能夠import標準庫中的包和其餘外部包。
  • 而後能夠定義包常量、包變量(暴露變量和非暴露變量,以首字母大小寫來區分實現)。
  • 而後定義自定義類型、函數或方法。

import語句

import能夠引入標準庫的包,也能夠引入外部包。Go語言中一旦引入某個包,必須在程序中使用到這個包的命名空間,不然編譯報錯會告訴你引入了某個包,但代碼中不曾使用。java

固然你也會有疑問,我若是須要引入包,但又不想使用怎麼辦。這個Go語言有一個特殊的符號"_", 放在引入包名前面,就能夠防止編譯報錯。爲何會有這種考慮呢? 由於有時候,咱們只是但願引入一個包,而後執行這個包的一些初始化設置。而後在代碼中暫時不使用該包的任何方法和變量。git

import (
    _ "gitHub.com/xxxxx/pkgname"
)

上面語句會引入pkgname命名空間,可是暫時不在代碼中使用這個命名空間。這樣引入以後,會在pkgname包中尋找init()函數,而後在main()函數執行以前先執行它們,這點對於須要使用包以前作初始化很是有用。github

暴露與非暴露的實現

咱們在其餘編程語言中,都接觸過private, protected, public之類的修飾符。 可是在Go語言中徹底沒有這些,可是Go語言仍是能夠某些東西從包中暴露出去,而某些東西不暴露出去,它用的原則很簡單的,就是標識符若是以小寫字母開頭的,包外不可見; 而若是是標識符以大寫字符開頭的,包外可見,可訪問。web

對於暴露變量和函數(方法)很是直觀簡單,可是若是是暴露的結構體,狀況稍微複雜一點。 不過本質上也是差很少, 結構體外部若是小寫字母開頭,內部屬性大寫字母開頭。 則外部包直接不訪問,但若是經過函數或方法返回這個外部類型,那麼能夠經過:=獲得這個外部類型,從而能夠訪問其內部屬性。舉例以下:ajax

// 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的結構以及包含的屬性名。json

內置類型和自定義類型

Go語言包含了幾種簡單的內置類型:整數、布爾值、數組、字符串、分片、映射等。除了內置類型,Go語言還支持方便的自定義類型。

自定義類型有兩種:

  • 自定義結構體類型: type MyType struct {}這種形式定義,這種相似C語言中的結構體定義。
  • 命名類型: type MyInt int。這種方式經過將內置類型或自定義類型命名爲新的類型的方式來實現。 須要注意MyInt和int是不一樣的類型,它們之間不能直接互相賦值。

函數和方法

Go語言的函數和方法都是使用func關鍵詞聲明的,方法和函數的惟一區別在於,方法須要綁定目標類型; 而函數則無需綁定。

type MyType struct {
}

// 這是函數
func DoSomething() {

}

// 這是方法
func (mt MyType) MyMethod() {
}

// 或者另一種類型的方法
func (mt *MyType) MyMethod2() {
}

對於方法來講,須要綁定一個receiver, 我稱之爲接收者。 接收者有兩種類型:

  • 值類型的接收者
  • 指針類型的接收者

關於接收者和接口部分,有不少須要延伸的,後續有時間整理補充出來。

接口

代碼分析

常量部分

代碼分析部分,咱們先跳過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: 映射類型,描述表明用於發送的響應數據便捷類型。
  • 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函數

// 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
}

用於建立Render類型的函數。它接受Options類型的參數,返回一個Render類型。

咱們通常一般不傳入Options類型變量調用renderer.New()來建立一個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函數,獲得了一個Render類型,接下來就是呈現具體類型的內容。咱們以JSON爲例,看看它怎麼實現的。

JSON方法

若是沒有renderer包,咱們想要用Go語言發送JSON數據響應,咱們的實現代碼大體以下:

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, 而後發送Content-Type爲application/json, 而後發送狀態碼, 最後將json序列發送出去。

那麼咱們再詳細看看renderer中的JSON方法的實現:

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包的實現基本同樣。指定Content-Type, 發送HTTP狀態碼,而後看JSON前綴是否設置,若是設置,前綴也發送到字節流中。 最後就是發送json字節流。

惟一區別在於,咱們使用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字節流。 這個方法其實就是將JSON格式化,使得結果看起來更好看。

另外這個方法還會根據配置,進行html實體的轉義。

所以整體來講原理和開頭的代碼基本同樣。只不過多了一些額外的修飾修補。

JSONP方法

咱們理解了JSON方法,理解起JSONP就更加簡單了。

JSONP全稱爲JSON with Padding, 用於解決Ajax跨域問題的一種方案。

它的原理很是簡單:

// 客戶端代碼
var dosomething = function(data) {
   // do something with data
}

var url = "server.jsonp?callback=dosomething";
  // 建立 <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類型是否是也是這個值?
  • 而後一樣的設置響應狀態碼, 這點沒有什麼特殊的。
  • 將值轉換爲json字節序列。這個json字節序列尚未向響應寫入進去。
  • 這個時候咱們檢查callback是否存在,不存在報錯出去。由於是JSONP, 必需要有callback, 這個callback是請求參數傳入的。
  • 而後用"callbak("和")"將json字節序列包圍起來,一塊兒輸出到響應流中。這樣jsonp響應就產生了。

那麼回過頭結合咱們開頭寫的一個前段jsonp代碼,咱們知道請求了server.jsonp?callback=xxxx以後,一個application/javascript的內容被嵌入到body內。它是js文件。 而其內容將callback替換爲傳入的dosomething, 咱們獲得相似的js內容:

dosomething({
   // ....
});

這樣服務端產生數據,並調用前端js的方法, 傳入這些數據, jsonp就完成了。這樣的js一旦加載成功,它和當前訪問域名是同源的,不存在跨域問題。 這樣就解決了ajax跨域問題。

剩下的其餘方法基本都是一樣的套路, 這裏再也不贅述, 有時間的話再從新整理下開頭的內容。

本文僅我的學習整理, 若有不對之處, 還望各位不吝指出。

在連接部分,有我本身對Go in Action英文書籍的翻譯, 英文比較差,再者也是初學Go語言,翻譯不到位, 有興趣的朋友能夠一塊兒翻譯此書,或者後續有其餘好的技術書籍,一塊兒翻譯學習。

引用連接

相關文章
相關標籤/搜索