Goプログラミング言語:Golang入門チュートリアル

公開: 2022-03-11

Goプログラミング言語とは何ですか?

比較的新しいGoプログラミング言語は、風景の真ん中にきちんと配置されており、多くの優れた機能を提供し、多くの悪い機能を意図的に省略しています。 コンパイルが速く、実行速度が速く、ランタイムとガベージコレクションが含まれ、単純な静的型システムと動的インターフェイス、および優れた標準ライブラリがあります。 これが、非常に多くの開発者がGoプログラミングを学びたがっている理由です。

Golangチュートリアル:ロゴイラスト

GoとOOP

OOPは、Goが意図的に省略している機能の1つです。 サブクラス化されていないため、継承ダイアモンドやスーパーコール、またはつまずく仮想メソッドはありません。 それでも、OOPの便利な部分の多くは他の方法で利用できます。

* Mixins *は、構造体を匿名で埋め込むことで利用でき、それらのメソッドを含む構造体で直接呼び出すことができます(埋め込みを参照)。 この方法でメソッドをプロモートすることは*転送*と呼ばれ、サブクラス化と同じではありません。メソッドは引き続き内部の埋め込み構造体で呼び出されます。

埋め込みは、ポリモーフィズムを意味するものでもありません。 `A`は`B`を持っているかもしれませんが、それが`B`であるという意味でありません-`B`をとる関数は代わりに`A`をとらないでしょう。 そのためには、インターフェースが必要です。これについては後で簡単に説明します。

一方、Golangは、混乱やバグにつながる可能性のある機能について強力な立場を取っています。 継承やポリモーフィズムなどのOOPイディオムを省略し、構成と単純なインターフェイスを優先します。 例外処理を軽視し、戻り値の明示的なエラーを優先します。 gofmtツールによって適用されるGoコードをレイアウトする正しい方法は1つだけです。 等々。

なぜGolangを学ぶのですか?

Goは、並行プログラムを作成するための優れた言語でもあります。独立して実行される多くの部分を持つプログラムです。 明らかな例はウェブサーバーです。すべてのリクエストは個別に実行されますが、多くの場合、リクエストはセッション、キャッシュ、通知キューなどのリソースを共有する必要があります。 これは、熟練したGoプログラマーがこれらのリソースへの同時アクセスに対処する必要があることを意味します。

Golangには、並行性を処理するための優れた低レベル機能のセットがありますが、それらを直接使用することは複雑になる可能性があります。 多くの場合、これらの低レベルのメカニズムに対する再利用可能な抽象化により、作業がはるかに簡単になります。

今日のGoプログラミングチュートリアルでは、そのような抽象化の1つである、任意のデータ構造をトランザクションサービスに変換できるラッパーについて説明します。 例としてFundタイプを使用します。これは、スタートアップの残りの資金を保管するシンプルなストアで、残高を確認して引き出しを行うことができます。

これを実際に示すために、サービスを小さなステップで構築し、途中で混乱させてから、もう一度クリーンアップします。 Goチュートリアルを進めると、次のような多くのクールなGo言語機能に遭遇します。

  • 構造体のタイプとメソッド
  • ユニットテストとベンチマーク
  • Goroutinesとチャネル
  • インターフェースと動的型付け

シンプルなファンドの構築

スタートアップの資金を追跡するためのコードを書いてみましょう。 ファンドは所定の残高から始まり、お金を引き出すことしかできません(後で収益を計算します)。

この図は、Goプログラミング言語を使用した簡単なゴルーチンの例を示しています。

Goは意図にオブジェクト指向言語ではありません。クラス、オブジェクト、または継承はありません。 代わりに、新しいファンド構造体を作成するための単純な関数と2つのパブリックメソッドを使用して、 Fundという構造体タイプを宣言します。

Fund.go

 package funding type Fund struct { // balance is unexported (private), because it's lowercase balance int } // A regular function returning a pointer to a fund func NewFund(initialBalance int) *Fund { // We can return a pointer to a new struct without worrying about // whether it's on the stack or heap: Go figures that out for us. return &Fund{ balance: initialBalance, } } // Methods start with a *receiver*, in this case a Fund pointer func (f *Fund) Balance() int { return f.balance } func (f *Fund) Withdraw(amount int) { f.balance -= amount }

ベンチマークを使用したテスト

次に、 Fundをテストする方法が必要です。 別のプログラムを作成するのではなく、単体テストとベンチマークの両方のフレームワークを提供するGoのテストパッケージを使用します。 Fundの単純なロジックは、単体テストを作成する価値はありませんが、後でファンドへの同時アクセスについて多くのことを話し合うので、ベンチマークを作成することは理にかなっています。

ベンチマークは単体テストに似ていますが、同じコードを何度も実行するループが含まれています(この場合、 fund.Withdraw(1) )。 これにより、フレームワークは、各反復にかかる時間を計測し、ディスクシーク、キャッシュミス、プロセススケジューリング、およびその他の予測できない要因からの一時的な差異を平均化できます。

テストフレームワークでは、各ベンチマークを少なくとも1秒間実行する必要があります(デフォルト)。 これを確実にするために、ベンチマークを複数回呼び出し、実行に少なくとも1秒かかるまで、毎回増加する「反復回数」の値( bNフィールド)を渡します。

今のところ、私たちのベンチマークは、いくらかのお金を預けてから、一度に1ドルずつ引き出します。

Fund_test.go

 package funding import "testing" func BenchmarkFund(b *testing.B) { // Add as many dollars as we have iterations this run fund := NewFund(bN) // Burn through them one at a time until they are all gone for i := 0; i < bN; i++ { fund.Withdraw(1) } if fund.Balance() != 0 { b.Error("Balance wasn't zero:", fund.Balance()) } }

それを実行してみましょう:

 $ go test -bench . funding testing: warning: no tests to run PASS BenchmarkWithdrawals 2000000000 1.69 ns/op ok funding 3.576s

それはうまくいった。 20億(!)回の反復を実行し、バランスの最終チェックは正しかった。 「実行するテストがありません」という警告は無視できます。これは、作成しなかった単体テストを指します(このチュートリアルの後のGoプログラミングの例では、警告は省略されています)。

Goでの同時アクセス

次に、ベンチマークを同時に作成して、同時に引き出しを行うさまざまなユーザーをモデル化します。 そのために、10個のゴルーチンをスポーンし、それぞれに10分の1のお金を引き出します。

Go言語で複数の同時実行ゴルーチンをどのように構成しますか?

Goroutinesは、Go言語での並行性の基本的な構成要素です。 これらはグリーンスレッドです。オペレーティングシステムではなく、Goランタイムによって管理される軽量スレッドです。 これは、大きなオーバーヘッドなしで数千(または数百万)を実行できることを意味します。 Goroutinesはgoキーワードで生成され、常に関数(またはメソッド呼び出し)で始まります。

 // Returns immediately, without waiting for `DoSomething()` to complete go DoSomething()

多くの場合、ほんの数行のコードで短い1回限りの関数を生成したいと思います。 この場合、関数名の代わりにクロージャを使用できます。

 go func() { // ... do stuff ... }() // Must be a function *call*, so remember the ()

すべてのゴルーチンが生成されたら、それらが終了するのを待つ方法が必要です。 チャネルを使用して自分で構築することもできますが、まだそれらに遭遇していないため、先にスキップします。

今のところ、この目的のために存在するGoの標準ライブラリのWaitGroupタイプを使用できます。 1つ(「 wg 」と呼ばれる)を作成し、各ワーカーを生成する前にwg.Add(1)を呼び出して、ワーカーの数を追跡します。 次に、ワーカーはwg.Done()を使用して報告します。 一方、メインのゴルーチンでは、 wg.Wait()と言うだけで、すべてのワーカーが終了するまでブロックできます。

次の例のワーカーゴルーチン内では、 deferを使用してwg.Done()を呼び出します。

deferは関数(またはメソッド)呼び出しを受け取り、他のすべてが完了した後、現在の関数が戻る直前にそれを実行します。 これはクリーンアップに便利です。

 func() { resource.Lock() defer resource.Unlock() // Do stuff with resource }()

このようにして、読みやすくするために、 UnlockLockを簡単に一致させることができます。 さらに重要なことに、メイン関数にパニックが発生した場合でも、遅延関数が実行されます(他の言語では、try-finallyを介して処理できる可能性があります)。

最後に、遅延関数は呼び出されたのとはの順序で実行されます。つまり、ネストされたクリーンアップを適切に実行できます(ネストされたgotolabelのCイディオムに似ていますが、はるかに優れています)。

 func() { db.Connect() defer db.Disconnect() // If Begin panics, only db.Disconnect() will execute transaction.Begin() defer transaction.Close() // From here on, transaction.Close() will run first, // and then db.Disconnect() // ... }()

さて、そうは言っても、新しいバージョンは次のとおりです。

Fund_test.go

 package funding import ( "sync" "testing" ) const WORKERS = 10 func BenchmarkWithdrawals(b *testing.B) { // Skip N = 1 if bN < WORKERS { return } // Add as many dollars as we have iterations this run fund := NewFund(bN) // Casually assume bN divides cleanly dollarsPerFounder := bN / WORKERS // WaitGroup structs don't need to be initialized // (their "zero value" is ready to use). // So, we just declare one and then use it. var wg sync.WaitGroup for i := 0; i < WORKERS; i++ { // Let the waitgroup know we're adding a goroutine wg.Add(1) // Spawn off a founder worker, as a closure go func() { // Mark this worker done when the function finishes defer wg.Done() for i := 0; i < dollarsPerFounder; i++ { fund.Withdraw(1) } }() // Remember to call the closure! } // Wait for all the workers to finish wg.Wait() if fund.Balance() != 0 { b.Error("Balance wasn't zero:", fund.Balance()) } }

ここで何が起こるかを予測できます。 ワーカーは全員、お互いの上にWithdrawを実行します。 その中で、 f.balance -= amountは残高を読み取り、1を引いてから、書き戻します。 しかし、2人以上のワーカーが両方とも同じバランスを読み取り、同じ減算を行う場合があり、合計が間違ってしまうことがあります。 右?

 $ go test -bench . funding BenchmarkWithdrawals 2000000000 2.01 ns/op ok funding 4.220s

いいえ、まだ合格です。 ここで何が起こったのですか?

ゴルーチンはグリーンスレッドであることに注意してください。これらはOSではなくGoランタイムによって管理されます。 ランタイムは、使用可能なOSスレッドの数に関係なくゴルーチンをスケジュールします。 このGo言語チュートリアルを書いている時点では、Goは使用するOSスレッドの数を推測しようとはしていません。複数のスレッドが必要な場合は、そう言わなければなりません。 最後に、現在のランタイムはゴルーチンをプリエンプトしません。ゴルーチンは、休憩の準備ができていることを示唆する何かを実行するまで実行を続けます(チャネルとの対話など)。

これはすべて、ベンチマークは現在並行しているものの、並行していないことを意味します。 一度に実行するのは1人のワーカーのみで、完了するまで実行されます。 これを変更するには、 GOMAXPROCS環境変数を使用して、Goにさらにスレッドを使用するように指示します。

 $ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 --- FAIL: BenchmarkWithdrawals-4 account_test.go:39: Balance wasn't zero: 4238 ok funding 0.007s

それがいいです。 今、私たちは予想通り、明らかに撤退の一部を失っています。

このGoプログラミングの例では、複数の並列ゴルーチンの結果は好ましくありません。

サーバーにする

この時点で、さまざまなオプションがあります。 ファンドの周りに明示的なミューテックスまたは読み取り/書き込みロックを追加することができます。 バージョン番号とのコンペアアンドスワップを使用できます。 すべてを実行してCRDTスキームを使用することができます(おそらく、 balanceフィールドを各クライアントのトランザクションのリストに置き換え、それらから残高を計算します)。

しかし、それらは厄介または恐ろしい、あるいはその両方であるため、今はそれらのことは何もしません。 代わりに、ファンドはサーバーである必要があると判断します。 サーバーとは何ですか? それはあなたが話しているものです。 Goでは、物事はチャネルを介して話します。

チャネルは、ゴルーチン間の基本的な通信メカニズムです。 値はチャネルに送信され( channel <- value )、反対側で受信できます( value = <- channel )。 チャネルは「ゴルーチンセーフ」です。つまり、任意の数のゴルーチンが同時に送受信できます。

バッファリング

通信チャネルのバッファリングは、特定の状況ではパフォーマンスを最適化することができますが、細心の注意を払って使用する必要があります(そしてベンチマーク!)。

ただし、通信に直接関係しないバッファリングされたチャネルの用途があります。

たとえば、一般的なスロットルイディオムは、(たとえば)バッファサイズが「10」のチャネルを作成し、すぐに10個のトークンをそのチャネルに送信します。 次に、任意の数のワーカーゴルーチンが生成され、それぞれが作業を開始する前にチャネルからトークンを受け取り、後でそれを送り返します。 そうすれば、どんなに多くの労働者がいても、同時に働くのは10人だけです。

デフォルトでは、Goチャネルはバッファリングされていません。 これは、チャネルに値を送信すると、別のゴルーチンがすぐに値を受信する準備ができるまでブロックされることを意味します。 Goは、チャネルの固定バッファーサイズもサポートします( make(chan someType, bufferSize)を使用)。 ただし、通常の使用では、これは通常悪い考えです。

私たちのファンドのウェブサーバーを想像してみてください。リクエストごとに引き出しが行われます。 物事が非常に忙しい場合、 FundServerは追いつくことができず、コマンドチャネルに送信しようとする要求はブロックを開始して待機します。 その時点で、サーバーで最大リクエスト数を強制し、その制限を超えて適切なエラーコード( 503 Service Unavailableなど)をクライアントに返すことができます。 これは、サーバーが過負荷の場合に可能な最良の動作です。

チャネルにバッファリングを追加すると、この動作の決定性が低下します。 クライアントがはるかに早く見た情報に基づいて(そしておそらくそれ以降アップストリームでタイムアウトになった要求の場合)、未処理のコマンドの長いキューに簡単になってしまう可能性があります。 受信者が送信者に追いつけないときにTCPを介してバックプレッシャを適用するなど、他の多くの状況でも同じことが当てはまります。

いずれにせよ、Goの例では、デフォルトのバッファなしの動作を維持します。

チャネルを使用して、 FundServerにコマンドを送信します。 すべてのベンチマークワーカーはコマンドをチャネルに送信しますが、サーバーのみがコマンドを受信します。

Fundタイプをサーバー実装に直接変換することもできますが、それは面倒です。並行処理とビジネスロジックを混在させることになります。 代わりに、Fundタイプをそのままにして、 FundServerをその周りの別個のラッパーにします。

他のサーバーと同様に、ラッパーには、コマンドを待機し、それぞれに順番に応答するメインループがあります。 ここで対処する必要のあるもう1つの詳細があります。それは、コマンドのタイプです。

このGoプログラミングチュートリアルでサーバーとして使用されているファンドの図。

ポインタ

コマンドチャネルにコマンドへの*ポインタ*を取得させることもできます( `chan * TransactionCommand`)。 なぜ私たちはしなかったのですか?

どちらのゴルーチンもそれを変更する可能性があるため、ゴルーチン間でポインタを渡すことは危険です。 また、他のゴルーチンが別のCPUコアで実行されている可能性があるため(キャッシュの無効化が増えることを意味します)、効率が低下することもよくあります。

可能な限り、プレーンな値を渡すことをお勧めします。

以下の次のセクションでは、それぞれが独自の構造体タイプを持ついくつかの異なるコマンドを送信します。 サーバーのコマンドチャネルがそれらのいずれかを受け入れるようにします。 OOP言語では、ポリモーフィズムを介してこれを行うことができます。チャネルにスーパークラスを取得させます。スーパークラスの個々のコマンドタイプはサブクラスでした。 Goでは、代わりにインターフェースを使用します。

インターフェイスは、メソッドシグネチャのセットです。 これらのメソッドをすべて実装するタイプは、(そうするように宣言されていなくても)そのインターフェースとして扱うことができます。 最初の実行では、コマンド構造体は実際にはメソッドを公開しないため、空のインターフェイスinterface{}を使用します。 要件がないため、任意の値(整数などのプリミティブ値を含む)は空のインターフェイスを満たします。 これは理想的ではありません-コマンド構造体のみを受け入れたいのですが-後で戻ってきます。

とりあえず、Goサーバーのスキャフォールディングを始めましょう。

server.go

 package funding type FundServer struct { Commands chan interface{} fund Fund } func NewFundServer(initialBalance int) *FundServer { server := &FundServer{ // make() creates builtins like channels, maps, and slices Commands: make(chan interface{}), fund: NewFund(initialBalance), } // Spawn off the server's main loop immediately go server.loop() return server } func (s *FundServer) loop() { // The built-in "range" clause can iterate over channels, // amongst other things for command := range s.Commands { // Handle the command } }

次に、コマンドにいくつかのGolang構造体タイプを追加しましょう。

 type WithdrawCommand struct { Amount int } type BalanceCommand struct { Response chan int }

WithdrawCommandには、引き出す金額だけが含まれています。 応答はありません。 BalanceCommandには応答があるため、それを送信するためのチャネルが含まれています。 これにより、後でファンドが順不同で応答することを決定した場合でも、応答は常に適切な場所に送られます。

これで、サーバーのメインループを記述できます。

 func (s *FundServer) loop() { for command := range s.Commands { // command is just an interface{}, but we can check its real type switch command.(type) { case WithdrawCommand: // And then use a "type assertion" to convert it withdrawal := command.(WithdrawCommand) s.fund.Withdraw(withdrawal.Amount) case BalanceCommand: getBalance := command.(BalanceCommand) balance := s.fund.Balance() getBalance.Response <- balance default: panic(fmt.Sprintf("Unrecognized command: %v", command)) } } }

うーん。 それはちょっと醜いです。 タイプアサーションを使用してコマンドタイプをオンに切り替えているため、クラッシュする可能性があります。 とにかく前進して、サーバーを使用するようにベンチマークを更新しましょう。

 func BenchmarkWithdrawals(b *testing.B) { // ... server := NewFundServer(bN) // ... // Spawn off the workers for i := 0; i < WORKERS; i++ { wg.Add(1) go func() { defer wg.Done() for i := 0; i < dollarsPerFounder; i++ { server.Commands <- WithdrawCommand{ Amount: 1 } } }() } // ... balanceResponseChan := make(chan int) server.Commands <- BalanceCommand{ Response: balanceResponseChan } balance := <- balanceResponseChan if balance != 0 { b.Error("Balance wasn't zero:", balance) } }

特にバランスをチェックしたときは、それもちょっと醜いものでした。 どうでも。 試してみよう:

 $ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 5000000 465 ns/op ok funding 2.822s

はるかに良いことに、私たちはもはや引き出しを失うことはありません。 しかし、コードは読みにくくなり、より深刻な問題があります。 BalanceCommandを発行した後、応答を読むのを忘れた場合、ファンドサーバーはそれを送信しようとして永久にブロックします。 少し片付けましょう。

それをサービスにする

サーバーはあなたが話しかけるものです。 サービスとは何ですか? サービスは、APIを使用して通信するものです。 クライアントコードをコマンドチャネルで直接機能させる代わりに、チャネルを非エクスポート(プライベート)にして、使用可能なコマンドを関数にラップします。

 type FundServer struct { commands chan interface{} // Lowercase name, unexported // ... } func (s *FundServer) Balance() int { responseChan := make(chan int) s.commands <- BalanceCommand{ Response: responseChan } return <- responseChan } func (s *FundServer) Withdraw(amount int) { s.commands <- WithdrawCommand{ Amount: amount } }

これで、ベンチマークはserver.Withdraw(1)balance := server.Balance()とだけ言うことができ、誤って無効なコマンドを送信したり、応答を読み忘れたりする可能性が低くなります。

このサンプルのGo言語プログラムでは、ファンドをサービスとして使用すると次のようになります。

コマンドにはまだ多くの追加の定型文がありますが、後で戻ってきます。

トランザクション

最終的に、お金は常になくなります。 資金が最後の10ドルに下がったら、引き出しをやめ、そのお金を共同ピザに使って祝ったり、祝ったりすることに同意しましょう。 私たちのベンチマークはこれを反映します:

 // Spawn off the workers for i := 0; i < WORKERS; i++ { wg.Add(1) go func() { defer wg.Done() for i := 0; i < dollarsPerFounder; i++ { // Stop when we're down to pizza money if server.Balance() <= 10 { break } server.Withdraw(1) } }() } // ... balance := server.Balance() if balance != 10 { b.Error("Balance wasn't ten dollars:", balance) }

今回は本当に結果を予測することができます。

 $ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 --- FAIL: BenchmarkWithdrawals-4 fund_test.go:43: Balance wasn't ten dollars: 6 ok funding 0.009s

開始したところに戻ります。複数のワーカーが一度に残高を読み取ってから、全員がそれを更新できます。 これに対処するために、 minimumBalanceプロパティなど、ファンド自体にロジックを追加したり、 WithdrawIfOverXDollarsという別のコマンドを追加したりできます。 これらは両方ともひどい考えです。 私たちの合意は私たち自身の間のものであり、基金の所有物ではありません。 アプリケーションロジックに保持する必要があります。

私たちが本当に必要としているのは、データベーストランザクションと同じ意味でのトランザクションです。 私たちのサービスは一度に1つのコマンドしか実行しないため、これは非常に簡単です。 コールバック(クロージャ)を含むTransactコマンドを追加します。 サーバーは、独自のゴルーチン内でそのコールバックを実行し、生のFundを渡します。 その後、コールバックはFundで好きなことを安全に行うことができます。

セマフォとエラー

この次の例では、2つの小さなことを間違って行っています。

まず、トランザクションが終了したときに呼び出し元のコードに通知するためのセマフォとして「Done」チャネルを使用しています。 それは問題ありませんが、チャネルタイプが「bool」なのはなぜですか? 「完了」を意味するために「true」を送信するだけです(「false」を送信するとはどういう意味ですか?)。 私たちが本当に必要としているのは、単一状態の値(値のない値?)です。 Goでは、空の構造体タイプ`struct{}`を使用してこれを行うことができます。 これには、使用するメモリが少ないという利点もあります。 この例では、あまり怖く見えないように「bool」を使用します。

次に、トランザクションコールバックは何も返しません。 すぐにわかるように、スコープのトリックを使用して、コールバックから呼び出し元のコードに値を取得できます。 ただし、実際のシステムでのトランザクションはおそらく失敗することがあるため、Goの規則では、トランザクションに「エラー」が返されます(その後、呼び出しコードで「nil」かどうかを確認します)。

生成するエラーがないため、現時点ではこれも行っていません。
 // Typedef the callback for readability type Transactor func(fund *Fund) // Add a new command type with a callback and a semaphore channel type TransactionCommand struct { Transactor Transactor Done chan bool } // ... // Wrap it up neatly in an API method, like the other commands func (s *FundServer) Transact(transactor Transactor) { command := TransactionCommand{ Transactor: transactor, Done: make(chan bool), } s.commands <- command <- command.Done } // ... func (s *FundServer) loop() { for command := range s.commands { switch command.(type) { // ... case TransactionCommand: transaction := command.(TransactionCommand) transaction.Transactor(s.fund) transaction.Done <- true // ... } } }

トランザクションコールバックは直接何も返しませんが、Go言語を使用すると、クロージャから直接値を簡単に取得できるため、ベンチマークでこれを実行して、お金が少なくなったときにpizzaTimeフラグを設定します。

 pizzaTime := false for i := 0; i < dollarsPerFounder; i++ { server.Transact(func(fund *Fund) { if fund.Balance() <= 10 { // Set it in the outside scope pizzaTime = true return } fund.Withdraw(1) }) if pizzaTime { break } }

そして、それが機能することを確認します。

 $ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 5000000 775 ns/op ok funding 4.637s

トランザクション以外は何もありません

あなたは今、物事をもう少しきれいにする機会を見つけたかもしれません。 一般的なTransactコマンドがあるので、 WithdrawCommandBalanceCommandもう必要ありません。 トランザクションの観点からそれらを書き直します。

 func (s *FundServer) Balance() int { var balance int s.Transact(func(f *Fund) { balance = f.Balance() }) return balance } func (s *FundServer) Withdraw(amount int) { s.Transact(func (f *Fund) { f.Withdraw(amount) }) }

これで、サーバーが取るコマンドはTransactionCommandだけなので、その実装でinterface{}の混乱全体を削除し、トランザクションコマンドのみを受け入れるようにすることができます。

 type FundServer struct { commands chan TransactionCommand fund *Fund } func (s *FundServer) loop() { for transaction := range s.commands { // Now we don't need any type-switch mess transaction.Transactor(s.fund) transaction.Done <- true } }

ずっといい。

ここで実行できる最後のステップがあります。 BalanceWithdrawのための便利な機能は別として、サービスの実装はもはやFundに結び付けられていません。 Fundを管理する代わりに、 interface{}を管理し、あらゆるものをラップするために使用できます。 ただし、各トランザクションコールバックは、 interface{}を実際の値に変換し直す必要があります。

 type Transactor func(interface{}) server.Transact(func(managedValue interface{}) { fund := managedValue.(*Fund) // Do stuff with fund ... })

これは醜く、エラーが発生しやすいです。 本当に必要なのはコンパイル時のジェネリックスなので、特定のタイプ( *Fundなど)のサーバーを「テンプレート化」できます。

残念ながら、Goはジェネリックスをまだサポートしていません。 誰かがそれのためのいくつかの賢明な構文とセマンティクスを理解すると、それは最終的に到着すると予想されます。 それまでの間、慎重なインターフェース設計によりジェネリックスの必要性がなくなることがよくあります。ジェネリックスがない場合は、型アサーション(実行時にチェックされます)で対処できます。

完了しましたか?

はい。

まあ、大丈夫、いや。

例えば:

  • トランザクションがパニックになると、サービス全体が停止します。

  • タイムアウトはありません。 決して返されないトランザクションは、サービスを永久にブロックします。

  • ファンドがいくつかの新しいフィールドを成長させ、それらを更新する途中でトランザクションがクラッシュした場合、一貫性のない状態になります。

  • トランザクションはマネージドFundオブジェクトをリークする可能性がありますが、これは良くありません。

  • 複数のファンド間で取引を行うための合理的な方法はありません(あるファンドからの引き出しや別のファンドへの入金など)。 デッドロックが発生する可能性があるため、トランザクションをネストすることはできません。

  • トランザクションを非同期で実行するには、新しいゴルーチンと多くの混乱が必要になります。 関連して、長期的なトランザクションの進行中に、他の場所から最新のFundの状態を読み取れるようにしたいと考えています。

次のGoプログラミング言語チュートリアルでは、これらの問題に対処するためのいくつかの方法を見ていきます。

関連:適切に構造化されたロジック:GolangOOPチュートリアル