Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2使用说明 #49

Open
Degfy opened this issue Mar 11, 2025 · 0 comments
Open

v2使用说明 #49

Degfy opened this issue Mar 11, 2025 · 0 comments

Comments

@Degfy
Copy link
Member

Degfy commented Mar 11, 2025

Gone@v2 使用说明

Goner的定义

Goner是Gone框架定义的组件,是只嵌入了gone.Flag的结构体指针,可以用于Gone框架的依赖注入,如下代码就定义了一个简单的Goner:

package demo

import "github.com/gone-io/gone"

type Component struct {
	gone.Flag
}

var aGoner = &Component{}

Goner加载器

Gone框架核心是一个Goner仓库,加载器的作用就是将用户定义的组件(Goner)注册(或者加载,在本文后续我们都称为加载)到仓库中,以便后续的依赖注入。

加载Goner

package main

import "github.com/gone-io/gone"

func main() {
	type Dep struct {
		gone.Flag
		Name string
	}

	// 加载一个Dep
	gone.Load(&Dep{})

	//加载 一个名为dep1的Dep
	gone.Load(&Dep{}, gone.Name("dep1"))

	//支持链式调用
	gone.
		Load(&Dep{}, gone.Name("dep3")).
		Load(&Dep{}, gone.Name("dep2"))

	//通过加载函数批量加载
	gone.Loads(func(loader gone.Loader) error {
		err := loader.Load(&Dep{}, gone.Name("dep4"))
		if err != nil {
			return gone.ToError(err)
		}
		err = loader.Load(&Dep{}, gone.Name("dep5"))

		return err
	})
}

上面的代码展示了Gone框架中加载Goner的几种方式:

  1. 直接加载:使用gone.Load()方法可以直接加载一个Goner。例如gone.Load(&Dep{})将加载一个默认的Dep组件。

  2. 命名加载:通过gone.Name()选项可以为加载的Goner指定一个名称。例如gone.Load(&Dep{}, gone.Name("dep1"))将加载一个名为"dep1"的Dep组件。

  3. 链式加载:Gone框架支持链式调用方式加载多个Goner。可以通过.Load()方法连续加载多个组件,使代码更加简洁。

  4. 批量加载:使用gone.Loads()方法可以在一个函数中批量加载多个Goner。这种方式特别适合需要进行错误处理的场景,可以统一处理加载过程中的错误。

这些加载方式提供了灵活的组件注册机制,开发者可以根据具体需求选择合适的方式来加载Goner。

加载函数

查看源代码,可以看到gone.Loads()方法的定义:

func (s *Application) Loads(loads ...LoadFunc) *Application {
//...
}

LoadFunc是一个函数类型,定义如下:

type LoadFunc func (Loader) error

如果编写一个功能模块,需要加载多个Goner到Gone框架中,可以提供一个LoadFunc函数,业务代码只需要通过gone.Loads()
方法调用这个函数即可。

加载选项

查到源代码,可以看到gone.Load()方法的定义:

func Load(goner Goner, options ...Option) *Application {
//...
}

Option就是加载选项,它的作用是在加载Goner时,可以设置一些选项,比如gone.Name("dep1")就是设置Goner的名称为"dep1"。

支持的选项

  • gone.IsDefault(objPointers ...any): 将组件标记为其类型的默认实现。当存在多个相同类型的组件时,如果没有指定具体名称,将使用默认实现进行注入。

    // 将EnvConfigure标记为默认实现
    gone.Load(&EnvConfigure{}, gone.IsDefault())
  • gone.Order(order int): 设置组件的启动顺序。order值越小,组件启动越早。框架提供了三个预设的顺序选项:

    • gone.HighStartPriority(): 相当于Order(-100),最早启动
    • gone.MediumStartPriority(): 相当于Order(0),默认启动顺序
    • gone.LowStartPriority(): 相当于Order(100),最后启动
    // Database会在Service之前启动
    gone.Load(&Database{}, gone.Order(1))  // 先启动
    gone.Load(&Service{}, gone.Order(2))   // 后启动
  • gone.Name(name string): 为组件设置一个自定义名称。组件可以通过这个名称被注入。

    // 加载一个名为"configure"的组件
    gone.Load(&EnvConfigure{}, gone.Name("configure"))
  • gone.OnlyForName(): 标记组件仅支持基于名称的注入。使用此选项时,组件不会被注册为类型提供者,只能通过显式引用其名称进行注入。

    // EnvConfigure只能通过`gone:"configure"`标签注入
    gone.Load(&EnvConfigure{}, gone.Name("configure"), gone.OnlyForName())
  • gone.ForceReplace(): 允许替换具有相同名称或类型的现有组件。加载带有此选项的组件时:

    • 如果存在同名组件,将被替换
    • 如果存在相同类型的提供者,将被替换
    // 这将替换任何名为"service"的现有组件
    gone.Load(&MyService{}, gone.Name("service"), gone.ForceReplace())
  • gone.LazyFill(): 将组件标记为延迟填充。使用此选项时,组件只有在实际被注入时才会被加载。这对于加载成本高或有外部依赖的组件很有用。

    // 只有在实际注入时才会加载组件
    gone.Load(&MyService{}, gone.Name("service"), gone.LazyFill())

依赖注入

基于字段类型的注入

package main

import "github.com/gone-io/gone"

type Dep struct {
	gone.Flag
	Name string
}
type Service struct {
	gone.Flag
	dep1 *Dep `gone:""` // 默认注入
	dep2 *Dep `gone:"*"`
}

在上面的代码中展示了基于类型的注入方式:
按类型注入,需要在被注入的的字段上添加gone:""(或者 gone:"*",在v2中两者是等效的)
标签,框架会自动根据类型进行注入。如果在注入前,加载了多个相同类型的组件,那么框架会优先选择默认实现(通过IsDefault()
选项设置),否则会选择第一个加载的组件并提示警告。

基于Goner名称注入

gone标签中可以指定组件名称,框架会根据名称进行注入。

package main

import "github.com/gone-io/gone"

type Dep struct {
	gone.Flag
	Name string
}
type Service struct {
	gone.Flag
	dep1 *Dep `gone:"dep1"` // 指定名称注入
	dep2 *Dep `gone:"dep2"` // 指定名称注入
}

Goner支持两种方式设置名称:

  1. 在加载时,使用gone.Name()选项设置名称。
  2. Goner实现了NamedGoner接口,通过GonerName方法设置名称,NamedGoner接口定义如下:
type NamedGoner interface {
	Goner
	GonerName() string
}

通过Provider给组件注入依赖

在v1中,给组件注入配置参数是这样写的:

type Service struct {
gone.Flag
confStr string `gone:"config,configKeyName"` // 通过标签注入配置参数
}

在v2中,依然支持这样方式,底层实现改为由Provider提供值。
让我们来看看Provider的接口定义:

type Provider[T any] interface {
	Goner
	Provide(tagConf string) (T, error)
}

它是一个泛型接口,在实际使用中,我们需要定义一个Provider,并实现Provide方法,在Provide
方法中,我们可以通过tagConf获取到配置参数,然后返回值即可。必然为了支持上面的confStr的配置,我们需要定义一个Provider,如下:

type ConfigProvider struct {
gone.Flag
}

func (c *ConfigProvider) Provide(tagConf string) (string, error) {
return config.Get(tagConf)
}

Provide方法中,我们通过tagConf获取到配置参数,然后返回值即可。这样,我们就可以通过Provider给组件注入配置参数了。

如果需要被配置的字段是一个int类型,那么我们需要定义一个Provider,如下:

type ConfigProvider struct {
	gone.Flag
}

func (c *ConfigProvider) Provide(tagConf string) (int, error) {
	return config.Get(tagConf)
}

会很快发现一个问题,为了实现一个Config模块,我们需要定义无数个Provider,这显然是不合理的,因此,我们需要一个通用的Provider,如下:

type NamedProvider interface {
	NamedGoner
	Provide(tagConf string, t reflect.Type) (any, error)
}

NamedProvider 接口定义了一个Provide
方法,该方法接收两个参数,第一个参数是tagConf,第二个参数是t,t是reflect.Type类型,用于获取字段的类型,然后根据字段的类型返回对应的值。这样,我们就可以通过NamedProvider给组件注入配置参数了。

现在对比一下 Provider 和 NamedProvider 的区别:

  1. Provider 是一个泛型,它可以根据字段的类型返回对应的值,我们每次实现的Provider都只能固定一个类型,所以只能返回一个值,不能返回多个类型。它的应用场景是,是按类型注入第三方的值。
  2. NamedProvider 是一个接口,它可以根据字段的类型返回对应的值。它的应用场景是,需要通过一个Provider给组件注入多个类型的值。

下面讲讲依赖注入查找的流程:

  1. 如果gone标签中没有指定名称或者指定的名称为*,那么框架会使用内核中提供Provider来给组件注入值(对,内核其实就是一个Provider,v2的整个注入机制都是基于Provider的)。
  • 内核Provider会按类型查找Provider,如果找到了,那么就会调用Provider的Provide方法,将值注入到组件中。
  • 如果没有找到,内核Provider会按类型查找加载到Goner仓库的值,如果能找到兼容的值,那么就会将值注入到组件中。
  • 如果还是没有找到,那么就会报错。
  1. 如果gone标签中指定了名称,那么框架会使用指定的Provider来给组件注入值。
  • 优先按名字查找Provider[T any]NoneParamProvider[T any],如果找到了,那么就会调用Provider的Provide
    方法,如果他们提供的值是兼容的,那么就会将值注入到组件中。
  • 如果还没有注入成功,则继续按名字查找NamedProvider,如果找到了,那么就会调用它的
    Provide(tagConf string, t reflect.Type) (any, error)方法,它如果能返回一个兼容的值,那么就会将值注入到组件中。
  • 如果还是没能注入成功,还用回调StructFieldInjector来注入值。
  • 如果还是没有找到,那么就会报错。

补充说明:
NoneParamProvider的定义:

type NoneParamProvider[T any] interface {
  Goner
	Provide() T
}

它是一个泛型接口,它只有一个方法Provide,这个方法没有参数,返回一个值。它的应用场景是,需要通过一个Provider给组件注入一个值,这个值不需要通过参数传递,只需要通过方法返回即可。

StructFieldInjector的定义:

type StructFieldInjector interface {
  NamedGoner
	Inject(tagConf string, field reflect.StructField, fieldValue reflect.Value) error
}

它是一个接口,它只有一个方法InjectStructField,这个方法接收三个参数,第一个参数是v,第二个参数是tagConf,第三个参数是t,t是reflect.Type类型,用于获取字段的类型,然后根据字段的类型返回对应的值。它的应用场景是,需要通过一个Provider给组件注入多个类型的值。

从接口的定义上可以看到,Provider、NoneParamProvider、NamedProvider和StructFieldInjector都是Goner的子接口,要实现他们都必须嵌入
gone.Flag。他们的用途都是将第三方的值提给Gone框架,让框架来进行依赖注入。

Goner的生命周期

flow.png

  1. 初始化阶段
    依赖注入前,如果组件上存在 BeforeInit() error 或者 BeforeInit() ,那么就会调用这个方法,这个方法会在依赖注入之前被调用,
    在这个方法中不能够使用依赖注入的值
    依赖注入后,如果组件上存在 Init() error 或者 Init() ,那么就会调用这个方法,这个方法会在依赖注入之后被调用,*
    在这个方法中可以使用依赖注入的值*。
    组件被注入到其他组件前,必须先完成自己的初始化。
  2. 运行阶段
    这个阶段,如果组件实现了Daemon接口,该阶段会运行Daemon接口的Start() error方法来启动自己。可以在加载组件时,通过
    gone.Order()方法设置组件的启动顺序。
  3. 停机阶段
    这个阶段,如果组件实现了Daemon接口,该阶段会运行Daemon接口的Stop() error方法来停止自己。Stop的顺序和Start
    的顺序相反。

注意:如果需要Daemon持续提供服务,需要调用Serve()方法,而不是Run(),Serve函数会阻塞,直到End被调用或者进程收到终止的信号。

package use_case

import (
	"github.com/gone-io/gone"
	"testing"
	"time"
)

type testDaemon struct {
	gone.Flag
	isStart bool
	isStop  bool
}

func (t *testDaemon) Start() error {
	println("testDaemon Start")
	t.isStart = true
	return nil
}

func (t *testDaemon) Stop() error {
	println("testDaemon Stop")
	t.isStop = true
	return nil
}

func TestServe(t *testing.T) {
	daemon := &testDaemon{}
	var t1, t2 time.Time
	ins := gone.NewApp() //这里创建了一个实例,在后面的##Application章节有说明。 

	ins.
		Load(daemon).
		BeforeStart(func() {
			go func() {
				time.Sleep(5 * time.Millisecond)
				t1 = time.Now()
				ins.End() //如果调用gone.End()方法,将终止框架默认的实例`gone.Default`
			}()
		}).
		AfterStop(func() {
			t2 = time.Now()
		}).
		Serve()

	if !daemon.isStart {
		t.Fatal("daemon start failed")
	}
	if !daemon.isStop {
		t.Fatal("daemon stop failed")
	}
	if !t2.After(t1) {
		t.Fatal("daemon stop after serve failed")
	}
}

四个hook函数

框架提供4个hook函数,分别为作用于beforeStart、afterStart、beforeStop、afterStop。其中两个before Hook 先注册的后执行;两个after
Hook 先注册的先执行。下面代码是通过依赖注入的方式来演示的,只能在组件完成初始化之后才能使用。

package use_case

import (
	"github.com/gone-io/gone"
	"testing"
)

type hookTest struct {
	gone.Flag
	beforeStart gone.BeforeStart `gone:""`
	afterStart  gone.AfterStart  `gone:""`
	beforeStop  gone.BeforeStop  `gone:""`
	afterStop   gone.AfterStop   `gone:""`
}

var orders []int

func (h *hookTest) Init() {
	//通过注入的BeforeStart方法,可以注册一个函数,在服务启动前执行
	h.beforeStart(func() {
		println("before start 1")
		orders = append(orders, 1)
	})

	//before 类型的hook 可以注册多个,先注册的后执行,后注册的先执行
	h.beforeStart(func() {
		println("before start 2")
		orders = append(orders, 2)
	})

	h.afterStart(func() {
		println("after start 3")
		orders = append(orders, 3)
	})

	h.afterStart(func() {
		println("after start 4")
		orders = append(orders, 4)
	})

	h.beforeStop(func() {
		println("before stop 5")
		orders = append(orders, 5)
	})

	h.beforeStop(func() {
		println("before stop 6")
		orders = append(orders, 6)
	})

	h.afterStop(func() {
		println("after stop 7")
		orders = append(orders, 7)
	})
	h.afterStop(func() {
		println("after stop 8")
		orders = append(orders, 8)
	})
}

func TestUseHook(t *testing.T) {
	gone.Load(&hookTest{}).Run()

	wantedOrder := []int{2, 1, 3, 4, 6, 5, 7, 8}
	for i := range wantedOrder {
		if wantedOrder[i] != orders[i] {
			t.Errorf("wanted %v, got %v", wantedOrder[i], orders[i])
		}
	}
}

hook函数也可以直接在加载Goner组件后注册,像这样:

func TestUseHookDirectly(t *testing.T) {
    type testGoner struct {
        gone.Flag
    }
    gone.
        Load(&testGoner{}).
        // 直接注册Hook函数
        BeforeStart(func () {
            println(" BeforeStart")
        }).
        AfterStart(func () {
            println(" AfterStart")
        }).
        Run()
}

Application

查看源代码,可以发现:gone.Loadgone.Loadsgone.Rungone.Serve等函数实际是调用的Application的一个实例Default
上的对应方法。

var Default = NewApp()
//...
func Load(goner Goner, options ...Option) *Application {
    return Default.Load(goner, options...)
}
//...
func Loads(loads ...LoadFunc) *Application {
    return Default.Loads(loads...)
}
//...

所以,如果希望在同一个进程中使用多个Gone框架实例,可以使用 gone.NewApp 函数来创建多个 Application 实例,然后分别调用 RunServe 方法启动框架。
下面是NewApp的定义:

func NewApp(loads ...LoadFunc) *Application {
    application := Application{}
    //....
    return &application
}

GonerKeeper

如果希望在组件中,使用代码动态地获取其他组件,可以注入gone.GonerKeeper接口(当然也可以直接注入 *gone.Core),这个接口定义如下:

type GonerKeeper interface {
    GetGonerByName(name string) any
    GetGonerByType(t reflect.Type) any
}

示例代码:

package use_case

import (
	"github.com/gone-io/gone"
	"testing"
)

type useKeeper struct {
	gone.Flag
	keeper gone.GonerKeeper `gone:"*"`
	core   *gone.Core       `gone:"*"`
}

func (u *useKeeper) Test(t *testing.T) {
	goner := u.keeper.GetGonerByName("*")
	if goner != u.core {
		t.Fatal("keeper get core error")
	}
}

func TestGonerKeeper(t *testing.T) {
	gone.
		NewApp().
		Load(&useKeeper{}).
		Run(func(k *useKeeper) {
			k.Test(t)
		})
}

数组注入

在v2版本中,依然可以使用 接口slice 来接收 多个实例。
测试代码如下:

package use_case

import (
	"github.com/gone-io/gone"
	"testing"
)

type worker interface {
	Work()
}

type workerImpl struct {
	gone.Flag
	name string
}

func (w *workerImpl) Work() {
	println("worker", w.name, "work")
}

type workerImpl2 struct {
	gone.Flag
	name string
}

func (w *workerImpl2) Work() {
	println("worker", w.name, "work")
}

type factory struct {
	gone.Flag
	workers []worker `gone:"*"`
}

func TestUseSlice(t *testing.T) {
	gone.
		NewApp().
		Load(&factory{}, gone.Name("factory")).
		Load(&workerImpl{name: "worker1"}, gone.Name("worker1")).
		Load(&workerImpl2{name: "worker2"}, gone.Name("worker2")).
		Run(func(f *factory) {
			if len(f.workers) != 2 {
				t.Fatal("worker count is not 2")
			}
		})

}

使用FuncInjector来实现函数参数的注入

函数参数注入是Gone框架的一个特性,它允许框架在调用函数时,自动从Goner仓库中查找并注入与函数参数类型匹配的组件实例。
在前面例子中,Run方法接收的函数,其参数就是被自动注入的。
例子:

package use_case

import (
	"github.com/gone-io/gone"
	"testing"
)

type funcTest struct {
	gone.Flag
	injector gone.FuncInjector `gone:"*"`
}

func (f *funcTest) Test(t *testing.T) {
	// 定义一个需要注入参数的函数
	fn := func(factory *factory) {
		if factory == nil {
			t.Fatal("factory is nil")
		}
	}

	// 使用 InjectWrapFunc 来执行函数,框架会自动注入参数
	wrapped, err := f.injector.InjectWrapFunc(fn, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	_ = wrapped()

	// 也可以注入多个参数
	fn2 := func(factory *factory, worker worker) {
		if factory == nil {
			t.Fatal("factory is nil")
		}
		if worker == nil {
			t.Fatal("worker is nil")
		}
	}

	wrapped2, err := f.injector.InjectWrapFunc(fn2, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	_ = wrapped2()
}

func TestFuncInjector(t *testing.T) {
	gone.
		NewApp().
		Load(&funcTest{}).
		Load(&factory{}, gone.Name("factory")).
		Load(&workerImpl{name: "worker1"}, gone.Name("worker1")).
		Run(func(f *funcTest) {
			f.Test(t)
		})
}

内置的ConfigLogger

在v2版本中,内核代码内置了ConfigLogger组件,分别用于配置管理和日志记录。

Config

内置的Config组件,是从环境变量中读取配置,可以通过实现gone.Configure来实现自定义读取配置的方式。
下面结束如何读取配置的示例代码,注意环境变量的名需要加上GONE_前缀,并且需要全部大写,如果被注入的不是简单类型,默认的Configure会尝试使用json解析环境变量的值。

package use_case

import (
	"github.com/gone-io/gone/v2"
	"os"
	"testing"
)

type useConfig struct {
	gone.Flag
	goneVersion string `gone:"config,gone-version"` //框架在启动时,会自动加载配置,并注入到goRoot字段中。
}

func TestUseConfig(t *testing.T) {
	os.Setenv("GONE_GONE-VERSION", "v2.0.0")

	gone.
		Load(&useConfig{}).
		Run(func(c *useConfig) {
			println("goRoot:", c.goneVersion)
			if c.goneVersion != "v2.0.0" {
				t.Fatal("配置注入失败")
			}
		})
}

Logger

v2内核内置的Logger,只是简单在控制台打印日志,可以通过实现gone.Logger来实现自定义日志记录的方式。

package use_case

import (
	"github.com/gone-io/gone/v2"
	"testing"
)

type worker struct {
	gone.Flag
	log gone.Logger `gone:"*"`
}

func TestUseLogger(t *testing.T) {
	gone.
		Load(&worker{}).
		Run(func(app *app) {
			app.log.Infof("hello world")
			app.log.Errorf("hello world")
			app.log.Warnf("hello world")
			app.log.Debugf("hello world")
		})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant