現在、アプリケーション実行における最も一般的なボトルネックはネットワーク リクエストです。ネットワーク要求には数ミリ秒しかかかりませんが、応答の待機には 100 倍の時間がかかります。したがって、複数のネットワーク リクエストを実行する場合、それらをすべて並行して実行することが、待ち時間を短縮するための最良のオプションです。 Future/Promiseはその目的を達成するための手段の一つです。
将来とは、「将来」何か (通常はネットワーク リクエストの結果) が必要であるが、そのようなリクエストを今開始する必要があり、リクエストは非同期で実行されることを意味します。別の言い方をすると、バックグラウンドで非同期リクエストを実行する必要があります。
Future/Promise パターンには、複数の言語で対応する実装があります。たとえば、ES2015 には Promise と async-await があり、Scala には組み込みの Future があり、最後に、Golang には同様の機能を実現する goroutine と Channel があります。簡単な実装を以下に示します。
//RequestFuture, http request promise. func RequestFuture(url string) <-chan []byte { c := make(chan []byte, 1) go func() { var body []byte defer func() { c <- body }() res, err := http.Get(url) if err != nil { return } defer res.Body.Close() body, _ = ioutil.ReadAll(res.Body) }() return c } func main() { future := RequestFuture("https://api.github.com/users/octocat/orgs") body := <-future log.Printf("reponse length: %d", len(body)) }
RequestFuture
メソッドはチャネルを返します。この時点では、http リクエストはまだ goroutine バックグラウンドで非同期に実行されています。 main メソッドは、他の Future
をトリガーするなど、他のコードを引き続き実行できます。結果が必要な場合は、チャネルから結果を読み取る必要があります。 http リクエストが返されていない場合、現在のゴルーチンは結果が返されるまでブロックされます。
ただし、上記の方法にはまだいくつかの制限があります。エラーは返せません。上記の例では、http リクエストでエラーが発生した場合、body の値は nil/empty になります。ただし、チャネルは 1 つの値しか返せないため、返された 2 つの結果をラップする別の構造体を作成する必要があります。
変更後の結果:
// RequestFutureV2 return value and error func RequestFutureV2(url string) func() ([]byte, error) { var body []byte var err error c := make(chan struct{}, 1) go func() { defer close(c) var res *http.Response res, err = http.Get(url) if err != nil { return } defer res.Body.Close() body, err = ioutil.ReadAll(res.Body) }() return func() ([]byte, error) { <-c return body, err } }
このメソッドは 2 つの結果を返し、最初のメソッドの制限を解決します。使用すると、次のようになります。
func main() { futureV2 := RequestFutureV2("https://api.github.com/users/octocat/orgs") // not block log.Printf("V2 is this locked again") bodyV2, err := futureV2() // block if err == nil { log.Printf("V2 response length %d\n", len(bodyV2)) } else { log.Printf("V2 error is %v\n", err) } }
上記の変更の利点は、futureV2()
メソッドを複数回呼び出すことができることです。そして、どちらも同じ結果を返すことができます。
ただし、この方法を使用してさまざまな非同期関数を実装したい場合は、大量の追加コードを記述する必要があります。この困難を克服するための util メソッドを作成できます。
// Future boilerplate method func Future(f func() (interface{}, error)) func() (interface{}, error) { var result interface{} var err error c := make(chan struct{}, 1) go func() { defer close(c) result, err = f() }() return func() (interface{}, error) { <-c return result, err } }
Future
メソッドを呼び出すと、ルーム内の多くのチャネル トリックが実行されます。汎用的な目的を達成するために、[]buyte
->interface{}
->[]byte
からの型変換があります。エラーが発生すると、ランタイム panic がトリガーされます。