Skip to content

Design pattern in Gitlay(Git PRC service)

· 7 min
TL;DR

这篇文章源于偶然看到的一篇文章 # Golang 技巧之默认值设置的高阶玩法,它讲的是 GRPC 中的设计模式。而我没有研究过 GRPC 源码,看起来稍显陌生。但好在手头上有 Gitaly 源码,算是稍微熟悉它的源码,因此想看看这个项目里面是不是也应用到了前面文章里讲的高阶用法,简单搜索一下源码后,发现这种代码模式还挺多的,于是趁热记录了其中一小段阅读结果。

本文深入分析 Gitaly 源码中的函数选项模式(Functional Options Pattern),详细展示了如何通过闭包和可变参数实现灵活配置。以 git-rev-list 命令封装为例,讲解了从结构体定义、选项函数设计到具体实现的完整流程,展现了 Go 语言中优雅处理复杂参数和默认值的高级设计模式,对 Go 开发者构建可维护 API 提供了实用范例。

Gitaly is a Git RPC service for handling all the git calls made by GitLab repo: gitaly: https://gitlab.com/gitlab-org/gitaly

git rev-list 是 Git 中非常重要和有用的命令,它存在许多选项以及子命令,Gitaly 中对这些选项和命令进行了封装。

首先定义了一个 config 结构体,包含不同的配置选项:

type ObjectType string
type revlistConfig struct {
blobLimit int
objects bool
objectType ObjectType
order Order
reverse bool
maxParents uint
disabledWalk bool
firstParent bool
before, after time.Time
author []byte
skipResult func(*RevisionResult) bool
}

然后定义一个函数,该函数的参数是上述结构体,且是指针类型参数,这点非常重要,它使得这个函数能够直接修改入参 cfg

type RevlistOption func(cfg *revlistConfig)

然后是一系列对 git-rev-list 参数进行修改的方法,每个 WithXXX 方法调用前面的匿名函数,并返回 RevlistOption 类型变量:

func WithBlobLimit(limit int) RevlistOption {
return func(cfg *revlistConfig) {
cfg.blobLimit = limit
}
}
func WithObjectTypeFilter(t ObjectType) RevlistOption {
return func(cfg *revlistConfig) {
cfg.objectType = t
}
}
func WithRevrse() RevlistOption {
return func(cfg *revlistConfig) {
cfg.reverse = true
}
}
func WithMaxParents(p uint) RevlistOption {
return func(cfg *revlistConfig) {
cfg.maxParents = p
}
}
func WithDisabledWalk() RevlistOption {
return func(cfg *revlistConfig) {
cfg.disabledWalk = true
}
}
func WithFirstParent() RevlistOption {
return func(cfg *revlistConfig) {
cfg.firstParent = true
}
}
func WithBefore(t time.Time) RevlistOption {
return func(cfg *revlistConfig) {
cfg.before = t
}
}
func WithAfter(t time.Time) RevlistOption {
return func(cfg *revlistConfig) {
cfg.after = t
}
}
func WithAuthor(author []byte) RevlistOption {
return func(cfg *revlistConfig) {
cfg.author = author
}
}
// 省略...

git-rev-list 方法定义如下:

func Revlist(
ctx context.Context,
repo *localrepo.Repo,
revisions []string,
options ...RevlistOption,
) RevisionIterator

在 Revlist() 中最后一个参数 options ...RevlistOption,它表示是不定长变参列表 具体参数数量交给调用者。

具体实现如下:

func Revlist(
ctx context.Context,
repo *localrepo.Repo,
revisions []string,
options ...RevlistOption,
) RevisionIterator {
// 定义一个 config 配置变量
var cfg revlistConfig
// 注意这里,遍历 options 中每一个方法,每个方法都会对 config 变量进行赋值、修改
for _, option := range options {
option(&cfg)
}
resultChan := make(chan RevisionResult)
// 使用 goroutine 丢到后台运行
go func() {
defer close(resultChan)
// 定义 git option 变量 flags
flags := []git.Option{}
// 下面几项都是根据 cfg 配置,更新 flag 变量
if cfg.objects {
flags = append(flags,
git.Flag{Name: "--in-commit-order"},
git.Flag{Name: "--objects"},
git.Flag{Name: "--object-names"},
)
}
if cfg.blobLimit > 0 {
flags = append(flags, git.Flag{
Name: fmt.Sprintf("--filter=blob:limit=%d", cfg.blobLimit),
})
}
if cfg.objectType != "" {
flags = append(flags,
git.Flag{Name: fmt.Sprintf("--filter=object:type=%s", cfg.objectType)},
git.Flag{Name: "--filter-provided-objects"},
)
}
/// 此处省略更多项 cfg 配置
// 配置完成后,执行 git-rev-list 命令
var stderr strings.Builder
revlist, err := repo.Exec(ctx,
git.SubCmd{
Name: "rev-list",
Flags: flags,
Args: revisions,
},
git.WithStderr(&stderr),
)
// 此处省略错误处理
// 对结果的每行进行处理
scanner := bufio.NewScanner(revlist)
for scanner.Scan() {
line := make([]byte, len(scanner.Bytes()))
copy(line, scanner.Bytes())
oidAndName := bytes.SplitN(line, []byte{' '}, 2)
result := RevisionResult{
OID: git.ObjectID(oidAndName[0]),
}
// 省略...
if isDone := sendRevisionResult(ctx, resultChan, result); isDone {
return
}
}
// scan 结束
if err := scanner.Err(); err != nil {
sendRevisionResult(ctx, resultChan, RevisionResult{
err: fmt.Errorf("scanning rev-list output: %w", err),
})
return
}
// 等待 goruntine 结束
if err := revlist.Wait(); err != nil {
sendRevisionResult(ctx, resultChan, RevisionResult{
err: fmt.Errorf("rev-list pipeline command: %w, stderr: %q", err, stderr.String()),
})
return
}
}()
// 返回结果
return &revisionIterator{
ch: resultChan,
}
}

那么如何调用这个方法呢?搜索 Gitaly 源码可以看到:

ListBlobs 中是这样用的:

func (s *server) ListBlobs(req *gitalypb.ListBlobsRequest, stream gitalypb.BlobService_ListBlobsServer) error {
// 省略...
// 定义一个 RevlistOption 变量,向里面注入多个配置选项
revlistOptions := []gitpipe.RevlistOption{
gitpipe.WithObjects(),
gitpipe.WithObjectTypeFilter(gitpipe.ObjectTypeBlob),
}
// 调用 Revlist()
revlistIter := gitpipe.Revlist(ctx, repo, req.GetRevisions(), revlistOptions...)
// 省略...
}

ListCommits 中根据请求中不同的 case 分别向 revlistOptions 变量追加不同的配置选项:

func (s *server) ListCommits(
request *gitalypb.ListCommitsRequest,
stream gitalypb.CommitService_ListCommitsServer,
) error {
// 省略...
ctx := stream.Context()
repo := s.localrepo(request.GetRepository())
// 定义一个 revlistOptions 变量
revlistOptions := []gitpipe.RevlistOption{}
// 省略...
/// 根据不同的 case 向 revlistOptions 追加不同的配置
if request.GetReverse() {
revlistOptions = append(revlistOptions, gitpipe.WithReverse())
}
if request.GetMaxParents() > 0 {
revlistOptions = append(revlistOptions, gitpipe.WithMaxParents(uint(request.GetMaxParents())))
}
if request.GetDisableWalk() {
revlistOptions = append(revlistOptions, gitpipe.WithDisabledWalk())
}
if request.GetFirstParent() {
revlistOptions = append(revlistOptions, gitpipe.WithFirstParent())
}
if request.GetBefore() != nil {
revlistOptions = append(revlistOptions, gitpipe.WithBefore(request.GetBefore().AsTime()))
}
if request.GetAfter() != nil {
revlistOptions = append(revlistOptions, gitpipe.WithAfter(request.GetAfter().AsTime()))
}
if len(request.GetAuthor()) != 0 {
revlistOptions = append(revlistOptions, gitpipe.WithAuthor(request.GetAuthor()))
}
// 调用 Revlist()
revlistIter := gitpipe.Revlist(ctx, repo, request.GetRevisions(), revlistOptions...)
// 省略后续...
return nil
}

ListLFSPointers 中省略定义 RevlistOption 变量,直接利用 Revlist 方法中不定参数特性,添加不同的配置选项:

func (s *server) ListLFSPointers(in *gitalypb.ListLFSPointersRequest, stream gitalypb.BlobService_ListLFSPointersServer) error {
ctx := stream.Context()
// 省略...
repo := s.localrepo(in.GetRepository())
// 省略...
// 调用 Revlist()
revlistIter := gitpipe.Revlist(ctx, repo, in.GetRevisions(),
gitpipe.WithObjects(),
gitpipe.WithBlobLimit(lfsPointerMaxSize),
gitpipe.WithObjectTypeFilter(gitpipe.ObjectTypeBlob),
)
/// 省略后续...
}

总结:

这种设计模式对于多配置,多参数的方法非常适合,虽然代码实现起来有点麻烦,但是可读性强,使用灵活。 这种模式还有一些变体,后面有时间再记录一下。