Goの遅延を理解する

Go

前書き

Goには、「+ if 」、「 switch 」、「 for 」など、他のプログラミング言語で見られる一般的な制御フローキーワードの多くがあります。 他のほとんどのプログラミング言語では見られないキーワードの1つは「 defer +」です。あまり一般的ではありませんが、プログラムでどれほど役立つかがすぐにわかります。

`+ defer `ステートメントの主な用途の1つは、開いているファイル、ネットワーク接続、https://en.wikipedia.org/wiki/Handle_(computing)[データベースハンドル]などのリソースのクリーンアップです。 プログラムがこれらのリソースで終了したら、プログラムの制限を使い果たすことを避け、他のプログラムがそれらのリソースにアクセスできるようにするために、それらを閉じることが重要です。 ` defer +`は、ファイル/リソースを閉じる呼び出しを開いた呼び出しの近くに保持することにより、コードをクリーンにし、エラーを起こしにくくします。

この記事では、リソースをクリーンアップするために `+ defer `ステートメントを適切に使用する方法と、 ` defer +`を使用する際によくあるいくつかの間違いを学習します。

`+ defer +`ステートメントとは

`+ defer `ステートメントは、 ` defer +`キーワードに続くhttps://www.digitalocean.com/community/tutorials/how-to-define-and-call-functions-in-go[function]呼び出しを追加しますスタック。 そのスタック上のすべての呼び出しは、追加された関数が戻るときに呼び出されます。 呼び出しはスタックに配置されるため、後入れ先出しの順序で呼び出されます。

テキストを印刷して、 `+ defer +`がどのように機能するかを見てみましょう。

main.go

package main

import "fmt"

func main() {
   defer fmt.Println("Bye")
   fmt.Println("Hi")
}

`+ main `関数には、2つのステートメントがあります。 最初のステートメントは、 ` defer `キーワードで始まり、 ` Bye `を出力する ` print `ステートメントが続きます。 次の行は、「 Hi +」を出力します。

プログラムを実行すると、次の出力が表示されます。

OutputHi
Bye

最初に `+ Hi `が出力されたことに注意してください。 これは、「 defer 」キーワードが前にあるステートメントは、「 defer +」が使用された関数の最後まで呼び出されないためです。

プログラムをもう一度見てみましょう。今回は、何が起こっているのかを説明するためにコメントを追加します。

main.go

package main

import "fmt"

func main() {
   // defer statement is executed, and places
   // fmt.Println("Bye") on a list to be executed prior to the function returning
   defer fmt.Println("Bye")

   // The next line is executed immediately
   fmt.Println("Hi")

   // fmt.Println*("Bye") is now invoked, as we are at the end of the function scope
}

`+ defer `を理解する鍵は、 ` defer `ステートメントが実行されると、遅延関数への引数がすぐに評価されることです。 ` defer +`が実行されると、関数が戻る前に呼び出されるリストに、それに続くステートメントを配置します。

このコードは、「+ defer 」が実行される順序を示していますが、Goプログラムを作成するときに使用される一般的な方法ではありません。 ` defer +`を使用して、ファイルハンドルなどのリソースをクリーンアップする可能性が高くなります。 次にその方法を見てみましょう。

`+ defer +`を使用してリソースをクリーンアップする

Goでは、 `+ defer `を使用してリソースをクリーンアップすることが非常に一般的です。 まず、ファイルに文字列を書き込むが、リソースのクリーンアップを処理するために「 defer +」を使用しないプログラムを見てみましょう。

main.go

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   if err := write("readme.txt", "This is a readme file"); err != nil {
       log.Fatal("failed to write file:", err)
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }
   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }
   file.Close()
   return nil
}

このプログラムには、最初にファイルを作成しようとする「+ write 」という関数があります。 エラーがある場合、エラーを返し、関数を終了します。 次に、指定されたファイルに文字列「 This is a readme file」を書き込もうとします。 エラーを受け取ると、エラーを返し、関数を終了します。 次に、関数はファイルを閉じてシステムにリソースを解放しようとします。 最後に、関数は `+ nil +`を返し、関数がエラーなしで実行されたことを示します。

このコードは機能しますが、微妙なバグがあります。 `+ io.WriteString +`の呼び出しが失敗した場合、関数はファイルを閉じずにリソースをシステムに解放せずに戻ります。

別の `+ file.Close()`ステートメントを追加することで問題を修正できます。これは、 ` defer +`のない言語でこれを解決する方法です。

main.go

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   if err := write("readme.txt", "This is a readme file"); err != nil {
       log.Fatal("failed to write file:", err)
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }
   _, err = io.WriteString(file, text)
   if err != nil {

       return err
   }
   file.Close()
   return nil
}

これで、「+ io.WriteString +」の呼び出しが失敗した場合でも、ファイルを閉じます。 これは、見つけて修正するのが比較的簡単なバグでしたが、より複雑な機能を備えていましたが、見逃されていた可能性があります。

2番目の呼び出しを `+ file.Close()`に追加する代わりに、 ` defer `ステートメントを使用して、実行中にどの分岐が取られるかに関係なく、常に ` Close()+`を呼び出すことができます。

`+ defer +`キーワードを使用するバージョンは次のとおりです。

main.go

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   if err := write("readme.txt", "This is a readme file"); err != nil {
       log.Fatal("failed to write file:", err)
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }

   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }
   return nil
}

今回は、次のコード行を追加しました: + defer file.Close()+。 これは、関数 `+ write `を終了する前に ` file.Close +`を実行するようコンパイラーに指示します。

コードを追加して、将来関数を終了する別のブランチを作成しても、常にファイルをクリーンアップして閉じるようにしました。

ただし、遅延を追加することにより、さらに別のバグを導入しました。 `+ Close `メソッドから返される可能性のある潜在的なエラーをチェックしなくなりました。 これは、 ` defer +`を使用する場合、関数に戻り値を返す方法がないためです。

Goでは、プログラムの動作に影響を与えることなく、 `+ Close()`を複数回呼び出すことは安全で受け入れられている慣行と見なされます。 ` Close()+`がエラーを返す場合、最初に呼び出されたときにエラーを返します。 これにより、関数の実行の成功パスで明示的に呼び出すことができます。

「+ Close 」の呼び出しを「 defer +」し、エラーが発生した場合は引き続きエラーを報告する方法を見てみましょう。

main.go

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   if err := write("readme.txt", "This is a readme file"); err != nil {
       log.Fatal("failed to write file:", err)
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }
   defer file.Close()
   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }

   return
}

このプログラムでの唯一の変更は、 `+ file.Close()`を返す最後の行です。 ` Close `の呼び出しでエラーが発生した場合、呼び出し関数に期待どおりに返されるようになりました。 ` defer file.Close()`ステートメントも ` return`ステートメントの後に実行されることに注意してください。 これは、 `+ file.Close()+`が2回呼び出される可能性があることを意味します。 これは理想的ではありませんが、プログラムに副作用を引き起こしてはならないため、受け入れられる方法です。

ただし、 `+ WriteString `を呼び出すときなど、関数の早い段階でエラーを受け取った場合、関数はそのエラーを返し、延期されたために ` file.Close `を呼び出そうとします。 ` file.Close +`もエラーを返す可能性があります(おそらくそうなる可能性があります)が、何が問題なのかを教えてくれる可能性が高いエラーを受け取ったので、もはや気にすることではありません。

これまで、リソースを適切にクリーンアップするために、単一の `+ defer `を使用する方法を見てきました。 次に、複数のリソースをクリーンアップするために複数の ` defer +`ステートメントを使用する方法を確認します。

複数の `+ defer +`ステートメント

関数に複数の `+ defer `ステートメントがあるのは普通です。 複数の遅延を導入するとどうなるかを見るために、 ` defer +`ステートメントのみを含むプログラムを作成しましょう:

main.go

package main

import "fmt"

func main() {
   defer fmt.Println("one")
   defer fmt.Println("two")
   defer fmt.Println("three")
}

プログラムを実行すると、次の出力が表示されます。

Outputthree
two
one

この順序は、 `+ defer +`ステートメントを呼び出した順序と逆であることに注意してください。 これは、呼び出される各遅延ステートメントが前のステートメントの上にスタックされ、関数がスコープ(Last In、First Out)を出るときに逆に呼び出されるためです。

関数で必要な数の遅延呼び出しを行うことができますが、それらはすべて、実行された順序と逆の順序で呼び出されることを覚えておくことが重要です。

複数の遅延が実行される順序がわかったので、複数の遅延を使用して複数のリソースをクリーンアップする方法を見てみましょう。 ファイルを開いて書き込み、その後再び開いて内容を別のファイルにコピーするプログラムを作成します。

main.go

package main

import (
   "fmt"
   "io"
   "log"
   "os"
)

func main() {
   if err := write("sample.txt", "This file contains some sample text."); err != nil {
       log.Fatal("failed to create file")
   }

   if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil {
       log.Fatal("failed to copy file: %s")
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }
   defer file.Close()
   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }

   return file.Close()
}

func fileCopy(source string, destination string) error {
   src, err := os.Open(source)
   if err != nil {
       return err
   }
   defer src.Close()

   dst, err := os.Create(destination)
   if err != nil {
       return err
   }
   defer dst.Close()

   n, err := io.Copy(dst, src)
   if err != nil {
       return err
   }
   fmt.Printf("Copied %d bytes from %s to %s\n", n, source, destination)

   if err := src.Close(); err != nil {
       return err
   }

   return dst.Close()
}

`+ fileCopy `という新しい関数を追加しました。 この関数では、まずコピー元のソースファイルを開きます。 ファイルを開くときにエラーが発生したかどうかを確認します。 その場合、エラーを「 return 」して関数を終了します。 それ以外の場合は、開いたばかりのソースファイルの終了を「 defer +」します。

次に、宛先ファイルを作成します。 繰り返しますが、ファイルの作成中にエラーが発生したかどうかを確認します。 もしそうなら、そのエラーを `+ return `して関数を終了します。 それ以外の場合、宛先ファイルの ` Close()`を ` defer `します。 関数がスコープを出るときに呼び出される2つの ` defer +`関数があります。

両方のファイルを開いたので、ソースファイルから宛先ファイルにデータを `+ Copy()`します。 それが成功した場合、両方のファイルを閉じようとします。 いずれかのファイルを閉じようとしてエラーを受け取った場合、エラーを「 return +」して関数スコープを終了します。

`+ defer `も ` Close()`を呼び出しますが、各ファイルに対して明示的に ` Close()+`を呼び出すことに注意してください。 これは、ファイルを閉じるときにエラーが発生した場合にエラーを報告するためです。 また、何らかの理由で関数がエラーで早期に終了した場合、たとえば2つのファイル間でコピーに失敗した場合、各ファイルは遅延呼び出しから適切に閉じようとします。

結論

この記事では、 `+ defer `ステートメントについて学び、プログラムでシステムリソースを適切にクリーンアップするためにそれをどのように使用できるかを学びました。 システムリソースを適切にクリーンアップすると、プログラムのメモリ使用量が少なくなり、パフォーマンスが向上します。 「 defer +」の使用場所の詳細については、パニックの処理に関する記事を読むか、https://www.digitalocean.com/community/tutorial_series/how-to-code-in-go [How To Code Goシリーズ]。

前の投稿:Python 3でデータ型を変換する方法
次の投稿:Ubuntu 16.04でCassandraとElasticSearchを使用してTitan Graphデータベースをセットアップする方法