2024年十月月 发布的文章

Go项目中使用Git Submodule,还有这个必要吗?

本文永久链接 – https://tonybai.com/2024/10/05/using-git-submodules-in-go-projects

在软件开发中,依赖管理一直是一个重要的议题,特别是在像Go这样的编程语言中,随着项目的扩展,如何有效管理依赖变得至关重要。Git Submodule作为Git的一个重要功能,允许在一个Git仓库中嵌入另一个仓库,从而方便地管理跨项目的代码共享。然而,Go语言引入的Go Module机制似乎已经解决了依赖管理的问题,那么在Go项目中,是否还有使用Git Submodule的必要呢?本文将简单探讨一下Go项目中Git Submodule的使用方法,并分析它是否还值得使用。

1. Git Submodule是什么?

Git Submodule是Git版本管理工具提供的一个功能,允许你将一个Git仓库作为另一个Git仓库(主仓库)的子目录。主仓库通过记录Submodule的URL和commit hash来追踪Submodule。当你克隆一个包含Submodule的仓库时,需要额外的步骤来初始化和更新Submodule。

下面是一个将github.com/rsc/pdf仓库作为git submodule的示例。

我们先建立主仓库:

$mkdir main-project
$cd main-project
$go mod init main-project
$git init
$git add -A
$git commit -m"initial import" .
[master (root-commit) 8227e65] initial import
 1 file changed, 3 insertions(+)
 create mode 100644 go.mod

接下来,我们来添加submodule:

$git submodule add https://github.com/rsc/pdf.git
Cloning into '/Users/tonybai/Test/Go/submodule/main-project/pdf'...
remote: Enumerating objects: 48, done.
remote: Counting objects: 100% (30/30), done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 48 (delta 21), reused 21 (delta 21), pack-reused 18 (from 1)
Unpacking objects: 100% (48/48), done.

$git commit -m "Add rsc/pdf as a submodule"
[master 2778170] Add rsc/pdf as a submodule
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 pdf

git submodule在主仓库的顶层目录下创建一个.gitmodules文件:

$cat .gitmodules
[submodule "pdf"]
    path = pdf
    url = https://github.com/rsc/pdf.git

pdf子目录下的.git不再是目录而是一个文件,其内容指示了pdf仓库的git元数据目录的位置,即主仓库下的.git/modules/pdf下:

$cat pdf/.git
gitdir: ../.git/modules/pdf

git submodule这种机制的主要用途是当多个项目之间有共享代码时,避免将共享的代码直接复制到每个项目中,而是通过Submodule来引用外部仓库。这种方式使得共享代码的版本控制更加明确和独立,也方便了项目之间的更新、管理与版本控制。

通过git submodule status可以查看主仓库下各个submodule的当前状态:

$git submodule status
 c47d69cf462f804ff58ca63c61a8fb2aed76587e pdf (v0.1.0-1-gc47d69c)

通过git submodule update还可以更新各个submodule到最新版本。 但通常在主仓库中会锁定Submodule的特定版本,通过锁定Submodule的版本,可以确保主仓库使用的是经过测试和验证的Submodule代码,这减少了因Submodule更新而导致的意外问题。同时,锁定版本还可以确保所有开发者和构建环境都使用完全相同版本的Submodule,这对于保证构建的一致性和可重现性至关重要。版本锁定让你还可以精确控制何时更新Submodule,你可以在准备好处理潜在的变更和进行必要的测试时,有计划地更新Submodule版本。submodule的版本锁定可以通过下面命令组合实现:

cd path/to/submodule
git checkout <specific-commit-hash>
cd -
git add path/to/submodule
git commit -m "Lock submodule to specific version"

这个提交会更新主仓库中记录的Submodule版本,其他克隆主仓库的人在初始化和更新Submodule时,就会自动获取到这个特定版本。

在以Git为版本管理工具的项目中,Submodule在以下一些场景中还是很有用的:

  • 在多项目依赖场景下,我们可以使用Submodule共享公共库;
  • 在大型单一仓库中,Submodule有助于我们模块化管理各个子项目;
  • 统一对Submodule的版本进行严格管理,避免在更新时引入未测试的新代码。

submodule虽然可以解决一些问题,但由于增加了项目管理复杂度以及学习成本,应用算不上广泛,但也不乏一些知名的开源项目在使用,比如git项目自身、opensslqemu等。

不过,对于Go项目而言,Go Modules是Go在Go 1.11引入的新的官方依赖管理机制,它通过go.mod文件声明依赖关系,通过go.sum文件确保依赖的完整性,实现了构建的可重现性。那么,在Go项目中还有必要引入sub modules吗?

这里我们先不下结论,而是先来看看Go项目引入submodule后该如何使用呢。

2. Go项目的Git Submodule使用方法

在前面我们在本地建立了一个main-project,然后将rsc/pdf作为submodule导入到了main-project中,main-project是一个Go项目,它的go.mod如下:

// main-project/go.mod

module main-project

go 1.23.0

我们现在就继续使用这个示例来看看Go项目中git submodule的使用方法。

我们先来看一种错误的使用方法:使用相对路径

我们在main-project下建立一个main.go的源文件:

// main-project/main.go

package main

import (
    _ "./pdf"
)

func main() {
    println("ok")
}

建完后,整个main-project的目录布局如下:

$tree -F
.
├── go.mod
├── main.go
└── pdf/
    ├── LICENSE
    ├── README.md
    ├── lex.go
    ├── name.go
    ├── page.go
    ├── pdfpasswd/
    │   └── main.go
    ├── ps.go
    ├── read.go
    └── text.go

在第一版main.go中,我们期望使用相对路径来导入submomdule中的pdf包,运行main.go,我们得到下面结果:

$go run main.go
main.go:4:2: "./pdf" is relative, but relative import paths are not supported in module mode

我们看到:在go module构建模式下,Go已经不再支持以相对路径导入Go包了!但是如果我们直接通过rsc.io/pdf这个路径导入,那显然使用的就不是submodule中的pdf包了。

下面我们试试第二种方法,即将pdf目录看成main-project的子目录,将pdf包看成是main-project这个module下的一个包,这样pdf包在main-project这个module下的导入路径就变成了main-project/pdf:

// main-project/main.go
package main

import (
    _ "main-project/pdf"
)

func main() {
    println("ok")
}

这次构建和运行main.go,我们将得到正确的预期结果。

到这里,我们似乎又找到了go module之外go项目依赖管理的新方法,并且这种方法特别适合当某些依赖项目尚未发布,还无法直接通过Go Module导入的库,甚至是一些永远不会发布的内部库或私有库。这种方法让pdf看起来是main-project的一部分,但实际上pdf包的版本却是需要开发人员自己通过git submodule命令管理的,pdf包的版本无法用go.mod(和go.sum)控制,因为它被视为是main-project的一部分了,而不是外部依赖包

如果你不想将其视为main-project的一部分,还想将其以外部依赖的方式管理起来,那就需要利用到go module的replace或go.work了。不过这种方法的前提是submodule下必须是一个go module,即有自己的go.mod。rsc.io/pdf包是一个legacy package,还没有自己的go.mod,我们先在本地pdf目录下为其添加一个go.mod:go mod init rsc.io/pdf。

接下来,我们先来简单看看用replace如何实现导入pdf包,我们需要修改一下main-project/go.mod:

// main-project/go.mod

module main-project

go 1.23.0

require rsc.io/pdf v0.1.1

replace rsc.io/pdf => ./pdf

这里我们用replace指示符将rsc.io/pdf替换为本地pdf目录下的go module,这样修改后,我们运行main.go也会得到正确的结果。

另外我们还可以使用go.work来导入pdf,下面命令初始化一个go.work:

$go work init .

编辑go.work,添加workspace包含的路径:

go 1.23.0

use (
    .
    ./pdf
)

这样go编译器会默认在当前目录和pdf目录下搜索rsc.io/pdf模块,运行main.go也是ok的。

相对于将pdf包看成是main-project module下的一个包并用main-project/pdf这个内部依赖的包导入路径的方法,使用replace或go.work的好处在于一旦pdf包得以发布,main.go可以无需修改pdf包导入路径,并可以基于go.mod精确管理pdf包的版本。

3. 小结

那么我们在Go项目中到底是否有必要使用sub modules呢?我们来小结一下。

总的来说,在大多数情况下,Go Modules确实已经覆盖了Git Submodule在Go项目中的主要功能,甚至做的更好,比如:Go Modules提供了更细粒度的版本控制,能自动解析和下载依赖,并也可以确保了构建的可重现性。因此,对于大多数Go项目而言,使用Go Modules已经足够满足依赖管理需求,而无需再使用git submodule。 并且,在Go项目以及Go社区的实践中,应对类似共享未发布的依赖包的场景(git submodule适用的场景),使用replace或go.work是比较主流的实践,或者说go.work以及replace就是为了这种情况而添加的。

当然如果组织/公司内部尚未构建可以很好地支持内部Go项目间依赖包获取、导入和管理的基础设施,那么git submodule不失为一种可以在内部Go项目中实施的可行的依赖版本管理和控制方案。

最后,无论选择使用Git Submodule、Go Modules,还是两者结合,最重要的是要确保项目结构清晰,依赖关系明确,以便于团队协作和项目维护。


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

探索Go守护进程的实现方法

本文永久链接 – https://tonybai.com/2024/10/03/how-to-daemonize-go-program

在后端开发的世界里,守护进程(daemon)这个概念与Unix系统一样古老。守护进程是在后台运行的长期服务程序,不与任何终端关联。尽管现代进程管理工具如systemdsupervisor等让应用转化为守护进程变得十分简单,我们甚至可以使用以下命令来在后台运行程序:

nohup ./your_go_program &

但在某些情况下,程序的原生转化为守护进程的能力仍然是有必要的。比如分布式文件系统juicefs cli的mount子命令,它就支持以-d选项启动,并以守护进程方式运行:

$juicefs mount -h
NAME:
   juicefs mount - Mount a volume

USAGE:
   juicefs mount [command options] META-URL MOUNTPOINT

... ...

OPTIONS:
   -d, --background  run in background (default: false)
   ... ...
... ...

这种自我守护化的能力会让很多Go程序受益,在这一篇文章中,我们就来探索一下Go应用转化为守护进程的实现方法。

1. 标准的守护进程转化方法

W.Richard Stevens的经典著作《UNIX环境高级编程》中对将程序转化为一个守护进程的 (daemonize) 步骤进行了详细的说明,主要步骤如下:

  • 创建子进程并终止父进程

通过fork()系统调用创建子进程,父进程立即终止,保证子进程不是控制终端的会话组首领。

  • 创建新的会话

子进程调用setsid()来创建一个新会话,成为会话组首领,从而摆脱控制终端和进程组。

  • 更改工作目录

使用chdir(“/”) 将当前工作目录更改为根目录,避免守护进程持有任何工作目录的引用,防止对文件系统卸载的阻止。

  • 重设文件权限掩码

通过umask(0) 清除文件权限掩码,使得守护进程可以自由设置文件权限。

  • 关闭文件描述符

关闭继承自父进程的已经open的文件描述符(通常是标准输入、标准输出和标准错误)。

  • 重定向标准输入/输出/错误

重新打开标准输入、输出和错误,重定向到/dev/null,以避免守护进程无意输出内容到不应有的地方。

注:fork()系统调用是一个较为难理解的调用,它用于在UNIX/Linux系统中创建一个新的进程。新创建的进程被称为子进程,它是由调用fork()的进程(即父进程)复制出来的。子进程与父进程拥有相同的代码段、数据段、堆和栈,但它们是各自独立的进程,有不同的进程ID (PID)。在父进程中,fork()返回子进程的PID(正整数),在子进程中,fork()返回0,如果fork()调用失败(例如系统资源不足),则返回-1,并设置errno以指示错误原因。

下面是一个符合UNIX标准的守护进程转化函数的C语言实现,参考了《UNIX环境高级编程》中的经典步骤:

// daemonize/c/daemon.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <syslog.h>
#include <signal.h>

void daemonize()
{
    pid_t pid;

    // 1. Fork off the parent process
    pid = fork();
    if (pid < 0) {
        exit(EXIT_FAILURE);
    }
    // If we got a good PID, then we can exit the parent process.
    if (pid > 0) {
        exit(EXIT_SUCCESS);
    }

    // 2. Create a new session to become session leader to lose controlling TTY
    if (setsid() < 0) {
        exit(EXIT_FAILURE);
    }

    // 3. Fork again to ensure the process won't allocate controlling TTY in future
    pid = fork();
    if (pid < 0) {
        exit(EXIT_FAILURE);
    }
    if (pid > 0) {
        exit(EXIT_SUCCESS);
    }

    // 4. Change the current working directory to root.
    if (chdir("/") < 0) {
        exit(EXIT_FAILURE);
    }

    // 5. Set the file mode creation mask to 0.
    umask(0);

    // 6. Close all open file descriptors.
    for (int x = sysconf(_SC_OPEN_MAX); x>=0; x--) {
        close(x);
    }

    // 7. Reopen stdin, stdout, stderr to /dev/null
    open("/dev/null", O_RDWR); // stdin
    dup(0);                    // stdout
    dup(0);                    // stderr

    // Optional: Log the daemon starting
    openlog("daemonized_process", LOG_PID, LOG_DAEMON);
    syslog(LOG_NOTICE, "Daemon started.");
    closelog();
}

int main() {
    daemonize();

    // Daemon process main loop
    while (1) {
        // Perform some background task...
        sleep(30); // Sleep for 30 seconds.
    }

    return EXIT_SUCCESS;
}

注:这里省略了书中设置系统信号handler的步骤。

这里的daemonize函数完成了标准的守护化转化过程,并确保了程序在后台无依赖地稳定运行。我们编译运行该程序后,程序进入后台运行,通过ps命令可以查看到类似下面内容:

$ ./c-daemon-app
$ ps -ef|grep c-daemon-app
root     28517     1  0 14:11 ?        00:00:00 ./c-daemon-app

我们看到c-daemon-app的父进程是ppid为1的进程,即linux的init进程。我们看到上面c代码中转化为守护进程的函数daemonize进行了两次fork,至于为何要做两次fork,在我的《理解Zombie和Daemon Process》一文中有说明,这里就不赘述了。

那么Go是否可以参考上述步骤实现Go程序的守护进程转化呢?我们接着往下看。

2. Go语言实现守护进程的挑战

关于Go如何实现守护进程的转换,在Go尚未发布1.0之前的2009年就有issue提到,在runtime: support for daemonize中,Go社区与Go语言的早起元老们讨论了在Go中实现原生守护进程的复杂性,主要挑战源于Go的运行时及其线程管理方式。当一个进程执行fork操作时,只有主线程被复制到子进程中,如果fork前Go程序有多个线程(及多个goroutine)在执行(可能是由于go runtime调度goroutine和gc产生的线程),那么fork后,这些非执行fork线程的线程(以及goroutine)将不会被复制到新的子进程中,这可能会导致后续子进程中线程运行的不确定性(基于一些fork前线程留下的数据状态)。

理想情况下是Go runtime提供类似的daemonize函数,然后在多线程启动之前实现守护进程的转化,不过Go团队至今也没有提供该机制,而是建议大家使用如systemd的第三方工具来实现Go程序的守护进程转化。

既然Go官方不提供方案,Go社区就会另辟蹊径,接下来,我们看看目前Go社区的守护进程解决方案。

3. Go社区的守护进程解决方案

尽管面临挑战,Go社区还是开发了一些库来支持Go守护进程的实现,其中一个star比较多的解决方案是github.com/sevlyar/go-daemon。

go-daemon库的作者巧妙地解决了Go语言中无法直接使用fork系统调用的问题。go-daemon采用了一个简单而有效的技巧来模拟fork的行为:该库定义了一个特殊的环境变量作为标记。程序运行时,首先检查这个环境变量是否存在。如果环境变量不存在,执行父进程相关操作,然后使用os.StartProcess(本质是fork-and-exec)启动带有特定环境变量标记的程序副本。如果环境变量存在,执行子进程相关操作,继续执行主程序逻辑,下面是该库作者提供的原理图:

这种方法有效地模拟了fork的行为,同时避免了Go运行时中与线程和goroutine相关的问题。下面是使用go-daemon包实现Go守护进程的示例:

// daemonize/go-daemon/main.go

package main

import (
    "log"
    "time"

    "github.com/sevlyar/go-daemon"
)

func main() {
    cntxt := &daemon.Context{
        PidFileName: "example.pid",
        PidFilePerm: 0644,
        LogFileName: "example.log",
        LogFilePerm: 0640,
        WorkDir:     "./",
        Umask:       027,
    }

    d, err := cntxt.Reborn()
    if err != nil {
        log.Fatal("无法运行:", err)
    }
    if d != nil {
        return
    }
    defer cntxt.Release()

    log.Print("守护进程已启动")

    // 守护进程逻辑
    for {
        // ... 执行任务 ...
        time.Sleep(time.Second * 30)
    }
}

运行该程序后,通过ps可以查看到对应的守护进程:

$make
go build -o go-daemon-app
$./go-daemon-app 

$ps -ef|grep go-daemon-app
  501  4025     1   0  9:20下午 ??         0:00.01 ./go-daemon-app

此外,该程序会在当前目录下生成example.pid(用于实现file lock),用于防止意外重复执行同一个go-daemon-app:

$./go-daemon-app
2024/09/26 21:21:28 无法运行:daemon: Resource temporarily unavailable

虽然原生守护进程化提供了精细的控制且无需安装和配置外部依赖,但进程管理工具提供了额外的功能,如开机自启、异常退出后的自动重启和日志记录等,并且Go团队推荐使用进程管理工具来实现Go守护进程。进程管理工具的缺点在于需要额外的配置(比如systemd)或安装设置(比如supervisor)。

4. 小结

在Go中实现守护进程化,虽然因为语言运行时的特性而具有挑战性,但通过社区开发的库和谨慎的实现是可以实现的。随着Go语言的不断发展,我们可能会看到更多对进程管理功能的原生支持。同时,开发者可以根据具体需求,在原生守护进程化、进程管理工具或混合方法之间做出选择。

本文涉及的源码可以在这里下载。


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats