当前位置:首页 > 科技  > 软件

Golang 状态机设计模式,你知道多少?

来源: 责编: 时间:2024-05-27 17:24:30 91观看
导读导言在我们开发的许多项目中,都需要依赖某种运行状态从而实现连续操作。这方面的例子包括:解析配置语言、编程语言等在系统、路由器、集群上执行操作...ETL(Extract Transform Load,提取转换加载)很久以前,Rob Pike 有一个

导言

在我们开发的许多项目中,都需要依赖某种运行状态从而实现连续操作。Ug928资讯网——每日最新资讯28at.com

这方面的例子包括:Ug928资讯网——每日最新资讯28at.com

  • 解析配置语言、编程语言等
  • 在系统、路由器、集群上执行操作...
  • ETL(Extract Transform Load,提取转换加载)

很久以前,Rob Pike 有一个关于 Go 中词法扫描[2]的演讲,内容很讲座,我看了好几遍才真正理解。但演讲中介绍的最基本知识之一就是某个版本的 Go 状态机。Ug928资讯网——每日最新资讯28at.com

该状态机利用了 Go 的能力,即从函数中创建类型并将函数赋值给变量。Ug928资讯网——每日最新资讯28at.com

他在演讲中介绍的状态机功能强大,打破了让函数执行 if/else 并调用下一个所需函数的逻辑。取而代之的是,每个状态都会返回下一个需要调用的函数。Ug928资讯网——每日最新资讯28at.com

这样就能将调用链分成更容易测试的部分。Ug928资讯网——每日最新资讯28at.com

Ug928资讯网——每日最新资讯28at.com

调用链

下面是一个用简单的调用链来完成任务的例子:Ug928资讯网——每日最新资讯28at.com

func Caller(args Args) {  callA(args)  callB(args)}

Ug928资讯网——每日最新资讯28at.com

func Caller(args Args) {  callA(args)}func callA(args Args) {  callB(args)}func callB(args Args) {  return}

两种方法都表示调用链,其中 Caller() 调用 callA(),并最终调用 callB(),从中可以看到这一系列调用是如何执行的。Ug928资讯网——每日最新资讯28at.com

当然,这种设计没有任何问题,但当调用者远程调用其他系统时,必须对这些远程调用进行模拟/打桩,以提供密封测试。Ug928资讯网——每日最新资讯28at.com

你可能还想实现条件调用链,即根据某些参数或状态,在特定条件下通过 if/else 调用不同函数。Ug928资讯网——每日最新资讯28at.com

这就意味着,要对 Caller() 进行密封测试,可能需要处理整个调用链中的桩函数。如果有 50 个调用层级,则可能需要对被测函数下面每个层级的所有函数进行模拟/打桩。Ug928资讯网——每日最新资讯28at.com

这正是 Pike 的状态机设计大显身手的地方。Ug928资讯网——每日最新资讯28at.com

状态机模式

首先定义状态:Ug928资讯网——每日最新资讯28at.com

type State[T any] func(ctx context.Context, args T) (T, State[T], error)

状态表示为函数/方法,接收一组参数(任意类型 T),并返回下一个状态及其参数或错误信息。Ug928资讯网——每日最新资讯28at.com

如果返回的状态为 nil,那么状态机将停止运行。如果设置了 error,状态机也将停止运行。因为返回的是下一个要运行的状态,所以根据不同的条件,会有不同的下一个状态。Ug928资讯网——每日最新资讯28at.com

这个版本与 Pike 的状态机的不同之处在于这里包含了泛型并返回 T。这样我们就可以创建纯粹的函数式状态机(如果需要的话),可以返回某个类型,并将其传递给下一个状态。Pike 最初实现状态机设计时还没有使用泛型。Ug928资讯网——每日最新资讯28at.com

为了实现这一目标,需要一个状态驱动程序:Ug928资讯网——每日最新资讯28at.com

func Run[T any](ctx context.Context, args T, start State[T] "T any") (T, error) {  var err error  current := start  for {    if ctx.Err() != nil {      return args, ctx.Err()    }    args, current, err = current(ctx, args)    if err != nil {      return args, err    }    if current == nil {      return args, nil    }  }}

寥寥几行代码,我们就有了一个功能强大的状态驱动程序。Ug928资讯网——每日最新资讯28at.com

下面来看一个例子,在这个例子中,我们为集群中的服务关闭操作编写了状态机:Ug928资讯网——每日最新资讯28at.com

package remove...// storageClient provides the methods on a storage service// that must be provided to use Remove().type storageClient interface {  RemoveBackups(ctx context.Context, service string, mustKeep int) error  RemoveContainer(ctx context.Context, service string) error}// serviceClient provides methods to do operations for services // within a cluster.type servicesClient interface {  Drain(ctx context.Context, service string) error  Remove(ctx context.Context, service string) error  List(ctx context.Context) ([]string, error)  HasStorage(ctx context.Context, service string) (bool, error)}

这里定义了几个需要客户实现的私有接口,以便从集群中移除服务。Ug928资讯网——每日最新资讯28at.com

我们定义了私有接口,以防止他人使用我们的定义,但会通过公有变量公开这些接口。这样,我们就能与客户保持松耦合,保证只使用我们需要的方法。Ug928资讯网——每日最新资讯28at.com

// Args are arguments to Service().type Args struct {  // Name is the name of the service.  Name string    // Storage is a client that can remove storage backups and storage  // containers for a service.  Storage storageClient  // Services is a client that allows the draining and removal of  // a service from the cluster.  Services servicesClient}func (a Args) validate(ctx context.Context) error {  if a.Name == "" {    return fmt.Errorf("Name cannot be an empty string")  }  if a.Storage == nil {    return fmt.Errorf("Storage cannot be nil")  }  if a.Services == nil {    return fmt.Errorf("Services cannot be nil")  }  return nil}

这里设置了要通过状态传递的参数,可以将在一个状态中设置并传递到另一个状态的私有字段包括在内。Ug928资讯网——每日最新资讯28at.com

请注意,Args 并非指针。Ug928资讯网——每日最新资讯28at.com

由于我们修改了 Args 并将其传递给每个状态,因此不需要给垃圾回收器增加负担。对于像这样操作来说,这点节约微不足道,但在工作量大的 ETL 管道中,节约的时间可能就很明显了。Ug928资讯网——每日最新资讯28at.com

实现中包含 validate() 方法,用于测试参数是否满足使用的最低基本要求。Ug928资讯网——每日最新资讯28at.com

// Service removes a service from a cluster and associated storage.// The last 3 storage backups are retained for whatever the storage retainment// period is.func Service(ctx context.Context, args Args) error {  if err := args.validate(); err != nil {    return err  }    start := drainService  _, err := Run[Args](ctx, args, start "Args")  if err != nil {    return fmt.Errorf("problem removing service %q: %w", args.Name, err)  }  return nil}

用户只需调用 Service(),传入 Args,如果出错就会收到错误信息。用户不需要看到状态机模式,也不需要理解状态机模式就能执行操作。Ug928资讯网——每日最新资讯28at.com

我们只需验证 Args 是否正确,将状态机的起始状态设置为名为 drainService 的函数,然后调用上面定义的 Run() 函数即可。Ug928资讯网——每日最新资讯28at.com

func drainService(ctx context.Context, args Args) (Args, State[Args], error) {  l, err := args.Services.List(ctx)  if err != nil {    return args, nil, err  }  found := false  for _, entry := range l {    if entry == args.Name {      found = true      break    }  }  if !found {    return args, nil, fmt.Errorf("the service was not found")  }  if err := args.Services.Drain(ctx, args.Name); err != nil {    return args, nil, fmt.Errorf("problem draining the service: %w", err)  }  return args, removeService, nil}

我们的第一个状态叫做 drainService(),实现了上面定义的状态类型。Ug928资讯网——每日最新资讯28at.com

它使用 Args 中定义的 Services 客户端列出集群中的所有服务,如果找不到服务,就会返回错误并结束状态机。Ug928资讯网——每日最新资讯28at.com

如果找到服务,就会对服务执行关闭。一旦完成,就进入下一个状态,即 removeService()。Ug928资讯网——每日最新资讯28at.com

func removeService(ctx context.Context, args Args) (Args, State[Args], error) {  if err := args.Services.Remove(ctx, args.Name); err != nil {    return args, nil, fmt.Errorf("could not remove the service: %w", err)  }  hasStorage, err := args.Services.HasStorage(ctx, args.Name)  if err != nil {    return args, nil, fmt.Errorf("HasStorage() failed: %w", err)  }  if hasStorage{    return args, removeBackups, nil  }  return args, nil, nil}

removeService() 使用我们的 Services 客户端将服务从群集中移除。Ug928资讯网——每日最新资讯28at.com

调用 HasStorage() 方法确定是否有存储,如果有,就会进入 removeBackups() 状态,否则就会返回 args, nil, nil,这将导致状态机在无错误的情况下退出。Ug928资讯网——每日最新资讯28at.com

这个示例说明如何根据 Args 中的信息或代码中的远程调用在状态机中创建分支。Ug928资讯网——每日最新资讯28at.com

其他状态调用由你自行决定。我们看看这种设计如何更适合测试此类操作。Ug928资讯网——每日最新资讯28at.com

测试优势

这种模式首先鼓励的是小块的可测试代码,模块变得很容易分割,这样当代码块变得太大时,只需创建新的状态来隔离代码块。Ug928资讯网——每日最新资讯28at.com

但更大的优势在于无需进行大规模端到端测试。由于操作流程中的每个阶段都需要调用下一阶段,因此会出现以下情况:Ug928资讯网——每日最新资讯28at.com

  • 顶层调用者按一定顺序调用所有子函数
  • 每个调用者都会调用下一个函数
  • 两者的某种混合

两者都会导致某种类型的端到端测试,而这种测试本不需要。Ug928资讯网——每日最新资讯28at.com

如果我们对顶层调用者方法进行编码,可能看起来像这样:Ug928资讯网——每日最新资讯28at.com

func Service(ctx context.Context, args Args) error {  ...  if err := drainService(ctx, args); err != nil {    return err  }  if err := removeService(ctx, args); err != nil {    return err  }  hasStorage, err := args.Services.HasStorage(ctx, args.Name)  if err != nil {    return err  }  if hasStorage{    if err := removeBackups(ctx, args); err != nil {      return err    }    if err := removeStorage(ctx, args); err != nil {      return err    }  }  return nil} 

如你所见,可以为所有子函数编写单独的测试,但要测试 Service(),现在必须对调用的所有客户端或方法打桩。这看起来就像是端到端测试,而对于这类代码来说,通常不是好主意。Ug928资讯网——每日最新资讯28at.com

如果转到功能调用链,情况也不会好到哪里去:Ug928资讯网——每日最新资讯28at.com

func Service(ctx context.Context, args Args) error {  ...  return drainService(ctx, args)}func drainService(ctx context.Context, args Args) (Args, error) {  ...  return removeService(ctx, args)}func removeService(ctx context.Context, args Args) (Args, error) {  ...  hasStorage, err := args.Services.HasStorage(ctx, args.Name)  if err != nil {    return args, fmt.Errorf("HasStorage() failed: %w", err)  }    if hasStorage{    return removeBackups(ctx, args)  }  return nil}...

当我们测试时,越接近调用链的顶端,测试的实现就变得越困难。在 Service() 中,必须测试 drainService()、removeService() 以及下面所有调用。Ug928资讯网——每日最新资讯28at.com

有几种方法可以做到,但都不太好。Ug928资讯网——每日最新资讯28at.com

如果使用状态机,只需测试每个阶段是否按要求运行,并返回想要的下一阶段。Ug928资讯网——每日最新资讯28at.com

顶层调用者甚至不需要测试,它只是调用 validate() 方法,并调用应该能够被测试的 Run() 函数。Ug928资讯网——每日最新资讯28at.com

我们为 drainService() 编写一个表驱动测试,这里会拷贝一份 drainService() 代码,这样就不用返回到前面看代码了。Ug928资讯网——每日最新资讯28at.com

func drainService(ctx context.Context, args Args) (Args, State[Args], error) {  l, err := args.Services.List(ctx)  if err != nil {    return args, nil, err  }  found := false  for _, entry := range l {    if entry == args.Name {      found = true      break    }  }  if !found {    return args, nil, fmt.Errorf("the service was not found")  }  if err := args.Services.Drain(ctx, args.Name); err != nil {    return args, nil, fmt.Errorf("problem draining the service: %w", err)  }  return args, removeService, nil}func TestDrainSerivce(t *testing.T) {  t.Parallel()  tests := []struct {    name      string    args      Args    wantErr   bool    wantState State[Args]  }{    {      name: "Error: Services.List() returns an error",      args: Args{        Services: &fakeServices{          list: fmt.Errorf("error"),        },      },      wantErr: true,    },    {      name: "Error: Services.List() didn't contain our service name",      args: Args{        Name: "myService",        Services: &fakeServices{          list: []string{"nope", "this", "isn't", "it"},        },      },      wantErr: true,    },    {      name: "Error: Services.Drain() returned an error",      args: Args{        Name: "myService",        Services: &fakeServices{          list:  []string{"yes", "mySerivce", "is", "here"},          drain: fmt.Errorf("error"),        },      },      wantErr: true,    },    {      name: "Success",      args: Args{        Name: "myService",        Services: &fakeServices{          list:  []string{"yes", "myService", "is", "here"},          drain: nil,        },      },      wantState: removeService,    },  }  for _, test := range tests {    _, nextState, err := drainService(context.Background(), test.args)    switch {    case err == nil && test.wantErr:      t.Errorf("TestDrainService(%s): got err == nil, want err != nil", test.name)      continue    case err != nil && !test.wantErr:      t.Errorf("TestDrainService(%s): got err == %s, want err == nil", test.name, err)      continue    case err != nil:      continue    }      gotState := methodName(nextState)    wantState := methodName(test.wantState)    if gotState != wantState {      t.Errorf("TestDrainService(%s): got next state %s, want %s", test.name, gotState, wantState)    }  }}

可以在 Go Playground[3]玩一下。Ug928资讯网——每日最新资讯28at.com

如你所见,这避免了测试整个调用链,同时还能确保测试调用链中的下一个函数。Ug928资讯网——每日最新资讯28at.com

这些测试很容易划分,维护人员也很容易遵循。Ug928资讯网——每日最新资讯28at.com

其他可能性

这种模式也有变种,即根据 Args 中设置的字段确定状态,并跟踪状态的执行以防止循环。Ug928资讯网——每日最新资讯28at.com

在第一种情况下,状态机软件包可能是这样的:Ug928资讯网——每日最新资讯28at.com

type State[T any] func(ctx context.Context, args T) (T, State[T], error)type Args[T] struct {  Data T  Next State}func Run[T any](ctx context.Context, args Args[T], start State[T] "T any") (T, error) {  var err error  current := start  for {    if ctx.Err() != nil {      return args, ctx.Err()    }    args, current, err = current(ctx, args)    if err != nil {      return args, err    }    current = args.Next // Set our next stage    args.Next = nil // Clear this so to prevent infinite loops    if current == nil {      return args, nil    }  }}

可以很容易的将分布式跟踪或日志记录集成到这种设计中。Ug928资讯网——每日最新资讯28at.com

如果希望推送大量数据并利用并发优势,不妨试试 stagedpipe 软件包[4],其内置了大量高级功能,可以看视频和 README 学习如何使用。Ug928资讯网——每日最新资讯28at.com

希望这篇文章能让你充分了解 Go 状态机设计模式,现在你的工具箱里多了一个强大的新工具。Ug928资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-91031-0.htmlGolang 状态机设计模式,你知道多少?

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: Python 字符串格式化方法性能与可读性对比

下一篇: Node 之父新作:一个全新的 NPM 下载源工具!

标签:
  • 热门焦点
  • 掘力计划第 20 期:Flutter 混合开发的混乱之治

    掘力计划第 20 期:Flutter 混合开发的混乱之治

    在掘力计划系列活动第20场,《Flutter 开发实战详解》作者,掘金优秀作者,Github GSY 系列目负责人恋猫的小郭分享了Flutter 混合开发的混乱之治。Flutter 基于自研的 Skia 引擎
  • Flowable工作流引擎的科普与实践

    Flowable工作流引擎的科普与实践

    一.引言当我们在日常工作和业务中需要进行各种审批流程时,可能会面临一系列技术和业务上的挑战。手动处理这些审批流程可能会导致开发成本的增加以及业务复杂度的上升。在这
  • 在线图片编辑器,支持PSD解析、AI抠图等

    在线图片编辑器,支持PSD解析、AI抠图等

    自从我上次分享一个人开发仿造稿定设计的图片编辑器到现在,不知不觉已过去一年时间了,期间我经历了裁员失业、面试找工作碰壁,寒冬下一直没有很好地履行计划.....这些就放在日
  • 这款新兴工具平台,让你的电脑效率翻倍

    这款新兴工具平台,让你的电脑效率翻倍

    随着信息技术的发展,我们获取信息的渠道越来越多,但是处理信息的效率却成为一个瓶颈。于是各种工具应运而生,都在争相解决我们的工作效率问题。今天我要给大家介绍一款效率
  • 2023年,我眼中的字节跳动

    2023年,我眼中的字节跳动

    此时此刻(2023年7月),字节跳动从未上市,也从未公布过任何官方的上市计划;但是这并不妨碍它成为中国最受关注的互联网公司之一。从2016-17年的抖音强势崛起,到2018年的“头腾
  • 拼多多APP上线本地生活入口,群雄逐鹿万亿市场

    拼多多APP上线本地生活入口,群雄逐鹿万亿市场

    Tech星球(微信ID:tech618)文 | 陈桥辉 Tech星球独家获悉,拼多多在其APP内上线了“本地生活”入口,位置较深,位于首页的“充值中心”内,目前主要售卖美食相关的
  • 网红炒股不为了赚钱,那就是耍流氓!

    网红炒股不为了赚钱,那就是耍流氓!

    来源:首席商业评论6月26日高调宣布入市,网络名嘴大v胡锡进居然进军了股市。在一次财经媒体峰会上,几个财经圈媒体大佬就“胡锡进炒股是否知道认真报道”展开讨论。有
  • 华为和江淮汽车合作开发百万元问界MPV?双方回应来了

    华为和江淮汽车合作开发百万元问界MPV?双方回应来了

    8月1日消息,郭明錤今天在社交平台发文称,华为正在和江淮汽车合作,开发售价在100万元的问界MPV,预计在2024年第2季度量产,销量目标为上市首年交付5万辆。
  • Counterpoint :OPPO双旗舰战略全面落地 高端产品销量增长22%

    Counterpoint :OPPO双旗舰战略全面落地 高端产品销量增长22%

    2023年6月30日,全球行业分析机构Counterpoint Research发布的《中国智能手机高端市场白皮书》显示,中国智能手机品牌正在寻求高质量发展,中国高端智能
Top