Golang中两个相互依赖仓库的最佳实践

Golang中两个相互依赖仓库的最佳实践 我有两个微服务,每个都位于其独立的代码仓库中。

RepoA 依赖于 repoB/entityB。 repoB 依赖于 repoA/entityA。

也许我应该把 entityA 和 entityB 放到一个单独的公共仓库里。但考虑以下情况: 我有 1 万个仓库(A, B, C, D, 1, 2, …996)。 RepoA 依赖于 repoB/entityB。 RepoB 依赖于 repoA/entityA。 RepoC 依赖于 repoD/entityD。 RepoD 依赖于 repoC/entityC。 如果我把 entity A, B, C, D 放到公共仓库里,会有点奇怪,因为 repo1…996 根本不需要它们。

我是否应该将 entityX 保留在 repoX 中,并使用 Go 模块功能(在 repoX 中设置两个模块,其中一个用于 entity X)? 对于这种情况,最佳实践是什么?


更多关于Golang中两个相互依赖仓库的最佳实践的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

实体只是一个类型定义。

更多关于Golang中两个相互依赖仓库的最佳实践的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


“实体”能否被明确定义?它严格来说是一个包、类型定义,还是其他什么东西?

扩展一下 @hollowaykeanho 所问的,如果您的每个服务都是一个模块,您可以将您的实体转换为该模块的包,然后相互导入彼此的包而不会出现问题(我希望如此)。或者,您实际上可以创建一个实体模块,将所有实体放置其中,并将它们转换为单独的包,然后在您的服务模块中导入它们。我在测试微服务并为我的应用程序寻找目录布局时遇到了同样的问题,我决定采用单一代码仓库的方法。

Moon: RepoA 依赖于 repoB/entityB。 RepoB 依赖于 repoA/entityA。

Moon: entity 只是一个类型定义。

在我看来,如果处理不当,这看起来像是一个循环导入的陷阱。如果 RepoARepoB 相互交叉依赖,你应该有一个库模块(称之为 RepoLib1),让每个实体都有自己的子包以便于导入。

换句话说,这应该始终是实践准则:

RepoA 依赖于 RepoLib1/DataB(包含 entityB 类型定义的包)。
RepoB 依赖于 RepoLib1/DataA(包含 entityA 类型定义的包)。

选择是将这些实体包保留在小型库包下,还是使用一个公共的大型仓库(例如 repoLib 而不是 repoLib1),取决于其他因素,例如:

  1. 维护者(不仅仅是你一个人)维护这 10k 仓库的方式。 1.1. 考虑更改实体结构会破坏仓库的事实,以及团队能多快修正这些问题并发布。 1.2. 考虑你升级/降级一个仓库的频率,这会影响其对应实体的更改。 1.3. 考虑客户如何使用这些库(对我来说,除非你清除了潜在的循环导入问题,否则我不会使用它)。

这样,你大致可以清楚地了解是创建多个库仓库还是单个公共仓库。两者都可行,这只是偏好问题。

对于Golang中相互依赖仓库的问题,最佳实践是使用多模块仓库(multi-module repository)结构。以下是具体实现方案:

方案一:多模块仓库(推荐)

在每个仓库中创建两个模块:一个主模块,一个实体模块。

RepoA 结构:

repoA/
├── go.mod          # module github.com/yourname/repoA
├── main.go
├── internal/
└── entity/
    ├── go.mod      # module github.com/yourname/repoA/entity
    └── entityA.go

RepoA/entity/go.mod:

module github.com/yourname/repoA/entity

go 1.21

RepoA/entity/entityA.go:

package entity

type EntityA struct {
    ID   string
    Name string
    // 只包含共享字段,不包含业务逻辑
}

RepoB 结构类似:

repoB/
├── go.mod          # module github.com/yourname/repoB
├── main.go
├── internal/
└── entity/
    ├── go.mod      # module github.com/yourname/repoB/entity
    └── entityB.go

在 RepoA 中引用 RepoB 的实体:

// repoA/main.go
package main

import (
    "fmt"
    "github.com/yourname/repoB/entity"
)

func main() {
    b := entity.EntityB{
        ID:   "123",
        Type: "example",
    }
    fmt.Printf("Using entity from repoB: %v\n", b)
}

RepoA/go.mod 需要添加依赖:

module github.com/yourname/repoA

go 1.21

require github.com/yourname/repoB/entity v0.0.0

replace github.com/yourname/repoB/entity => ../repoB/entity

方案二:使用版本控制

为实体模块打上版本标签,确保稳定性:

发布实体模块:

# 在 repoB/entity 目录中
git tag entity/v1.0.0
git push origin entity/v1.0.0

在 RepoA 中引用特定版本:

// go.mod
require github.com/yourname/repoB/entity v1.0.0

方案三:使用工作区(Go 1.18+)

创建 go.work 文件管理本地开发:

go.work:

go 1.21

use (
    ./repoA
    ./repoA/entity
    ./repoB
    ./repoB/entity
)

完整示例

实体定义最佳实践:

// repoA/entity/entityA.go
package entity

import "time"

// EntityA 只包含数据结构和简单方法
type EntityA struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    CreatedAt time.Time `json:"created_at"`
}

// 可以包含简单的验证方法
func (e *EntityA) Validate() error {
    if e.ID == "" {
        return errors.New("ID is required")
    }
    return nil
}

// 但不要包含业务逻辑或外部依赖

避免循环依赖的关键:

  1. 实体模块只包含数据结构
  2. 不导入业务逻辑包
  3. 使用接口进行解耦

这种多模块方案的优势:

  • 保持仓库独立性
  • 避免不必要的公共依赖
  • 支持精细化的版本控制
  • 符合 Go 模块设计哲学

对于 1 万个仓库的场景,这种方案可以按需建立依赖关系,只有真正需要共享的实体才被其他仓库引用,避免了将所有实体放入单一公共仓库的耦合问题。

回到顶部