Golang中如何同时使用同一依赖的不同分支版本

Golang中如何同时使用同一依赖的不同分支版本 我目前遇到了一个似乎相当独特的问题。希望有人能帮助我解决。

本质上,我正在尝试支持两个不同的项目 AO,它们都是基于 E 构建的。有一个针对 E 的大型 Go 库,我们称之为 lib-E,但为了支持其他项目,它们发布了自己的库,这些库是 lib-E 的直接分支。

值得注意的是,lib-Alib-O 在整个代码库中没有任何导入路径,并且 go.mod 项目定义在所有三个库中都是相同的:github.com/maintainer/lib-E。我推测这样做是为了最小化维护工作,并允许库的使用者更容易地采用即插即用的方法。

也许你现在已经开始看到我的问题了。假设我的代码库中有两个包,其中一个应该针对 lib-A 构建,另一个针对 lib-O 构建。这两个包的代码是相同的,但由于它们底层依赖的库不同,编译输出也会不同。

到目前为止,我已经尝试了几种方法,但都没有成功:

  • 将两个包提取到它们自己的仓库中,每个都有自己的 go.mod,每个都包含将 lib-E 替换为 lib-Alib-Oreplace 语句。这失败了,因为我的主 go.mod 声明了对 lib-E 的直接依赖,这将覆盖模块替换。
  • 将代码保留在一个仓库中,但在两个包中使用命名导入(例如 import libA "github.com/maintainer/libE"),然后在主 go.mod 中使用两个 replace 指令,分别指向 libAlibO 的各个仓库。这失败了,因为两个模块仍然被声明为 github.com/maintainer/libE,所以我最终得到:
module declares its path as: github.com/maintainer/libE
       but was required as: libA

就目前情况而言,我开始认为我试图做的事情在 Go 中是不可能的,唯一的解决方案是构建两个小二进制文件(每个都直接针对 libAlibO),然后在这些小的适配器二进制文件和我的主应用程序之间使用 RPC。

显然,如果可能的话,我希望避免这种大量的开销。有人能为这个问题想出更好的解决方案吗?


更多关于Golang中如何同时使用同一依赖的不同分支版本的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

你好 @byte-bandit,欢迎来到论坛。

我不太确定是否完全理解了这个问题,但在我看来,这似乎是问题的根源:

Notably, lib-A and lib-O do not have any import paths across the code base, and the go.mod project definition is identical across all three libraries: github.com/maintainer/lib-E

如果 lib-Alib-Olib-E 的分支,那么它们的 go.mod 文件应该反映出这一点,AO 中的导入路径也应分别反映这一点。

如果无法更改 go.mod 文件和导入路径,你可以尝试使用 Go 工作区在本地替换导入路径。(教程参考

更多关于Golang中如何同时使用同一依赖的不同分支版本的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


感谢你的回复,非常感谢你的意见。让我更清楚地说明一下我的动机。

看看 https://github.com/ethereum-optimism/op-gethhttps://github.com/OffchainLabs/go-ethereum/ —— 它们都是 github.com/ethereum/go-ethereum 的分支(抱歉,我只能发布两个链接)。请注意,它们都没有修改它们的 go.mod 文件,也没有修改它们的导入路径。我只能推测,这样做是为了让任何使用者只需在他们自己的 go.mod 文件中使用 replace 关键字,就可以将其中任何一个作为原始模块的直接替代品。

我正在维护一个现有的代码库,它使用了 op-geth,现在希望通过使用 OfficialLabs 实现创建一个模块来引入对 Arbitrum 的支持。请注意,它们都指向 github.com/ethereum/go-ethereum。当只支持 op-geth 时,这不成问题,多亏了 replace。但是,当需要引入第二个使用相同导入路径的依赖项时,到目前为止这被证明是不可能的。

我已经尝试了我能想到的各种选项,比如将代码提取到单独的模块中、使用工作区、使用命名导入,我甚至不记得在这个阶段还尝试过什么。

我需要的是一个可靠的模块级重定向方法,并且不会被主模块覆盖。这在 Go 工作区中似乎是不允许的。

目前,我不确定这在 go 中是否可行,但很可能我只是没有看到显而易见的方法。

在 Go 模块系统中,确实无法直接同时使用同一模块路径的不同版本。不过,可以通过以下方法解决:

方案一:使用 replace + 不同模块路径

将分支版本发布到不同的模块路径:

// 主 go.mod
module myproject

go 1.21

require (
    github.com/maintainer/lib-a v0.0.0
    github.com/maintainer/lib-o v0.0.0
)

replace (
    github.com/maintainer/lib-a => ./vendor/lib-a
    github.com/maintainer/lib-o => ./vendor/lib-o
)

然后修改分支库的 go.mod

// lib-a/go.mod
module github.com/maintainer/lib-a

go 1.21

// lib-o/go.mod  
module github.com/maintainer/lib-o

go 1.21

方案二:使用构建标签隔离

创建带构建标签的包装器:

// +build liba

package adapter

import (
    lib "github.com/maintainer/lib-e"
)

func New() lib.Interface {
    return lib.New()
}
// +build libo

package adapter

import (
    lib "github.com/maintainer/lib-o"  // 不同的模块路径
)

func New() lib.Interface {
    return lib.New()
}

方案三:使用接口抽象

定义通用接口,通过依赖注入使用不同实现:

// common/interface.go
package common

type LibInterface interface {
    DoSomething() error
    GetValue() string
}

// a/adapter.go
package a

import (
    "github.com/maintainer/lib-a"
    "myproject/common"
)

type Adapter struct {
    client *lib.Client
}

func NewAdapter() common.LibInterface {
    return &Adapter{
        client: lib.NewClient(),
    }
}

func (a *Adapter) DoSomething() error {
    return a.client.DoSomething()
}

// o/adapter.go
package o

import (
    "github.com/maintainer/lib-o"
    "myproject/common"
)

type Adapter struct {
    client *lib.Client
}

func NewAdapter() common.LibInterface {
    return &Adapter{
        client: lib.NewClient(),
    }
}

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

创建 go.work 文件管理多个模块:

// go.work
go 1.21

use (
    ./main
    ./lib-a-module
    ./lib-o-module
)

replace (
    github.com/maintainer/lib-e => ./lib-a-module
)
# 构建不同版本
go work use ./lib-a-module
go build -tags liba ./cmd/a-app

go work use ./lib-o-module  
go build -tags libo ./cmd/o-app

方案五:使用代理重写(高级)

设置本地代理服务器重写导入路径:

// goproxy 配置
// proxy.go
package main

import (
    "net/http"
    "net/http/httputil"
    "strings"
)

func main() {
    http.HandleFunc("/github.com/maintainer/lib-e/@v/", func(w http.ResponseWriter, r *http.Request) {
        if strings.Contains(r.URL.Path, "lib-a") {
            // 重定向到 lib-a
            httputil.NewSingleHostReverseProxy("https://github.com/maintainer/lib-a").ServeHTTP(w, r)
        } else {
            // 重定向到 lib-o
            httputil.NewSingleHostReverseProxy("https://github.com/maintainer/lib-o").ServeHTTP(w, r)
        }
    })
    http.ListenAndServe(":8080", nil)
}
GOPROXY=http://localhost:8080,direct go build

最实用的方案是方案一,通过修改分支库的模块路径来区分不同版本。虽然需要维护者配合修改,但这是最符合 Go 模块设计的方式。

回到顶部