浅谈go语言renderer包代码分析

renderer是Go语言的一个简单的、轻量的、快速响应的呈现包,它可以支持JSON、JSONP、XML、HYAML、HTML、File等类型的响应。在开发web应用或RESTFul API的时候,这个包是非常方便的toolkit。

本文绕开如何使用它,深入到代码实现中研究它,同时也尝尝Go语言包的开发套路。

Go包基础介绍

代码结构

package pkgname

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

var (
  VAR1 typeX = xxx
  ...
)

func Fn1() {
}

在Go语言中包名和目录名保持一致,同一包内可共享命名空间。

  1. 包文件开头除了注释外,第一行,必须是package pkgname, 声明包的名称。
  2. 在包声明之后,可以import标准库中的包和其他外部包。
  3. 然后可以定义包常量、包变量(暴露变量和非暴露变量,以首字母大小写来区分实现)。
  4. 然后定义自定义类型、函数或方法。

import语句

import可以引入标准库的包,也可以引入外部包。Go语言中一旦引入某个包,必须在程序中使用到这个包的命名空间,否则编译报错会告诉你引入了某个包,但代码中未曾使用。

当然你也会有疑问,我如果需要引入包,但又不想使用怎么办。这个Go语言有一个特殊的符号"_", 放在引入包名前面,就可以防止编译报错。为什么会有这种考虑呢? 因为有时候,我们只是希望引入一个包,然后执行这个包的一些初始化设置。然后在代码中暂时不使用该包的任何方法和变量。

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

上面语句会引入pkgname命名空间,但是暂时不在代码中使用这个命名空间。这样引入之后,会在pkgname包中寻找init()函数,然后在main()函数执行之前先执行它们,这点对于需要使用包之前做初始化非常有用。

暴露与非暴露的实现

我们在其他编程语言中,都接触过private, protected, public之类的修饰符。 但是在Go语言中完全没有这些,但是Go语言还是可以某些东西从包中暴露出去,而某些东西不暴露出去,它用的原则很简单的,就是标识符如果以小写字母开头的,包外不可见; 而如果是标识符以大写字符开头的,包外可见,可访问。

对于暴露变量和函数(方法)非常直观简单,但是如果是暴露的结构体,情况稍微复杂一点。 不过本质上也是差不多, 结构体外部如果小写字母开头,内部属性大写字母开头。 则外部包直接不访问,但如果通过函数或方法返回这个外部类型,那么可以通过:=得到这个外部类型,从而可以访问其内部属性。举例如下:

// 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语言包含了几种简单的内置类型:整数、布尔值、数组、字符串、分片、映射等。除了内置类型,Go语言还支持方便的自定义类型。

自定义类型有两种:

  1. 自定义结构体类型: type MyType struct {}这种形式定义,这种类似C语言中的结构体定义。
  2. 命名类型: type MyInt int。这种方式通过将内置类型或自定义类型命名为新的类型的方式来实现。 需要注意MyInt和int是不同的类型,它们之间不能直接互相赋值。

函数和方法

Go语言的函数和方法都是使用func关键词声明的,方法和函数的唯一区别在于,方法需要绑定目标类型; 而函数则无需绑定。

type MyType struct {
}

// 这是函数
func DoSomething() {

}

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

// 或者另外一种类型的方法
func (mt *MyType) MyMethod2() {
}

对于方法来说,需要绑定一个receiver, 我称之为接收者。 接收者有两种类型:

  1. 值类型的接收者
  2. 指针类型的接收者

关于接收者和接口部分,有很多需要延伸的,后续有时间整理补充出来。

接口

代码分析

常量部分

代码分析部分,我们先跳过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前缀,模版左右分割符等等一些常量。

类型声明部分

这部分声明了如下类型:

  1. M: 映射类型,描述代表用于发送的响应数据便捷类型。
  2. Options: 描述选项类型。
  3. 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
}
  1. 设置Content-Type为application/javascript, 非常关键的一点。 想一想html中嵌入的js文件的mime类型是不是也是这个值?
  2. 然后同样的设置响应状态码, 这点没有什么特殊的。
  3. 将值转换为json字节序列。这个json字节序列还没有向响应写入进去。
  4. 这个时候我们检查callback是否存在,不存在报错出去。因为是JSONP, 必须要有callback, 这个callback是请求参数传入的。
  5. 然后用"callbak("和")"将json字节序列包围起来,一起输出到响应流中。这样jsonp响应就产生了。

那么回过头结合我们开头写的一个前段jsonp代码,我们知道请求了server.jsonp?callback=xxxx之后,一个application/javascript的内容被嵌入到body内。它是js文件。 而其内容将callback替换为传入的dosomething, 我们得到类似的js内容:

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

这样服务端产生数据,并调用前端js的方法, 传入这些数据, jsonp就完成了。这样的js一旦加载成功,它和当前访问域名是同源的,不存在跨域问题。 这样就解决了ajax跨域问题。

剩下的其他方法基本都是同样的套路, 这里不再赘述, 有时间的话再重新整理下开头的内容。

本文仅个人学习整理, 如有不对之处, 还望各位不吝指出。

在链接部分,有我自己对Go in Action英文书籍的翻译, 英文比较差,再者也是初学Go语言,翻译不到位, 有兴趣的朋友可以一起翻译此书,或者后续有其他好的技术书籍,一起翻译学习。

引用链接

  1. renderer
  2. Go In Action

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 浅谈go语言renderer包代码分析

    renderer是Go语言的一个简单的.轻量的.快速响应的呈现包,它可以支持JSON.JSONP.XML.HYAML.HTML.File等类型的响应.在开发web应用或RESTFul API的时候,这个包是非常方便的toolkit. 本文绕开如何使用它,深入到代码实现中研究它,同时也尝尝Go语言包的开发套路. Go包基础介绍 代码结构 package pkgname import ( "fmt" ... ) const ( CONST1 typeX = xx ... ) var ( V

  • 浅谈Go语言中的结构体struct & 接口Interface & 反射

    结构体struct struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套: go中的struct类型理解为类,可以定义方法,和函数定义有些许区别: struct类型是值类型. struct定义 type User struct { Name string Age int32 mess string } var user User var user1 *User = &User{} var user2 *User = new(User) struct使用 下面示例中user1和

  • 浅谈C语言转义字符和格式控制符

    转义字符参考: \a:蜂鸣,响铃 \b:回退:向后退一格 \f:换页 \n:换行,光标到下行行首 \r:回车,光标到本行行首 \t:水平制表 \v:垂直制表 \\:反斜杠 \':单引号 \":双引号 \?:问号 \ddd:三位八进制 \xhh:二位十六进制 \0:空字符(NULL),什么都不做 注: 1,\v垂直制表和\f换页符对屏幕没有任何影响,但会影响打印机执行响应操作. 2,\n其实应该叫回车换行.换行只是换一行,不改变光标的横坐标:回车只是回到行首,不改变光标的纵坐标. 3,\t 光标向

  • 浅谈C语言函数调用参数压栈的相关问题

    参数入栈的顺序 以前在面试中被人问到这样的问题,函数调用的时候,参数入栈的顺序是从左向右,还是从右向左.参数的入栈顺序主要看调用方式,一般来说,__cdecl 和__stdcall 都是参数从右到左入栈. 看下面的代码: #include <stdio.h> int test(int a, int b) { printf("address of a %x.\n", &a); printf("address of b %x.\n", &b)

  • 浅谈c语言中转义字符的用法及注意事项

    c语言中的转义字符: \a 响铃符 \b 退格 \f 换页符 \n 换行符 \r 回车符(回到该行的首位置) \v 纵向制表符 \\ 反斜杠 \? 问号(?经vs10测试可以直接打印) \"(\') 双引号(单引号) \ooo 八进制数(ooo表示一个用8进制数表示出来的对应ANSII代码对应出字符,用此方法可以表示出所有ASCII字符.不过测试发现打不出%号,存疑!) \xhh 十六进制数(功能同八进制数,用hh表示一个十六进制数,如\x20表示空格) 注:使用转义字符的退格符,换行符,回车符

  • 浅谈C语言的字节对齐 #pragma pack(n)2

    #pragma pack(n) 这是给编译器用的参数设置,有关结构体字节对齐方式设置, #pragma pack是指定数据在内存中的对齐方式. #pragma pack (n)             作用:C编译器将按照n个字节对齐. #pragma pack ()               作用:取消自定义字节对齐方式. #pragma  pack (push,1)     作用:是指把原来对齐方式设置压栈,并设新的对齐方式设置为一个字节对齐 #pragma pack(pop)      

  • 浅谈maven的jar包和war包区别 以及打包方法

    jar文件包括java普通类.资源文件和普通文件,在maven中即是打包src/main/java和src/main/resources资源文件夹下的所有文件.在打包的时候会自动生成MATA-INF文件夹,用于存储maven的pom信息和MANIFEST.MF文件.例如: war文件包含全部的web应用程序,即所有的java类,配置信息和jsp.js等静态资源.但是需要注意war引用war的时候会将应用war的资源全部拷贝到当前war的相同文件下,重名的文件会被替换.例如: war包依赖: <d

  • 浅谈Spring解决jar包依赖的bom

    名词解释: BOM(bill of materials):材料清单,用于解决jar包依赖的好方法. Spring IO Platform 缘起:Spring起初只专注ioc和aop,现在已发展成一个庞大体系.比如security.mvc等.如此一来,不同模块或者与外部进行集成时,依赖处理就需要各自对应版本号.比如,较新spring与较老的quartz,它们集成就会遇到问题,给搭建和升级带来不便.因此Spring IO Platform应运而生,只要项目中引入了它,外部集成时依赖关系无需版本号.官

  • 浅谈C语言中的注释风格小结

    C语言中常用的注释风格有两种,一种是通过如下模式进行一段代码的注释: /* comment*/ 另一种是单行注释符号: // comment 学生时代的注释我一般是选用后者,那时候编码量十分有限,即使是简单的小段落注释使用的IDE也支持批量添加单行注释符.而在编码之中,简单的单行注释进行注释的时候键盘的操作更为简单一点. 不过,工作之后接触了相应的编码规范之后,C语言的注释我基本上放弃了单行注释的方法,最多仅仅在调试的时候做简单的使用. 其实,单行注释是从C++中借鉴来的,算是C++风格的注释方

  • 浅谈vue的第一个commit分析

    为什么写这篇vue的分析文章? 对于天资愚钝的前端(我)来说,阅读源码是件不容易的事情,毕竟有时候看源码分析的文章都看不懂.每次看到大佬们用了1-2年的vue就能掌握原理,甚至精通源码,再看看自己用了好几年都还在基本的使用阶段,心中总是羞愧不已.如果一直满足于基本的业务开发,怕是得在初级水平一直待下去了吧.所以希望在学习源码的同时记录知识点,可以让自己的理解和记忆更加深刻,也方便将来查阅. 目录结构 本文以vue的第一次 commit a879ec06 作为分析版本 ├── build │ └─

随机推荐