0%

golang如何避免循环依赖

golang 包引用之间不允许循环依赖.
循环依赖的本质上是一个错误的设计, 在 golang中 循环依赖是 会产生编译时错误.

dependency-cycle

golang中为什么不允许循环依赖呢?

1. 没有支持循环导入,目的是迫使 Go 程序员更多地考虑程序的依赖关系.
  - 保持依赖关系图的简洁。
  - 快速的程序构建。
2. 如果支持循环导入,很容易会造成懒惰、不良的依赖性管理和缓慢的构建。这是设计者不希望看见的。
  - 混乱的依赖关系。
  - 缓慢的程序构建

golang中的循环依赖对编译的性能 和 程序的依赖关系的清晰非常不利, 所以在程序设计上,要保持 干净的 DAG.

常见的循环依赖 和 优化代码技巧

1. 抽象顶层包

ddd 的项目结构 一般是 依赖倒置, 在 service层定义接口, infrastrue 层实现,目的是为了解耦, 实现可替换.

你可能将代码写成这样:

repo包代码如下:

repo包依赖 service包

package repo

import (
	"context"
	"github.com/xxx/cycle/service"
)

type channelRepo struct {
}

func NewChannelRepo() *channelRepo {
	return &channelRepo{}
}

func (c channelRepo) GetByID(ctx context.Context, id int64) (*service.Channel, error) {
	panic("")
}

service 包

package service

import (
	"context"
	"github.com/xxx/cycle/repo"
)

type Channel struct {
	ID int64
}

type ChannelRepo interface {
	GetByID(ctx context.Context, id int64) (*Channel, error)
}

type ChannelService struct {
	channelRepo ChannelRepo
}

func NewChannelService() *ChannelService {
	return &ChannelService{
		channelRepo: repo.NewChannelRepo(),
	}
}

func (cs *ChannelService) Get() {
	
}

解决方式之一是我们可以引入一个顶层包 handler, 依赖 service & repo:

形成如下依赖关系:

package repo

import (
	"context"
	"github.com/xxx/cycle/service"
)

type channelRepo struct {
}

func NewChannelRepo() *channelRepo {
	return &channelRepo{}
}

func (c channelRepo) GetByID(ctx context.Context, id int64) (*service.Channel, error) {
	panic("")
}

service包

package service

import (
	"context"
)

type Channel struct {
	ID int64
}

type ChannelRepo interface {
	GetByID(ctx context.Context, id int64) (*Channel, error)
}

type ChannelService struct {
	channelRepo ChannelRepo
}

func NewChannelService(channelRepo ChannelRepo) *ChannelService {
	return &ChannelService{
		channelRepo: channelRepo,
	}
}

func (cs *ChannelService) Get() {}

handler包代码如下:

package handler

import (
	"github.com/xxx/cycle/repo"
	"github.com/xxx/cycle/service"
)

func useCase() {
	channelRepo := repo.NewChannelRepo()
	channelService := service.NewChannelService(channelRepo)
	channelService.Get()
}

2. 和上面的场景类似, 也是抽象一个顶层解决

比如我们没用争取使用策略模式的分包.

有下面这种场景,做 一些数据过滤, 在 filter 包里面定义了filter 接口, 将实现定义在子包里面.

type Filter interface {
	PreAction(ctx context.Context, data map[string]interface{}) error
}

1. 子包 word

word包引用了 filter包的 对象 DataContext

package word

import (
	"context"
	"github.com/xxx/cycle"
)

type wordFilter struct {
}

func (w wordFilter) PreAction(ctx context.Context, data *cycle.DataContext) error {
	panic("implement me")
}

func NewWordFilter() *wordFilter {
	return &wordFilter{}
}

2. 子包 policy

word包引用了 filter包的 对象 DataContext

package policy

import (
	"context"
	"github.com/xxx/cycle"
)

type policyFilter struct {
}

func NewPolicyFilter() *policyFilter {
	return &policyFilter{}
}

func (p policyFilter) PreAction(ctx context.Context, data *cycle.DataContext) error {
	panic("implement me")
}

3. 在filter包中使用

package filter

import (
	"context"
	"github.com/xxx/cycle/policy"
	"github.com/xxx/cycle/word"
)

var filterRegistry map[string]Filter

func init() {
	filterRegistry = map[string]Filter{
		"word":   word.NewWordFilter(),
		"policy": policy.NewPolicyFilter(),
	}
}

type DataContext struct {
}

type Filter interface {
	PreAction(ctx context.Context, data *DataContext) error
}

func GetFilter(filterType string) Filter {
	return filterRegistry[filterType]
}
|filter
├── filter.go
├── policy
│   └── filter.go
└── word
    └── filter.go

此时产生了循环依赖.

解决的方式: 其实应该属于编程技巧:

不要再 filter包中有使用 filter 的逻辑, filer包仅定义接口, 在 filter包之外进行调用

修改后的方案:

  • filter包

    package filter
    
    import (
    	"context"
    )
    
    func init() {
    	filterRegistry = make(map[string]Filter)
    }
    
    var filterRegistry map[string]Filter
    
    func Register(filterKey string, filter Filter) {
    	filterRegistry[filterKey] = filter
    }
    
    type DataContext struct {
    }
    
    type Filter interface {
    	PreAction(ctx context.Context, data *DataContext) error
    }
    
    func GetFilter(filterType string) Filter {
    	return filterRegistry[filterType]()
    }
    
  • word子包

    package word
    
    import (
    	"context"
    	"github.com/xxx/cycle/filter"
    )
    
    func init() {
    	filter.Register("word", NewWordFilter())
    }
    
    type wordFilter struct {
    }
    
    func (w wordFilter) PreAction(ctx context.Context, data *filter.DataContext) error {
    	//TODO implement me
    	panic("implement me")
    }
    
    func NewWordFilter() *wordFilter {
    	return &wordFilter{}
    }
    
  • policy子包

    package policy
    
    import (
    	"context"
    	"github.com/xxx/cycle/filter"
    )
    
    func init() {
    	filter.Register("policy", NewPolicyFilter())
    }
    
    type policyFilter struct {
    }
    
    func NewPolicyFilter() *policyFilter {
    	return &policyFilter{}
    }
    
    func (p policyFilter) PreAction(ctx context.Context, data *filter.DataContext) error {
    	//TODO implement me
    	panic("implement me")
    }
  • 添加上层调用:

package cycle

import (
	"github.com/xxx/cycle/filter"
	_ "github.com/xxx/cycle/filter/policy"
	_ "github.com/xxx/cycle/filter/word"
)

func handle() {
	filter.GetFilter("word")
	filter.GetFilter("policy")
}

3. 使用事件总线解耦

想分享的是上面两种方案, 之后又在网上看到了某些方案:
比如事件总线,简单说就是: 使用时如果不关心返回结果, 就可以通过消息的方式解耦.

  1. eventBus 包

    package eventBus
     
    import (
    	"github.com/asaskevich/EventBus"
    )
     
    var globalEventBus EventBus.Bus
     
    func init() {
    	globalEventBus = EventBus.New()
    }
     
    func Subscribe(topic string, fn interface{}) error {
    	return globalEventBus.Subscribe(topic, fn)
    }
     
    func SubscribeAsync(topic string, fn interface{}, transactional bool) error {
    	return globalEventBus.SubscribeAsync(topic, fn, transactional)
    }
     
    func Publish(topic string, args ...interface{}) {
    	globalEventBus.Publish(topic, args...)
    }
    
  2. packageA

package package_a
 
import (
	"cycle/eventBus"
	"fmt"
)
 
func init() {
	eventBus.Subscribe("PrintA", new(PackageA).PrintA)
}
 
type PackageA struct {
}
 
func (a PackageA) PrintA() {
	fmt.Println("I'm a!")
}
 
func (a PackageA) PrintAll() {
	a.PrintA()
	eventBus.Publish("PrintB")
}
  1. packageB
package package_b
 
import (
	"cycle/eventBus"
	"fmt"
)
 
func init() {
	eventBus.Subscribe("PrintB", new(PackageB).PrintB)
}
 
type PackageB struct {
}
 
func (b PackageB) PrintB() {
	fmt.Println("I'm b!")
}
 
func (b PackageB) PrintAll() {
	b.PrintB()
	eventBus.Publish("PrintA")
}

总结

编写代码要符合规范,包的组织结构 和 依赖 要清晰, 符合规范.
golang的包组织规范可以参考:


[参考]
循环引用
golang包循环引用的几种解决方案

欢迎关注我的其它发布渠道