浅谈go语言renderer包代码分析
renderer是Go语言的一个简单的、轻量的、快速响应的呈现包,它可以支持JSON、JSONP、XML、HYAML、HTML、File等类型的响应。在开发web应用或RESTFulAPI的时候,这个包是非常方便的toolkit。
本文绕开如何使用它,深入到代码实现中研究它,同时也尝尝Go语言包的开发套路。
Go包基础介绍
代码结构
packagepkgname import( "fmt" ... ) const( CONST1typeX=xx ... ) var( VAR1typeX=xxx ... ) funcFn1(){ }
在Go语言中包名和目录名保持一致,同一包内可共享命名空间。
- 包文件开头除了注释外,第一行,必须是packagepkgname,声明包的名称。
- 在包声明之后,可以import标准库中的包和其他外部包。
- 然后可以定义包常量、包变量(暴露变量和非暴露变量,以首字母大小写来区分实现)。
- 然后定义自定义类型、函数或方法。
import语句
import可以引入标准库的包,也可以引入外部包。Go语言中一旦引入某个包,必须在程序中使用到这个包的命名空间,否则编译报错会告诉你引入了某个包,但代码中未曾使用。
当然你也会有疑问,我如果需要引入包,但又不想使用怎么办。这个Go语言有一个特殊的符号"_",放在引入包名前面,就可以防止编译报错。为什么会有这种考虑呢?因为有时候,我们只是希望引入一个包,然后执行这个包的一些初始化设置。然后在代码中暂时不使用该包的任何方法和变量。
import( _"gitHub.com/xxxxx/pkgname" )
上面语句会引入pkgname命名空间,但是暂时不在代码中使用这个命名空间。这样引入之后,会在pkgname包中寻找init()函数,然后在main()函数执行之前先执行它们,这点对于需要使用包之前做初始化非常有用。
暴露与非暴露的实现
我们在其他编程语言中,都接触过private,protected,public之类的修饰符。但是在Go语言中完全没有这些,但是Go语言还是可以某些东西从包中暴露出去,而某些东西不暴露出去,它用的原则很简单的,就是标识符如果以小写字母开头的,包外不可见;而如果是标识符以大写字符开头的,包外可见,可访问。
对于暴露变量和函数(方法)非常直观简单,但是如果是暴露的结构体,情况稍微复杂一点。不过本质上也是差不多,结构体外部如果小写字母开头,内部属性大写字母开头。则外部包直接不访问,但如果通过函数或方法返回这个外部类型,那么可以通过:=得到这个外部类型,从而可以访问其内部属性。举例如下:
//packagepkgname packagepkgname typeadminstruct{ Namestring EmailString } funcAdmin()*admin{ return&admin{ Name:"admin", Email:"admin@email.com", } }
那么我们在外部包中,可以直接通过下面代码访问admin结构体内部的属性:
admin:=pkgname.Admin() fmt.Println(admin.Name,admin.Email)
当然这种情况下,需要你事先知道admin的结构以及包含的属性名。
内置类型和自定义类型
Go语言包含了几种简单的内置类型:整数、布尔值、数组、字符串、分片、映射等。除了内置类型,Go语言还支持方便的自定义类型。
自定义类型有两种:
- 自定义结构体类型:typeMyTypestruct{}这种形式定义,这种类似C语言中的结构体定义。
- 命名类型:typeMyIntint。这种方式通过将内置类型或自定义类型命名为新的类型的方式来实现。需要注意MyInt和int是不同的类型,它们之间不能直接互相赋值。
函数和方法
Go语言的函数和方法都是使用func关键词声明的,方法和函数的唯一区别在于,方法需要绑定目标类型;而函数则无需绑定。
typeMyTypestruct{ } //这是函数 funcDoSomething(){ } //这是方法 func(mtMyType)MyMethod(){ } //或者另外一种类型的方法 func(mt*MyType)MyMethod2(){ }
对于方法来说,需要绑定一个receiver,我称之为接收者。接收者有两种类型:
- 值类型的接收者
- 指针类型的接收者
关于接收者和接口部分,有很多需要延伸的,后续有时间整理补充出来。
接口
代码分析
常量部分
代码分析部分,我们先跳过import部分,直接进入到常量的声明部分。
const( //ContentTyperepresentscontenttype ContentTypestring="Content-Type" //ContentJSONrepresentscontenttypeapplication/json ContentJSONstring="application/json" //ContentJSONPrepresentscontenttypeapplication/javascript ContentJSONPstring="application/javascript" //ContentXMLrepresentscontenttypeapplication/xml ContentXMLstring="application/xml" //ContentYAMLrepresentscontenttypeapplication/x-yaml ContentYAMLstring="application/x-yaml" //ContentHTMLrepresentscontenttypetext/html ContentHTMLstring="text/html" //ContentTextrepresentscontenttypetext/plain ContentTextstring="text/plain" //ContentBinaryrepresentscontenttypeapplication/octet-stream ContentBinarystring="application/octet-stream" //ContentDispositiondescribescontentDisposition ContentDispositionstring="Content-Disposition" //contentDispositionInlinedescribescontentdispositiontype contentDispositionInlinestring="inline" //contentDispositionAttachmentdescribescontentdispositiontype contentDispositionAttachmentstring="attachment" defaultCharSetstring="utf-8" defaultJSONPrefixstring="" defaultXMLPrefixstring=`\n` defaultTemplateExtstring="tpl" defaultLayoutExtstring="lout" defaultTemplateLeftDelimstring="{{" defaultTemplateRightDelimstring="}}" )
以上常量声明了内容类型常量以及本包支持的各种内容类型MIME值。以及各种具体内容类型相关的常量,比如字符编码方式、JSONP前缀、XML前缀,模版左右分割符等等一些常量。
类型声明部分
这部分声明了如下类型:
- M:映射类型,描述代表用于发送的响应数据便捷类型。
- Options:描述选项类型。
- Render:用于描述renderer类型。
type( //Mdescribeshandytypethatrepresentsdatatosendasresponse Mmap[string]interface{} //Optionsdescribesanoptiontype Optionsstruct{ //CharsetrepresentstheResponsecharset;default:utf-8 Charsetstring //ContentJSONrepresentstheContent-TypeforJSON ContentJSONstring //ContentJSONPrepresentstheContent-TypeforJSONP ContentJSONPstring //ContentXMLrepresentstheContent-TypeforXML ContentXMLstring //ContentYAMLrepresentstheContent-TypeforYAML ContentYAMLstring //ContentHTMLrepresentstheContent-TypeforHTML ContentHTMLstring //ContentTextrepresentstheContent-TypeforText ContentTextstring //ContentBinaryrepresentstheContent-Typeforoctet-stream ContentBinarystring //UnEscapeHTMLsetUnEscapeHTMLforJSON;defaultfalse UnEscapeHTMLbool //DisableCharsetsetDisableCharsetinResponseContent-Type DisableCharsetbool //Debugsetthedebugmode.ifdebugistruetheneverytime"VIEW"callparsethetemplates Debugbool //JSONIndentsetJSONIndentinresponse;defaultfalse JSONIndentbool //XMLIndentsetXMLIndentinresponse;defaultfalse XMLIndentbool //JSONPrefixsetPrefixinJSONresponse JSONPrefixstring //XMLPrefixsetPrefixinXMLresponse XMLPrefixstring //TemplateDirsettheTemplatedirectory TemplateDirstring //TemplateExtensionsettheTemplateextension TemplateExtensionstring //LeftDelimsettemplateleftdelimiterdefaultis{{ LeftDelimstring //RightDelimsettemplaterightdelimiterdefaultis}} RightDelimstring //LayoutExtensionsettheLayoutextension LayoutExtensionstring //FuncMapcontainfunctionmapfortemplate FuncMap[]template.FuncMap //ParseGlobPatterncontainparseglobpattern ParseGlobPatternstring } //Renderdescribesarenderertype Renderstruct{ optsOptions templatesmap[string]*template.Template globTemplates*template.Template headersmap[string]string } )
New函数
//NewreturnanewinstanceofapointertoRender funcNew(opts...Options)*Render{ varoptOptions ifopts!=nil{ opt=opts[0] } r:=&Render{ opts:opt, templates:make(map[string]*template.Template), } //buildoptionsfortheRenderinstance r.buildOptions() //ifTemplateDirisnotemptythencalltheparseTemplates ifr.opts.TemplateDir!=""{ r.parseTemplates() } //ParseGlobPatternisnotemptythenparsetemplatewithpattern ifr.opts.ParseGlobPattern!=""{ r.parseGlob() } returnr }
用于创建Render类型的函数。它接受Options类型的参数,返回一个Render类型。
我们一般通常不传入Options类型变量调用renderer.New()来创建一个Render类型。
varoptOptions ifopts!=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(){ ifr.opts.Charset==""{//没有指定编码方式,使用默认的编码方式UTF-8 r.opts.Charset=defaultCharSet } ifr.opts.JSONPrefix==""{//没有指定JSON前缀,使用默认的 r.opts.JSONPrefix=defaultJSONPrefix } ifr.opts.XMLPrefix==""{//没有指定XML前缀,使用默认XML前缀 r.opts.XMLPrefix=defaultXMLPrefix } ifr.opts.TemplateExtension==""{//模版扩展名设置 r.opts.TemplateExtension="."+defaultTemplateExt }else{ r.opts.TemplateExtension="."+r.opts.TemplateExtension } ifr.opts.LayoutExtension==""{//布局扩展名设置 r.opts.LayoutExtension="."+defaultLayoutExt }else{ r.opts.LayoutExtension="."+r.opts.LayoutExtension } ifr.opts.LeftDelim==""{//模版变量左分割符设置 r.opts.LeftDelim=defaultTemplateLeftDelim } ifr.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数据响应,我们的实现代码大致如下:
funcDoSomething(whttp.ResponseWriter,...){ //jsonfromavariablev jData,err:=json.Marshal(v) iferr!=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(whttp.ResponseWriter,statusint,vinterface{})error{ w.Header().Set(ContentType,r.opts.ContentJSON) w.WriteHeader(status) bs,err:=r.json(v) iferr!=nil{ returnerr } ifr.opts.JSONPrefix!=""{ w.Write([]byte(r.opts.JSONPrefix)) } _,err=w.Write(bs) returnerr }
大致看上去,和我们不使用renderer包的实现基本一样。指定Content-Type,发送HTTP状态码,然后看JSON前缀是否设置,如果设置,前缀也发送到字节流中。最后就是发送json字节流。
唯一区别在于,我们使用encoding/json包的Marshal方法来将给定的值转换成二进制序列,而renderer对这个方法进行了包装:
func(r*Render)json(vinterface{})([]byte,error){ varbs[]byte varerrerror ifr.opts.JSONIndent{ bs,err=json.MarshalIndent(v,"","") }else{ bs,err=json.Marshal(v) } iferr!=nil{ returnbs,err } ifr.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) } returnbs,nil }
如果有设置JSONIndent,即JSON缩进,那么使用json.MarshalIndent来将变量转换为json字节流。这个方法其实就是将JSON格式化,使得结果看起来更好看。
另外这个方法还会根据配置,进行html实体的转义。
因此总体来说原理和开头的代码基本一样。只不过多了一些额外的修饰修补。
JSONP方法
我们理解了JSON方法,理解起JSONP就更加简单了。
JSONP全称为JSONwithPadding,用于解决Ajax跨域问题的一种方案。
它的原理非常简单:
//客户端代码 vardosomething=function(data){ //dosomethingwithdata } varurl="server.jsonp?callback=dosomething"; //创建