比較的新しい プログラミング言語に行く 風景の真ん中にきちんと座って、たくさんの良い機能を提供し、意図的に多くの悪い機能を省略しています。コンパイルが速く、実行速度が速く、ランタイムとガベージコレクションが含まれ、単純な静的型システムと動的インターフェイス、および優れた標準ライブラリがあります。これが、非常に多くの開発者がGoプログラミングを学びたがっている理由です。
一方、Golangは、混乱やバグにつながる可能性のある機能について強力な立場を取っています。継承やポリモーフィズムなどのOOPイディオムを省略し、構成と単純なインターフェイスを優先します。戻り値の明示的なエラーを優先して、例外処理を軽視します。 gofmt
によって強制される、Goコードをレイアウトする正しい方法は1つだけです。ツール。等々。
Goは書くのにも最適な言語です 並行プログラム :多くの独立して実行される部分を持つプログラム。明らかな例はウェブサーバーです。すべてのリクエストは個別に実行されますが、多くの場合、リクエストはセッション、キャッシュ、通知キューなどのリソースを共有する必要があります。これの意味は 熟練したGoプログラマー それらのリソースへの同時アクセスに対処する必要があります。
Golangには、並行性を処理するための優れた低レベル機能のセットがありますが、それらを直接使用すると複雑になる可能性があります。多くの場合、これらの低レベルのメカニズムに対する再利用可能な抽象化により、作業がはるかに簡単になります。
今日のGoプログラミングチュートリアルでは、そのような抽象化の1つを見ていきます。それは、任意のデータ構造をに変換できるラッパーです。 トランザクションサービス 。 Fund
を使用します例として入力します–スタートアップの残りの資金を調達するためのシンプルなストアで、残高を確認して引き出しを行うことができます。
これを実際に示すために、サービスを小さなステップで構築し、途中で混乱させてから、もう一度クリーンアップします。 Goチュートリアルを進めると、次のような多くの優れたGo言語機能に遭遇します。
スタートアップの資金を追跡するためのコードを書いてみましょう。基金は所定の残高から始まり、お金を引き出すことしかできません(収益は後で計算します)。
行くは意図的に ない オブジェクト指向言語:クラス、オブジェクト、または継承はありません。代わりに、 構造体タイプ Fund
と呼ばれ、新しいファンド構造体を作成するための単純な関数と2つのパブリックメソッドを備えています。
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秒かかるまで、毎回増加する「反復回数」値(b.N
フィールド)を渡します。
今のところ、私たちのベンチマークは、いくらかのお金を預けてから、一度に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(b.N) // Burn through them one at a time until they are all gone for i := 0; i それを実行してみましょう:
$ 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のお金を引き出します。

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 ()
すべてのゴルーチンが生成されたら、それらが終了するのを待つ方法が必要です。を使用して自分で構築することができます チャネル 、しかし、まだそれらに遭遇していないので、それは先にスキップするでしょう。
今のところ、WaitGroup
を使用できますこの目的のために存在するGoの標準ライブラリを入力します。 1つ(「wg
」と呼ばれます)を作成し、wg.Add(1)
を呼び出します。各ワーカーをスポーンする前に、その数を追跡します。その後、ワーカーはwg.Done()
を使用して報告します。一方、メインのゴルーチンでは、wg.Wait()
とだけ言うことができます。すべてのワーカーが終了するまでブロックします。
次の例のワーカーゴルーチン内では、defer
を使用します。 wg.Done()
を呼び出します。
defer
関数(またはメソッド)呼び出しを受け取り、現在の関数が戻る直前、他のすべてが完了した後に実行します。これはクリーンアップに便利です。
func() { resource.Lock() defer resource.Unlock() // Do stuff with resource }()
このようにして、Unlock
を簡単に一致させることができます。読みやすくするために、Lock
を使用します。さらに重要なことに、据え置き関数が実行されます パニックがあっても main関数(他の言語でtry-finallyを介して処理する可能性のあるもの)。
最後に、遅延関数はで実行されます 逆行する それらが呼び出された順序。つまり、ネストされたクリーンアップを適切に実行できます(ネストされたgoto
sおよびlabel
sの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 b.N ここで何が起こるかを予測できます。ワーカーはすべて実行されます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
それが良いです。現在、予想どおり、撤退の一部が明らかに失われています。

サーバーにする
この時点で、さまざまなオプションがあります。ファンドの周りに明示的なミューテックスまたは読み取り/書き込みロックを追加できます。バージョン番号とのコンペアアンドスワップを使用できます。私たちはすべて出かけて、 CRDT スキーム(おそらく、balance
フィールドを各クライアントのトランザクションのリストに置き換え、それらから残高を計算します)。
しかし、それらは厄介であるか怖い、またはその両方であるため、今はこれらのことは何もしません。代わりに、ファンドは サーバ 。サーバーとは何ですか?それはあなたが話しているものです。 Goでは、物事はチャネルを介して話します。
チャネルは、ゴルーチン間の基本的な通信メカニズムです。値はチャネルに送信され(channel <- value
を使用)、反対側で受信できます(value = <- channel
を使用)。チャネルは「ゴルーチンセーフ」です。つまり、任意の数のゴルーチンが同時に送受信できます。
バッファリング
通信チャネルのバッファリングは、特定の状況ではパフォーマンスを最適化することができますが、細心の注意を払って使用する必要があります(そしてベンチマーク!)。
ただし、通信に直接関係しないバッファチャネルの用途があります。
たとえば、一般的なスロットルイディオムは、(たとえば)バッファサイズが「10」のチャネルを作成し、すぐに10個のトークンをそのチャネルに送信します。次に、任意の数のワーカーゴルーチンが生成され、それぞれが作業を開始する前にチャネルからトークンを受け取り、後でそれを送り返します。そうすれば、どんなに多くの労働者がいても、同時に働くのは10人だけです。デフォルトでは、Goチャネルは バッファなし 。これは、チャネルに値を送信すると、別のゴルーチンがすぐに値を受信する準備ができるまでブロックされることを意味します。 Goは、チャネルの固定バッファーサイズもサポートします(make(chan someType, bufferSize)
を使用)。ただし、通常の使用では、これは 通常は悪い考え 。
私たちのファンドのウェブサーバーを想像してみてください。リクエストごとに引き出しが行われます。物事が非常に忙しいとき、FundServer
は追いつくことができず、コマンドチャネルに送信しようとするリクエストはブロックを開始し、待機します。その時点で、サーバーで最大リクエスト数を強制し、その制限を超えたクライアントに適切なエラーコード(503 Service Unavailable
など)を返すことができます。これは、サーバーが過負荷の場合に可能な最善の動作です。
チャネルにバッファリングを追加すると、この動作の決定性が低下します。クライアントがはるかに早く見た情報に基づいて(そしておそらくそれ以降アップストリームでタイムアウトした要求の場合)、未処理のコマンドの長いキューに簡単になってしまう可能性があります。受信者が送信者に追いつけないときにTCPを介してバックプレッシャを適用するなど、他の多くの状況でも同じことが当てはまります。
いずれの場合も、Goの例では、デフォルトのバッファなしの動作を使用します。
チャネルを使用して、FundServer
にコマンドを送信します。すべてのベンチマークワーカーはコマンドをチャネルに送信しますが、サーバーのみがコマンドを受信します。
ファンドタイプをサーバー実装に直接変換することもできますが、それは面倒です。並行処理とビジネスロジックを混在させることになります。代わりに、ファンドタイプをそのままにして、FundServer
を作成します。その周りの別のラッパー。
他のサーバーと同様に、ラッパーには、コマンドを待機し、それぞれに順番に応答するメインループがあります。ここで対処する必要があるもう1つの詳細があります。それは、コマンドのタイプです。

ポインタ
コマンドチャネルにコマンドへの*ポインタ*を取得させることもできます( `chan * TransactionCommand`)。なぜ私たちはしなかったのですか?
どちらのゴルーチンもそれを変更する可能性があるため、ゴルーチン間でポインタを渡すことは危険です。また、他のゴルーチンが別のCPUコアで実行されている可能性があるため(キャッシュの無効化が増えることを意味します)、効率が低下することもよくあります。
可能な限り、プレーンな値を渡すことをお勧めします。以下の次のセクションでは、それぞれが独自の構造体タイプを持ついくつかの異なるコマンドを送信します。サーバーのコマンドチャネルがそれらのいずれかを受け入れるようにします。 OOP言語では、ポリモーフィズムを介してこれを行うことができます。チャネルにスーパークラスを取得させます。スーパークラスの個々のコマンドタイプはサブクラスでした。 Goでは、 インターフェイス 代わりに。
インターフェイスは、メソッドシグネチャのセットです。これらのメソッドをすべて実装する型は、(宣言されていなくても)そのインターフェイスとして扱うことができます。最初の実行では、コマンド構造体は実際にはメソッドを公開しないため、空のインターフェースinterface{}
を使用します。要件がないので、 どれか 値(整数などのプリミティブ値を含む)は、空のインターフェイスを満たします。これは理想的ではありません。コマンド構造体のみを受け入れたいのですが、後で戻ってきます。
とりあえず、囲碁サーバーのスキャフォールディングを始めましょう。
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(b.N) // ... // Spawn off the workers for i := 0; i 特にバランスをチェックしたとき、それもちょっと醜いものでした。気にしないで。試してみよう:
$ 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()
であり、誤って無効なコマンドを送信したり、応答の読み取りを忘れたりする可能性が低くなります。

コマンドにはまだ多くの追加の定型文がありますが、後で戻ってきます。
トランザクション
最終的に、お金は常に不足します。資金が最後の10ドルに下がったら引き出しを停止し、そのお金を共同ピザに使って祝ったり、祝ったりすることに同意しましょう。私たちのベンチマークはこれを反映します:
// Spawn off the workers for i := 0; i 今回は本当に結果を予測することができます。
$ 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 そして、それが機能することを確認します。
$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 5000000 775 ns/op ok funding 4.637s
トランザクション以外は何もありません
あなたは今、物事をもう少しきれいにする機会を見つけたかもしれません。ジェネリックTransact
があるのでコマンド、必要ありませんWithdrawCommand
またはBalanceCommand
もう。トランザクションの観点からそれらを書き直します。
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 } }
ずっといい。
ここで実行できる最後のステップがあります。 Balance
の便利な機能は別としておよびWithdraw
、サービスの実装はFund
に関連付けられなくなりました。 Fund
を管理する代わりに、interface{}
を管理できます。ラップに使用します 何でも 。ただし、各トランザクションコールバックは、interface{}
を変換する必要があります。実際の値に戻す:
type Transactor func(interface{}) server.Transact(func(managedValue interface{}) { fund := managedValue.(*Fund) // Do stuff with fund ... })
これは醜く、エラーが発生しやすいです。本当に必要なのはコンパイル時のジェネリックスなので、特定のタイプ(*Fund
など)のサーバーを「テンプレート化」できます。
残念ながら、Goはジェネリックスをまだサポートしていません。誰かがそれのためのいくつかの賢明な構文とセマンティクスを理解すると、最終的に到着することが期待されています。それまでの間、慎重なインターフェース設計によりジェネリックスが不要になることがよくあります。ジェネリックスがない場合は、型アサーション(実行時にチェックされます)を使用できます。
完了しましたか?
はい。
コルドバのsqliteデータベースを備えた完全なhtmlフォーム
まあ、大丈夫、いいえ。
例えば:
-
トランザクションのパニックはサービス全体を殺します。
-
タイムアウトはありません。戻らないトランザクションは、サービスを永久にブロックします。
-
ファンドがいくつかの新しいフィールドを拡大し、それらの更新の途中でトランザクションがクラッシュした場合、状態に一貫性がなくなります。
-
トランザクションは管理対象のFund
をリークする可能性がありますオブジェクト、それは良くありません。
-
複数のファンド間で取引を行うための合理的な方法はありません(あるファンドからの引き出しや別のファンドへの入金など)。デッドロックが発生する可能性があるため、トランザクションをネストすることはできません。
-
トランザクションを非同期で実行するには、新しいゴルーチンと多くの混乱が必要になります。関連して、おそらく最新のFund
を読みたいと思うでしょう。長時間実行されるトランザクションが進行している間、他の場所から状態を示します。
次のGoプログラミング言語チュートリアルでは、これらの問題に対処するいくつかの方法を見ていきます。
関連: 適切に構造化されたロジック:GolangOOPチュートリアル 基本を理解する
Go言語は何で書かれていますか?
Goプログラミング言語仕様は英語で書かれたドキュメントですが、Goの標準ライブラリとコンパイラはGo自体で書かれています。
Goは何に使用されますか?
Goは汎用プログラミング言語であり、さまざまな用途に使用できます。 Goは、バックエンドでWebサーバーとして使用でき、Docker、Kubernetes、およびHerokuCLIの構築に使用されています。
Goを使用している企業は何ですか?
Goは、多くの評判の良いテクノロジー企業、特にGoogle、Dropbox、Docker、Kubernetes、Herokuなどで使用されています。