golang反射框架Fx

一、概述

Fx是一个golang版本的依赖注入框架,它使得golang通过可重用、可组合的模块化来构建golang应用程序变得非常容易,可直接在项目中添加以下内容即可体验Fx效果。

import "github.com/uber-go/fx"

Fx是通过使用依赖注入的方式替换了全局通过手动方式来连接不同函数调用的复杂度,也不同于其他的依赖注入方式,Fx能够像普通golang函数去使用,而不需要通过使用struct标签或内嵌特定类型。这样使得Fx能够在很多go的包中很好的使用。
接下来会提供一些Fx的简单demo,并说明其中的一些定义。

生命周期

二、Fx应用

1、一般步骤

  • 按需定义一些构造函数:主要用于生成依赖注入的具体类型
type FxDemo struct{
 // 字段是可导出的,是由于golang的reflection特性决定: 必须能够导出且可寻址才能进行设置
  Name string  
}
func NewFxDemo(){
  return FxDemo{
    Name: "hello, world",
  }
}
  • 、使用Provide将具体反射的类型添加到container中
    可以按需添加任意多个构造函数
fx.Provide(NewFxDemo)  
  • 、使用Populate完成变量与具体类型间的映射
var fx FxDemo
fx.Populate(fx)
  • 、新建app对象(application容器包括定义注入变量、类型、不同对象lifecycle等)
app := fx.New(
  fx.Provide(NewFxDemo,),  // 构造函数可以任意多个
  fx.Populate(new(FxDemo)),// 反射变量也可以任意多个,并不需要和上面构造函数对应
)

app.Start(context.Background())  // 开启container
defer app.Stop(context.Background()) // 关闭container
  • 、使用
fmt.Printf("the result is %s \n", fx.Name)

大致的使用步骤就如下。下面会给出一些完整的demo

2、简单demo
将io.reader与具体实现类关联起来

package main

import (
    "context"
    "fmt"
    "github.com/uber-go/fx"
    "io"
    "io/ioutil"
    "log"
    "strings"
)

func main() {
    var reader io.Reader

    app := fx.New(
        // io.reader的应用
                                                  // 提供构造函数
        fx.Provide(func() io.Reader { 
                      return strings.NewReader("hello world") 
                }),  
        fx.Populate(&reader), // 通过依赖注入完成变量与具体类的映射
    )
    app.Start(context.Background())
    defer app.Stop(context.Background())

    // 使用
       // reader变量已与fx.Provide注入的实现类关联了
    bs, err := ioutil.ReadAll(reader)  
    if err != nil{
        log.Panic("read occur error, ", err)
    }
    fmt.Printf("the result is '%s' \n", string(bs))
}

输出:

2019/02/02 16:51:57 [Fx] PROVIDE    io.Reader <= main.main.func1() 
2019/02/02 16:51:57 [Fx] PROVIDE    fx.Lifecycle <= fx-master.New.func1()
2019/02/02 16:51:57 [Fx] PROVIDE    fx.Shutdowner <= fx-master.(*App).(fx-master.shutdowner)-fm()
2019/02/02 16:51:57 [Fx] PROVIDE    fx.DotGraph <= fx-master.(*App).(fx-master.dotGraph)-fm()
2019/02/02 16:51:57 [Fx] INVOKE     reflect.makeFuncStub()
2019/02/02 16:51:57 [Fx] RUNNING
the result is 'hello world'  

3、使用struct参数
前面的使用方式一旦需要进行注入的类型过多,可以通过struct参数方式来解决

package main

import (
    "context"
    "fmt"
    "github.com/uber-go/fx"
)


func main() {
    type t3 struct {
        Name string
    }

    type t4 struct {
        Age int
    }

    var (
        v1 *t3
        v2 *t4
    )

    app := fx.New(
        fx.Provide(func() *t3 { return &t3{"hello everybody!!!"} }),
        fx.Provide(func() *t4 { return &t4{2019} }),

        fx.Populate(&v1),
        fx.Populate(&v2),
    )

    app.Start(context.Background())
    defer app.Stop(context.Background())

    fmt.Printf("the reulst is %v , %v\n", v1.Name, v2.Age)
}

输出

2019/02/02 17:00:13 [Fx] PROVIDE    *main.t3 <= main.test2.func1()
2019/02/02 17:00:14 [Fx] PROVIDE    *main.t4 <= main.test2.func2()
2019/02/02 17:00:14 [Fx] PROVIDE    fx.Lifecycle <= fx-master.New.func1()
2019/02/02 17:00:14 [Fx] PROVIDE    fx.Shutdowner <= fx-master.(*App).(fx-master.shutdowner)-fm()
2019/02/02 17:00:14 [Fx] PROVIDE    fx.DotGraph <= fx-master.(*App).(fx-master.dotGraph)-fm()
2019/02/02 17:00:14 [Fx] INVOKE     reflect.makeFuncStub()
2019/02/02 17:00:14 [Fx] INVOKE     reflect.makeFuncStub()
2019/02/02 17:00:14 [Fx] RUNNING
the reulst is hello everybody!!! , 2019

如果通过Provide提供构造函数是生成相同类型会有什么问题?换句话也就是相同类型拥有多个值呢?
下面两种方式就是来解决这样的问题。
4、使用struct参数+Name标签
在Fx未使用Name或Group标签时不允许存在多个相同类型的构造函数,一旦存在会触发panic。

package main

import (
    "context"
    "fmt"
    "github.com/uber-go/fx"
)


func main() {
    type t3 struct {
        Name string
    }
    //name标签的使用
    type result struct {
        fx.Out

        V1 *t3 `name:"n1"`
        V2 *t3 `name:"n2"`
    }

    targets := struct {
        fx.In

        V1 *t3 `name:"n1"`
        V2 *t3 `name:"n2"`
    }{}

    app := fx.New(
        fx.Provide(func() result {
            return result{
                V1: &t3{"hello-HELLO"},
                V2: &t3{"world-WORLD"},
            }
        }),

        fx.Populate(&targets),
    )

    app.Start(context.Background())
    defer app.Stop(context.Background())

    fmt.Printf("the result is %v, %v \n", targets.V1.Name, targets.V2.Name)
}

输出

2019/02/02 17:12:02 [Fx] PROVIDE    *main.t3:n1 <= main.test3.func1()
2019/02/02 17:12:02 [Fx] PROVIDE    *main.t3:n2 <= main.test3.func1()
2019/02/02 17:12:02 [Fx] PROVIDE    fx.Lifecycle <= fx-master.New.func1()
2019/02/02 17:12:02 [Fx] PROVIDE    fx.Shutdowner <= fx-master.(*App).(fx-master.shutdowner)-fm()
2019/02/02 17:12:02 [Fx] PROVIDE    fx.DotGraph <= fx-master.(*App).(fx-master.dotGraph)-fm()
2019/02/02 17:12:02 [Fx] INVOKE     reflect.makeFuncStub()
2019/02/02 17:12:02 [Fx] RUNNING
the result is hello-HELLO, world-WORLD 

上面通过Name标签即可完成在Fx容器注入相同类型
5、使用struct参数+Group标签
使用group标签同样也能完成上面的功能

package main

import (
    "context"
    "fmt"
    "github.com/uber-go/fx"
)


func main() {
    type t3 struct {
        Name string
    }

    // 使用group标签
    type result struct {
        fx.Out

        V1 *t3 `group:"g"`
        V2 *t3 `group:"g"`
    }

    targets := struct {
        fx.In

        Group []*t3 `group:"g"`
    }{}

    app := fx.New(
        fx.Provide(func() result {
            return result{
                V1: &t3{"hello-000"},
                V2: &t3{"world-www"},
            }
        }),

        fx.Populate(&targets),
    )

    app.Start(context.Background())
    defer app.Stop(context.Background())

    for _,t := range targets.Group{
        fmt.Printf("the result is %v\n", t.Name)
    }
}

输出

2019/02/02 17:15:49 [Fx] PROVIDE    *main.t3 <= main.test4.func1()
2019/02/02 17:15:49 [Fx] PROVIDE    *main.t3 <= main.test4.func1()
2019/02/02 17:15:49 [Fx] PROVIDE    fx.Lifecycle <= fx-master.New.func1()
2019/02/02 17:15:49 [Fx] PROVIDE    fx.Shutdowner <= fx-master.(*App).(fx-master.shutdowner)-fm()
2019/02/02 17:15:49 [Fx] PROVIDE    fx.DotGraph <= fx-master.(*App).(fx-master.dotGraph)-fm()
2019/02/02 17:15:49 [Fx] INVOKE     reflect.makeFuncStub()
2019/02/02 17:15:49 [Fx] RUNNING
the result is hello-000
the result is world-www

基本上Fx简单应用在上面的例子也做了简单讲解

三、Fx定义

1、Annotated(位于annotated.go文件) 主要用于采用annotated的方式,提供Provide注入类型

type t3 struct {
    Name string
}

targets := struct {
    fx.In

    V1 *t3 `name:"n1"`
}{}

app := fx.New(
    fx.Provide(fx.Annotated{
        Name:"n1",
        Target: func() *t3{
            return &t3{"hello world"}
        },
    }),
    fx.Populate(&targets),
)
app.Start(context.Background())
defer app.Stop(context.Background())

fmt.Printf("the result is = '%v'\n", targets.V1.Name)

源码中Name和Group两个字段与前面提到的Name标签和Group标签是一样的,只能选其一使用
2、App(位于app.go文件) 提供注入对象具体的容器、LiftCycle、容器的启动及停止、类型变量及实现类注入和两者映射等操作

type App struct {
   err          error
   container    *dig.Container        // 容器
   lifecycle    *lifecycleWrapper    // 生命周期
   provides     []interface{}            // 注入的类型实现类
   invokes      []interface{}    
   logger       *fxlog.Logger
   startTimeout time.Duration
   stopTimeout  time.Duration
   errorHooks   []ErrorHandler

   donesMu sync.RWMutex
   dones   []chan os.Signal
}

// 新建一个App对象
func New(opts ...Option) *App {
   logger := fxlog.New()   // 记录Fx日志
   lc := &lifecycleWrapper{lifecycle.New(logger)}  // 生命周期

   app := &App{
       container:    dig.New(dig.DeferAcyclicVerification()),
       lifecycle:    lc,
       logger:       logger,
       startTimeout: DefaultTimeout,
       stopTimeout:  DefaultTimeout,
   }

   for _, opt := range opts {  // 提供的Provide和Populate的操作
       opt.apply(app)
   }

       // 进行app相关一些操作
   for _, p := range app.provides {
       app.provide(p)
   }
   app.provide(func() Lifecycle { return app.lifecycle })
   app.provide(app.shutdowner)
   app.provide(app.dotGraph)

   if app.err != nil {  // 记录app初始化过程是否正常
       app.logger.Printf("Error after options were applied: %v", app.err)
       return app
   }

       // 执行invoke
   if err := app.executeInvokes(); err != nil {
       app.err = err

       if dig.CanVisualizeError(err) {
           var b bytes.Buffer
           dig.Visualize(app.container, &b, dig.VisualizeError(err))
           err = errorWithGraph{
               graph: b.String(),
               err:   err,
           }
       }
       errorHandlerList(app.errorHooks).HandleError(err)
   }
   return app
}

至于Provide和Populate的源码相对比较简单易懂在这里不在描述
具体源码
3、Extract(位于extract.go文件)
主要用于在application启动初始化过程通过依赖注入的方式将容器中的变量值来填充给定的struct,其中target必须是指向struct的指针,并且只能填充可导出的字段(golang只能通过反射修改可导出并且可寻址的字段),Extract将被Populate代替。具体源码
4、其他
诸如Populate是用来替换Extract的,而LiftCycle和inout.go涉及内容比较多后续会单独提供专属文件说明。

四、其他

在Fx中提供的构造函数都是惰性调用,可以通过invocations在application启动来完成一些必要的初始化工作:fx.Invoke(function); 通过也可以按需自定义实现LiftCycle的Hook对应的OnStart和OnStop用来完成手动启动容器和关闭,来满足一些自己实际的业务需求。

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "time"

    "go.uber.org/fx"
)

// Logger构造函数
func NewLogger() *log.Logger {
    logger := log.New(os.Stdout, "" /* prefix */, 0 /* flags */)
    logger.Print("Executing NewLogger.")
    return logger
}

// http.Handler构造函数
func NewHandler(logger *log.Logger) (http.Handler, error) {
    logger.Print("Executing NewHandler.")
    return http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
        logger.Print("Got a request.")
    }), nil
}

// http.ServeMux构造函数
func NewMux(lc fx.Lifecycle, logger *log.Logger) *http.ServeMux {
    logger.Print("Executing NewMux.")

    mux := http.NewServeMux()
    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }
  
    lc.Append(fx.Hook{  // 自定义生命周期过程对应的启动和关闭的行为
        OnStart: func(context.Context) error {
            logger.Print("Starting HTTP server.")
            go server.ListenAndServe()
            return nil
        },
        OnStop: func(ctx context.Context) error {
            logger.Print("Stopping HTTP server.")
            return server.Shutdown(ctx)
        },
    })

    return mux
}

// 注册http.Handler
func Register(mux *http.ServeMux, h http.Handler) {
    mux.Handle("/", h)
}

func main() {
    app := fx.New(
        fx.Provide(
            NewLogger,
            NewHandler,
            NewMux,
        ),
        fx.Invoke(Register),  // 通过invoke来完成Logger、Handler、ServeMux的创建 
    )
    startCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()
    if err := app.Start(startCtx); err != nil {  // 手动调用Start
        log.Fatal(err)
    }

    http.Get("http://localhost:8080/")  // 具体操作

    stopCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 
    defer cancel()     
    if err := app.Stop(stopCtx); err != nil { // 手动调用Stop 
        log.Fatal(err)
    }

}

五、Fx源码解析

Fx框架源码解析
主要包括app.go、lifecycle.go、annotated.go、populate.go、inout.go、shutdown.go、extract.go(可以忽略,了解populate.go)以及辅助的internal中的fxlog、fxreflect、lifecycle

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269