golang: Martini之inject源码分析

2024-05-06 17:32

本文主要是介绍golang: Martini之inject源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

   转自:http://my.oschina.net/goal/blog/195036 

   ps: martini类似nodej express。 对于inject的了解学习推荐《Go 学习笔记 第三版 — 雨痕》《Go语言编程 — 许式伟等》相关章节

   依赖注入(Dependency Injection)和控制反转(Inversion of Control)是同一个概念。在传统的程序设计过程中,调用者是自己来决定使用哪些被调用者实现的。但是在依赖注入模式中,创建被调用者的工作不再由调用者来完成,因此称为控制反转;创建被调用者实例的工作通常由注入器来完成,然后注入调用者,因此也称为依赖注入。

inject 是依赖注入的golang实现,作者是 codegangsta 。它能在运行时注入参数,调用方法。是Martini框架的基础核心。

我对依赖注入提取了以下2点性质:

  1. 由注入器注入属性。

  2. 由注入器创建被调用者实例。

在inject中,被调用者为func,因此注入属性也即对func注入实参(当然inject也可以注入struct,这样的话注入的属性就是struct中的已添加tag为`inject`的导出字段)。我们来看下普通的函数调用:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
     "fmt"
)
func Say(name, gender string, age  int ) {
     fmt.Printf( "My name is %s, gender is %s, age is %d!\n" , name, gender, age)
}
func main() {
     Say( "陈一回" "男" , 20)
}

上面的例子中,定义了函数Say并在main方法中手动调用。这样总是可行的,但是有时候我们不得不面对这样一种情况:比如在web开发中,我们注册路由,服务器接受请求,然后根据request path调用相应的handler。这个handler必然不是由我们手动来调用的,而是由服务器端根据路由匹配来查找对应的handler并自动调用。

是时候引入inject了,尝试用inject改写上面的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
     "fmt"
     "github.com/codegangsta/inject"
)
type SpecialString interface{}
func Say(name string, gender SpecialString, age  int ) {
     fmt.Printf( "My name is %s, gender is %s, age is %d!\n" , name, gender, age)
}
func main() {
     inj := inject.New()
     inj.Map( "陈一回" )
     inj.MapTo( "男" , (*SpecialString)(nil))
     inj.Map(20)
     inj.Invoke(Say)
}

?
1
2
3
4
cd  $GOPATH /src/injector_test
$ go build
$ . /injector_test
My name is 陈一回, gender is 男, age is 20!

看不懂?没关系,因为我们对于inject还没有足够的知识储备,一切从分析inject的源码开始。

inject包只有2个文件,一个是inject.go文件,还有一个是inject_test.go,但我们只关注inject.go文件。

inject.go短小精悍,包括注释和空行才157行。定义了4个接口,包括一个父接口和三个子接口,接下来您就会知道这样定义的好处了。

为了方便,我把所有的注释都去掉了:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Injector interface {
     Applicator
     Invoker
     TypeMapper
     SetParent(Injector)
}
type Applicator interface {
     Apply(interface{}) error
}
type Invoker interface {
     Invoke(interface{}) ([]reflect.Value, error)
}
type TypeMapper interface {
     Map(interface{}) TypeMapper
     MapTo(interface{}, interface{}) TypeMapper
     Get(reflect.Type) reflect.Value
}

接口Injector是接口Applicator、接口Invoker、接口TypeMapper的父接口,所以实现了Injector接口的类型,也必然实现了Applicator接口、Invoker接口和TypeMapper接口。

Applicator接口只规定了Apply成员,它用于注入struct。

Invoker接口只规定了Invoke成员,它用于执行被调用者。

TypeMapper接口规定了三个成员,Map和MapTo都用于注入参数,但它们有不同的用法。Get用于调用时获取被注入的参数。

另外Injector还规定了SetParent行为,它用于设置父Injector,其实它相当于查找继承。也即通过Get方法在获取被注入参数时会一直追溯到parent,这是个递归过程,直到查找到参数或为nil终止。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type injector  struct  {
     values map[reflect.Type]reflect.Value
     parent Injector
}
func InterfaceOf(value interface{}) reflect.Type {
     t := reflect.TypeOf(value)
     for  t.Kind() == reflect.Ptr {
         t = t.Elem()
     }
     if  t.Kind() != reflect.Interface {
         panic( "Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)" )
     }
     return  t
}
func New() Injector {
     return  &injector{
         values: make(map[reflect.Type]reflect.Value),
     }
}

injector是inject包中唯一定义的struct,所有的操作都是基于injector struct来进行的。它有两个成员values和parent。values用于保存注入的参数,它是一个用reflect.Type当键、reflect.Value为值的map,这个很重要,理解这点将有助于理解Map和MapTo。New方法用于初始化injector struct,并返回一个指向injector struct的指针。但是请注意这个返回值被Injector接口包装了。

InterfaceOf方法虽然只有几句实现代码,但它是Injector的核心。InterfaceOf方法的参数必须是一个接口类型的指针,如果不是则引发panic。InterfaceOf方法的返回类型是reflect.Type,您应该还记得injector的成员values就是一个reflect.Type类型当键的map。这个方法的作用其实只是获取参数的类型,而不关心它的值。我之前有篇文章介绍过(*interface{})(nil),感兴趣的朋友可以去看看:golang: 详解interface和nil 。

为了加深理解,来举个例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
     "fmt"
     "github.com/codegangsta/inject"
)
type SpecialString interface{}
func main() {
     fmt.Println(inject.InterfaceOf((*interface{})(nil)))
     fmt.Println(inject.InterfaceOf((*SpecialString)(nil)))
}

?
1
2
3
4
5
cd  $GOPATH /src/injector_test
$ go build
$ . /injector_test
interface {}
main.SpecialString

上面的输出一点也不奇怪。InterfaceOf方法就是用来得到参数类型,而不关心它具体存储的是什么值。值得一提的是,我们定义了一个SpecialString接口。我们在之前的代码也有定义SpecialString接口,用在Say方法的参数声明中,之后您就会知道为什么要这么做。当然您不一定非得命名为SpecialString。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (i *injector) Map(val interface{}) TypeMapper {
     i.values[reflect.TypeOf(val)] = reflect.ValueOf(val)
     return  i
}
func (i *injector) MapTo(val interface{}, ifacePtr interface{}) TypeMapper {
     i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)
     return  i
}
func (i *injector) Get(t reflect.Type) reflect.Value {
     val := i.values[t]
     if  !val.IsValid() && i.parent != nil {
         val = i.parent.Get(t)
     }
     return  val
}
func (i *injector) SetParent(parent Injector) {
     i.parent = parent
}

Map和MapTo方法都用于注入参数,保存于injector的成员values中。这两个方法的功能完全相同,唯一的区别就是Map方法用参数值本身的类型当键,而MapTo方法有一个额外的参数可以指定特定的类型当键。但是MapTo方法的第二个参数ifacePtr必须是接口指针类型,因为最终ifacePtr会作为InterfaceOf方法的参数。

为什么需要有MapTo方法?因为注入的参数是存储在一个以类型为键的map中,可想而知,当一个函数中有一个以上的参数的类型是一样时,后执行Map进行注入的参数将会覆盖前一个通过Map注入的参数。

SetParent方法用于给某个Injector指定父Injector。Get方法通过reflect.Type从injector的values成员中取出对应的值,它可能会检查是否设置了parent,直到找到或返回无效的值,最后Get方法的返回值会经过IsValid方法的校验。举个例子来加深理解:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
import (
     "fmt"
     "github.com/codegangsta/inject"
     "reflect"
)
type SpecialString interface{}
func main() {
     inj := inject.New()
     inj.Map( "陈一回" )
     inj.MapTo( "男" , (*SpecialString)(nil))
     inj.Map(20)
     fmt.Println( "string is valid?" , inj.Get(reflect.TypeOf( "姓陈名一回" )).IsValid())
     fmt.Println( "SpecialString is valid?" , inj.Get(inject.InterfaceOf((*SpecialString)(nil))).IsValid())
     fmt.Println( "int is valid?" , inj.Get(reflect.TypeOf(18)).IsValid())
     fmt.Println( "[]byte is valid?" , inj.Get(reflect.TypeOf([]byte( "Golang" ))).IsValid())
     inj2 := inject.New()
     inj2.Map([]byte( "test" ))
     inj.SetParent(inj2)
     fmt.Println( "[]byte is valid?" , inj.Get(reflect.TypeOf([]byte( "Golang" ))).IsValid())
}

?
1
2
3
4
5
6
7
8
cd  $GOPATH /src/injector_test
$ go build
$ . /injector_test
string is valid?  true
SpecialString is valid?  true
int is valid?  true
[]byte is valid?  false
[]byte is valid?  true

通过以上例子应该知道SetParent是什么样的行为。是不是很像面向对象中的查找链?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (inj *injector) Invoke(f interface{}) ([]reflect.Value, error) {
     t := reflect.TypeOf(f)
     var in = make([]reflect.Value, t.NumIn())  //Panic if t is not kind of Func
     for  i := 0; i < t.NumIn(); i++ {
         argType := t.In(i)
         val := inj.Get(argType)
         if  !val.IsValid() {
             return  nil, fmt.Errorf( "Value not found for type %v" , argType)
         }
         in[i] = val
     }
     return  reflect.ValueOf(f).Call(in), nil
}

Invoke方法用于动态执行函数,当然执行前可以通过Map或MapTo来注入参数,因为通过Invoke执行的函数会取出已注入的参数,然后通过reflect包中的Call方法来调用。Invoke接收的参数f是一个接口类型,但是f的底层类型必须为func,否则会panic。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
     "fmt"
     "github.com/codegangsta/inject"
)
type SpecialString interface{}
func Say(name string, gender SpecialString, age  int ) {
     fmt.Printf( "My name is %s, gender is %s, age is %d!\n" , name, gender, age)
}
func main() {
     inj := inject.New()
     inj.Map( "陈一回" )
     inj.MapTo( "男" , (*SpecialString)(nil))
     inj2 := inject.New()
     inj2.Map(20)
     inj.SetParent(inj2)
     inj.Invoke(Say)
}

上面的例子如果没有定义SpecialString接口作为gender参数的类型,而把name和gender都定义为string类型,那么gender会覆盖name的值。如果您还没有明白,建议您把这篇文章从头到尾再看几遍。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func (inj *injector) Apply(val interface{}) error {
     v := reflect.ValueOf(val)
     for  v.Kind() == reflect.Ptr {
         v = v.Elem()
     }
     if  v.Kind() != reflect.Struct {
         return  nil
     }
     t := v.Type()
     for  i := 0; i < v.NumField(); i++ {
         f := v.Field(i)
         structField := t.Field(i)
         if  f.CanSet() && structField.Tag ==  "inject"  {
             ft := f.Type()
             v := inj.Get(ft)
             if  !v.IsValid() {
                 return  fmt.Errorf( "Value not found for type %v" , ft)
             }
             f.Set(v)
         }
     }
     return  nil
}

Apply方法是用于对struct的字段进行注入,参数为指向底层类型为结构体的指针。可注入的前提是:字段必须是导出的(也即字段名以大写字母开头),并且此字段的tag设置为`inject`。以例子来说明:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main
import (
     "fmt"
     "github.com/codegangsta/inject"
)
type SpecialString interface{}
type TestStruct  struct  {
     Name   string `inject`
     Nick   []byte
     Gender SpecialString `inject`
     uid     int            `inject`
     Age     int            `inject`
}
func main() {
     s := TestStruct{}
     inj := inject.New()
     inj.Map( "陈一回" )
     inj.MapTo( "男" , (*SpecialString)(nil))
     inj2 := inject.New()
     inj2.Map(20)
     inj.SetParent(inj2)
     inj.Apply(&s)
     fmt.Println( "s.Name =" , s.Name)
     fmt.Println( "s.Gender =" , s.Gender)
     fmt.Println( "s.Age =" , s.Age)
}

?
1
2
3
4
5
6
cd  $GOPATH /src/injector_test
$ go build
$ . /injector_test
s.Name = 陈一回
s.Gender = 男
s.Age = 20

刑星写了一篇博文可供参考:在Golang中用名字调用函数 ,建议大家都去看下。


这篇关于golang: Martini之inject源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/964915

相关文章

MyBatis Plus 中 update_time 字段自动填充失效的原因分析及解决方案(最新整理)

《MyBatisPlus中update_time字段自动填充失效的原因分析及解决方案(最新整理)》在使用MyBatisPlus时,通常我们会在数据库表中设置create_time和update... 目录前言一、问题现象二、原因分析三、总结:常见原因与解决方法对照表四、推荐写法前言在使用 MyBATis

Python主动抛出异常的各种用法和场景分析

《Python主动抛出异常的各种用法和场景分析》在Python中,我们不仅可以捕获和处理异常,还可以主动抛出异常,也就是以类的方式自定义错误的类型和提示信息,这在编程中非常有用,下面我将详细解释主动抛... 目录一、为什么要主动抛出异常?二、基本语法:raise关键字基本示例三、raise的多种用法1. 抛

github打不开的问题分析及解决

《github打不开的问题分析及解决》:本文主要介绍github打不开的问题分析及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、找到github.com域名解析的ip地址二、找到github.global.ssl.fastly.net网址解析的ip地址三

Golang 日志处理和正则处理的操作方法

《Golang日志处理和正则处理的操作方法》:本文主要介绍Golang日志处理和正则处理的操作方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考... 目录1、logx日志处理1.1、logx简介1.2、日志初始化与配置1.3、常用方法1.4、配合defer

Mysql的主从同步/复制的原理分析

《Mysql的主从同步/复制的原理分析》:本文主要介绍Mysql的主从同步/复制的原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录为什么要主从同步?mysql主从同步架构有哪些?Mysql主从复制的原理/整体流程级联复制架构为什么好?Mysql主从复制注意

java -jar命令运行 jar包时运行外部依赖jar包的场景分析

《java-jar命令运行jar包时运行外部依赖jar包的场景分析》:本文主要介绍java-jar命令运行jar包时运行外部依赖jar包的场景分析,本文给大家介绍的非常详细,对大家的学习或工作... 目录Java -jar命令运行 jar包时如何运行外部依赖jar包场景:解决:方法一、启动参数添加: -Xb

Apache 高级配置实战之从连接保持到日志分析的完整指南

《Apache高级配置实战之从连接保持到日志分析的完整指南》本文带你从连接保持优化开始,一路走到访问控制和日志管理,最后用AWStats来分析网站数据,对Apache配置日志分析相关知识感兴趣的朋友... 目录Apache 高级配置实战:从连接保持到日志分析的完整指南前言 一、Apache 连接保持 - 性

Linux中的more 和 less区别对比分析

《Linux中的more和less区别对比分析》在Linux/Unix系统中,more和less都是用于分页查看文本文件的命令,但less是more的增强版,功能更强大,:本文主要介绍Linu... 目录1. 基础功能对比2. 常用操作对比less 的操作3. 实际使用示例4. 为什么推荐 less?5.

spring-gateway filters添加自定义过滤器实现流程分析(可插拔)

《spring-gatewayfilters添加自定义过滤器实现流程分析(可插拔)》:本文主要介绍spring-gatewayfilters添加自定义过滤器实现流程分析(可插拔),本文通过实例图... 目录需求背景需求拆解设计流程及作用域逻辑处理代码逻辑需求背景公司要求,通过公司网络代理访问的请求需要做请

Java集成Onlyoffice的示例代码及场景分析

《Java集成Onlyoffice的示例代码及场景分析》:本文主要介绍Java集成Onlyoffice的示例代码及场景分析,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要... 需求场景:实现文档的在线编辑,团队协作总结:两个接口 + 前端页面 + 配置项接口1:一个接口,将o