国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

Golang 狀態機設計模式,你知道多少?

開發
本文介紹了Golang狀態機模式的一個實現示例,通過該模式,可以解耦調用鏈,有助于實現測試友好的代碼,提高代碼質量。

導言

在我們開發的許多項目中,都需要依賴某種運行狀態從而實現連續操作。

這方面的例子包括:

  • 解析配置語言、編程語言等
  • 在系統、路由器、集群上執行操作...
  • ETL(Extract Transform Load,提取轉換加載)

很久以前,Rob Pike 有一個關于 Go 中詞法掃描[2]的演講,內容很講座,我看了好幾遍才真正理解。但演講中介紹的最基本知識之一就是某個版本的 Go 狀態機。

該狀態機利用了 Go 的能力,即從函數中創建類型并將函數賦值給變量。

他在演講中介紹的狀態機功能強大,打破了讓函數執行 if/else 并調用下一個所需函數的邏輯。取而代之的是,每個狀態都會返回下一個需要調用的函數。

這樣就能將調用鏈分成更容易測試的部分。

調用鏈

下面是一個用簡單的調用鏈來完成任務的例子:

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

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

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

func callB(args Args) {
  return
}

兩種方法都表示調用鏈,其中 Caller() 調用 callA(),并最終調用 callB(),從中可以看到這一系列調用是如何執行的。

當然,這種設計沒有任何問題,但當調用者遠程調用其他系統時,必須對這些遠程調用進行模擬/打樁,以提供密封測試。

你可能還想實現條件調用鏈,即根據某些參數或狀態,在特定條件下通過 if/else 調用不同函數。

這就意味著,要對 Caller() 進行密封測試,可能需要處理整個調用鏈中的樁函數。如果有 50 個調用層級,則可能需要對被測函數下面每個層級的所有函數進行模擬/打樁。

這正是 Pike 的狀態機設計大顯身手的地方。

狀態機模式

首先定義狀態:

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

狀態表示為函數/方法,接收一組參數(任意類型 T),并返回下一個狀態及其參數或錯誤信息。

如果返回的狀態為 nil,那么狀態機將停止運行。如果設置了 error,狀態機也將停止運行。因為返回的是下一個要運行的狀態,所以根據不同的條件,會有不同的下一個狀態。

這個版本與 Pike 的狀態機的不同之處在于這里包含了泛型并返回 T。這樣我們就可以創建純粹的函數式狀態機(如果需要的話),可以返回某個類型,并將其傳遞給下一個狀態。Pike 最初實現狀態機設計時還沒有使用泛型。

為了實現這一目標,需要一個狀態驅動程序:

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
    }
  }
}

寥寥幾行代碼,我們就有了一個功能強大的狀態驅動程序。

下面來看一個例子,在這個例子中,我們為集群中的服務關閉操作編寫了狀態機:

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)
}

這里定義了幾個需要客戶實現的私有接口,以便從集群中移除服務。

我們定義了私有接口,以防止他人使用我們的定義,但會通過公有變量公開這些接口。這樣,我們就能與客戶保持松耦合,保證只使用我們需要的方法。

// 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
}

這里設置了要通過狀態傳遞的參數,可以將在一個狀態中設置并傳遞到另一個狀態的私有字段包括在內。

請注意,Args 并非指針。

由于我們修改了 Args 并將其傳遞給每個狀態,因此不需要給垃圾回收器增加負擔。對于像這樣操作來說,這點節約微不足道,但在工作量大的 ETL 管道中,節約的時間可能就很明顯了。

實現中包含 validate() 方法,用于測試參數是否滿足使用的最低基本要求。

// 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,如果出錯就會收到錯誤信息。用戶不需要看到狀態機模式,也不需要理解狀態機模式就能執行操作。

我們只需驗證 Args 是否正確,將狀態機的起始狀態設置為名為 drainService 的函數,然后調用上面定義的 Run() 函數即可。

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(),實現了上面定義的狀態類型。

它使用 Args 中定義的 Services 客戶端列出集群中的所有服務,如果找不到服務,就會返回錯誤并結束狀態機。

如果找到服務,就會對服務執行關閉。一旦完成,就進入下一個狀態,即 removeService()。

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 客戶端將服務從群集中移除。

調用 HasStorage() 方法確定是否有存儲,如果有,就會進入 removeBackups() 狀態,否則就會返回 args, nil, nil,這將導致狀態機在無錯誤的情況下退出。

這個示例說明如何根據 Args 中的信息或代碼中的遠程調用在狀態機中創建分支。

其他狀態調用由你自行決定。我們看看這種設計如何更適合測試此類操作。

測試優勢

這種模式首先鼓勵的是小塊的可測試代碼,模塊變得很容易分割,這樣當代碼塊變得太大時,只需創建新的狀態來隔離代碼塊。

但更大的優勢在于無需進行大規模端到端測試。由于操作流程中的每個階段都需要調用下一階段,因此會出現以下情況:

  • 頂層調用者按一定順序調用所有子函數
  • 每個調用者都會調用下一個函數
  • 兩者的某種混合

兩者都會導致某種類型的端到端測試,而這種測試本不需要。

如果我們對頂層調用者方法進行編碼,可能看起來像這樣:

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(),現在必須對調用的所有客戶端或方法打樁。這看起來就像是端到端測試,而對于這類代碼來說,通常不是好主意。

如果轉到功能調用鏈,情況也不會好到哪里去:

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() 以及下面所有調用。

有幾種方法可以做到,但都不太好。

如果使用狀態機,只需測試每個階段是否按要求運行,并返回想要的下一階段。

頂層調用者甚至不需要測試,它只是調用 validate() 方法,并調用應該能夠被測試的 Run() 函數。

我們為 drainService() 編寫一個表驅動測試,這里會拷貝一份 drainService() 代碼,這樣就不用返回到前面看代碼了。

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]玩一下。

如你所見,這避免了測試整個調用鏈,同時還能確保測試調用鏈中的下一個函數。

這些測試很容易劃分,維護人員也很容易遵循。

其他可能性

這種模式也有變種,即根據 Args 中設置的字段確定狀態,并跟蹤狀態的執行以防止循環。

在第一種情況下,狀態機軟件包可能是這樣的:

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
    }
  }
}

可以很容易的將分布式跟蹤或日志記錄集成到這種設計中。

如果希望推送大量數據并利用并發優勢,不妨試試 stagedpipe 軟件包[4],其內置了大量高級功能,可以看視頻和 README 學習如何使用。

希望這篇文章能讓你充分了解 Go 狀態機設計模式,現在你的工具箱里多了一個強大的新工具。

責任編輯:趙寧寧 來源: DeepNoMind
相關推薦

2021-05-17 12:10:05

C語言狀態機代碼

2025-04-02 03:15:00

狀態機設計工具

2024-05-06 00:30:00

MVCC數據庫

2022-08-11 08:46:23

索引數據結構

2023-03-10 13:30:00

MyBatis源碼ORM

2020-11-18 08:15:39

TypeScript設計模式

2020-11-04 08:54:54

狀態模式

2024-10-06 12:56:36

Golang策略設計模式

2022-03-25 11:01:28

Golang裝飾模式Go 語言

2022-03-23 15:36:13

數字化轉型數據治理企業

2023-08-02 08:14:33

監控MTS性能

2024-11-26 14:29:48

2020-09-07 19:38:12

安卓手機Android

2024-11-28 08:54:19

GolangGo變量

2025-10-10 01:55:00

GolangnoCopy函數

2022-06-07 08:55:04

Golang單例模式語言

2019-02-12 11:15:15

Spring設計模式Java

2024-07-03 08:33:08

2019-12-02 10:16:46

架構設計模式

2024-09-26 14:48:35

SpringAOP范式
點贊
收藏

51CTO技術棧公眾號

久久国产精品72免费观看| 精品福利视频导航| 欧美成在线观看| av在线天天| 亚洲一区日韩| 欧美国产日本高清在线 | 久久av电影| 欧美美女一区二区在线观看| 激情图片qvod| 国产精品免费99久久久| 欧美日韩免费观看一区二区三区 | 国产精品亚洲d| 亚洲四区在线观看| 亚洲精品无人区| 亚洲盗摄视频| 亚洲精品白浆高清久久久久久| 成人www视频网站免费观看| 亚洲深夜av| 欧美激情中文网| 黄页网站在线观看免费| 亚洲激情图片小说视频| 欧美h视频在线观看| 91精品久久久久久久蜜月| 久久精品中文字幕一区| 国产网站在线播放| 国产精品美女久久久久aⅴ国产馆| 日韩欧美一区二区视频在线播放 | 久久久久久久成人| 羞羞的网站在线观看| 亚洲人午夜精品天堂一二香蕉| 亚洲国产欧美一区二区三区不卡| 欧美日本成人| 欧美超级免费视 在线| 国产在线高清视频| 欧美日韩国产区| 蜜臀av色欲a片无码精品一区| 精品欧美久久| 欧美日产国产成人免费图片| 国产白浆在线免费观看| 色综合久久88色综合天天6| 在线黄色免费观看| av成人老司机| 麻豆md0077饥渴少妇| 美女久久网站| 91人人爽人人爽人人精88v| 波多野结衣亚洲| 欧美一区二区三区免费| 四虎成人免费在线| 亚洲永久精品大片| 免费一级电影| 欧美韩国日本不卡| 91最新在线观看| 91偷拍与自偷拍精品| 国产一二三四五| 日本伊人精品一区二区三区观看方式| 国产欧美韩国高清| 影视先锋久久| 欧美又大又粗又长| 99这里只有精品视频| 精品国内自产拍在线观看| 中文字幕在线高清| 欧美人狂配大交3d怪物一区 | 国产视频久久| 国产精品区一区二区三含羞草| 天天色天天射综合网| 性欧美视频videos6一9| 日本在线成人| 97在线视频免费看| 亚洲国产最新| 国产精品电影久久久久电影网| 你懂的视频欧美| 国产精品啪视频| 日韩av二区| 国产精品久久久久久久9999| 亚洲+小说+欧美+激情+另类| 欧美激情乱人伦| 欧美a大片欧美片| 欧美资源在线观看| 欧美激情欧美| 九9re精品视频在线观看re6 | 好男人免费精品视频| 欧美午夜理伦三级在线观看| 毛片在线播放a| 日韩成人免费视频| 超碰在线cao| 在线电影av不卡网址| 亚洲一区二区三区四区电影| 91精品国产成人www| 国产一区二区三区四区五区| 国产精品对白刺激| 国模 一区 二区 三区| 91在线短视频| 日韩一区精品视频| 国产午夜大地久久| 一区二区三区不卡在线观看| a视频网址在线观看| 日韩av综合网| 日韩三级网址| 91在线免费视频| 久久精品动漫| 欧美二区在线视频| 一区二区三区美女| 日本视频不卡| 中文字幕精品在线视频| 任我爽精品视频在线播放| 91精品国产91久久久久青草| 美女一区二区三区在线观看| 无码精品a∨在线观看中文| 亚洲综合成人网| 91精选在线| 欧美巨猛xxxx猛交黑人97人| 四虎成人精品永久免费av九九| 欧美精品二区三区四区免费看视频 | 亚洲欧美日韩爽爽影院| 粉嫩久久久久久久极品| 国产91精品网站| 国自产拍偷拍福利精品免费一 | 日韩香蕉视频| 欧美 日韩 国产在线观看| 亚洲国产精品一区二区www在线 | 亚洲国产成人精品久久久国产成人一区| 成人看片毛片免费播放器| 日韩免费在线视频| 国产精品一区不卡| 男人天堂亚洲二区| 欧美激情啊啊啊| 日韩精品国产欧美| 午夜亚洲成人| 97欧美精品一区二区三区| 国产一区二区三区av电影 | 五月综合激情网| 91精品一久久香蕉国产线看观看| 国产在线播放不卡| 97精品久久久午夜一区二区三区| 色吊丝在线永久观看最新版本| 亚洲片av在线| 中文无码久久精品| 中国丰满人妻videoshd| 欧美优质美女网站| 色天天色综合| 91国在线高清视频| 欧美二区乱c少妇| 国产精品美女久久久久久不卡| 欧洲金发美女大战黑人| 色猫猫国产区一区二在线视频| 午夜视频在线观看精品中文 | 好看的av在线不卡观看| 国产美女主播在线| 欧美三级欧美一级| 国内精品免费| 精品人妻人人做人人爽| 在线视频一区二区三| 97视频一区| 亚洲精品电影网站| 久久成人亚洲| 国产乱码在线| 亚洲电影免费| 日韩av在线网址| 国产精品资源站在线| 亚洲精品动漫| 91黄色在线看| 久久艹在线视频| 国产偷国产偷亚洲高清人白洁| 91精品在线免费视频| 免费在线观看日韩视频| 另类图片亚洲另类| 中文字幕av一区 二区| 久久久久97| 美女一级全黄| 亚洲自拍另类欧美丝袜| 一本久道中文字幕精品亚洲嫩| 无需播放器亚洲| 国产h在线观看| 欧美性色黄大片人与善| 日韩欧美色电影| 韩国成人精品a∨在线观看| 亚洲精品一区三区三区在线观看| 又粗又黑又大的吊av| 69av视频在线播放| 欧美午夜精品久久久久久浪潮| 欧美精品一卡| 俺来俺也去www色在线观看| av久久久久久| 欧美高跟鞋交xxxxhd| 一区二区三区四区激情| 午夜精品久久| 午夜欧美激情| 91淫黄看大片| 亚洲在线观看视频| 亚洲成人黄色在线| 欧美国产精品v| 天天天综合网| 538视频在线| 国产免费又粗又猛又爽| 国产日本欧美视频| 日韩一区二区高清| 久久久精品国产免费观看同学| 欧美女优在线视频| av美女在线观看| 无限资源日本好片|