ホームページ  >  記事  >  バックエンド開発  >  品質メモを共有する: Go クロージャーの使用方法を学ぶ

品質メモを共有する: Go クロージャーの使用方法を学ぶ

藏色散人
藏色散人転載
2021-08-26 16:36:412191ブラウズ

#go 言語 # の次のチュートリアル コラムでは、go クロージャの学習と使用方法を紹介します。困っている友人の役に立てば幸いです。

go クロージャ

Golang では、クロージャは外部の変数に対する関数です。範囲。 言い換えると、クロージャは、それが作成されたスコープ内の変数にアクセスできる内部関数です。外部関数が実行を完了し、スコープが破棄された場合でも、引き続きアクセスできます。
クロージャについて詳しく説明する前に、匿名関数とは何かを理解する必要があります。

#匿名関数名前が示すように、匿名関数は名前のない関数です。

たとえば、一般関数と匿名関数を作成します。

package mainimport (
    "fmt")func sayhi1() { // 一般函数
    fmt.Println("hello golang, I am Regular function")}func main() {
    sayhi1()
    sayhi2 := func() { //匿名函数
        fmt.Println("hello golang, I am Anonymous function")
    }
    sayhi2()}

結果出力:
hello golang, I am Regular functionhello golang, I am Anonymous function

関数を返す関数を作成して、匿名関数を使用します。

package mainimport (
    "fmt")func sayhello(s string) func() {
    return func() {
        fmt.Println("hello", s)
    }}func main() {
    sayhello := sayhello("golang")
    sayhello()}

結果出力:

hello golang

通常の関数と匿名関数の唯一の違いは、匿名関数はパッケージ レベルで宣言されず、動的に宣言され、通常は使用されるか、忘れられるか、または 後で使用するために変数に割り当てられます。

クロージャの本質クロージャは、このコード ブロックまたはグローバル コンテキスト内にない自由変数を含むコード ブロックです。ただし、コード ブロックが定義されている環境にあります。自由変数はコード ブロックに含まれているため、これらの自由変数とそれらが参照するオブジェクトは、クロージャがまだ使用されている限り解放されません。実行されるコードは、自由変数にバインドされたコンピューティング環境を提供します。

クロージャの価値は、関数オブジェクトまたは匿名関数として使用できることです。型システムの場合、これはデータを表すだけでなくコードも表すことを意味します。クロージャをサポートするほとんどの言語は、関数を第 1 レベルのオブジェクトとして使用します。つまり、これらの関数は変数に格納し、パラメータとして他の関数に渡すことができます。最も重要なことは、関数を動的に作成して関数によって返すことができることです。


Golang のクロージャは関数の外部の変数も参照します。クロージャを実装すると、クロージャが使用されている限り、クロージャによって参照される変数が常に存在することが保証されます。正式には、匿名関数はクロージャです。

クロージャの例を見てみましょう:

package mainimport (
    "fmt")func caller() func() int {
    callerd := 1
    sum := 0
    return func() int {
        sum += callerd        return sum    }}func main() {
    next := caller()
    fmt.Println(next())
    fmt.Println(next())
    fmt.Println(next())}

結果出力:
1
2
3

この例では、called と sum は自由変数であり、呼び出し側関数によって返される匿名関数は次の計算を提供します。自由変数: 環境、匿名関数、および自由変数で構成されるコード ブロックは、実際にはクロージャです。クロージャ関数では、呼び出される自由変数と sum にアクセスできるのは匿名関数のみですが、他の手段ではアクセスできないため、クロージャの自由変数は安全です。

命令型言語の規則に従って、呼び出し側関数は匿名関数のアドレスのみを返しますが、匿名関数が実行されると、そのスコープ内に合計と呼び出された変数が見つからないため、エラーが発生します。関数型言語では、埋め込み関数本体が本体外の変数を参照する場合、定義に関わる参照環境と関数本体をまとめて(クロージャ)返します。クロージャの使用は、通常の関数呼び出しと何ら変わりません。


ここで、参照環境の定義を示します。これは、プログラム実行の特定の時点でアクティブなすべての制約のセットであり、制約とは、変数の名前とそれが表すオブジェクトとの間の関係を指します。

だから、クロージャ = 関数参照環境と言います


実際には、クロージャ関数をクラス (C) とみなすことができます。クロージャ関数呼び出しは、オブジェクトとクロージャの自由変数をインスタンス化することです。これらはクラスのメンバー変数であり、クロージャ関数のパラメータはクラスの関数オブジェクトのパラメータです。この例では、next はインスタンス化されたオブジェクトとみなすことができ、next() はオブジェクト関数呼び出しの戻り値とみなすことができます。

これは有名な格言を思い出させます: オブジェクトは動作を伴うデータであり、クロージャはデータを伴う動作です


クロージャの使用例

クロージャを使用したデータ分離の実現
    関数が終了した後も保持されるデータにアクセスできる関数を作成するとします。たとえば、関数が呼び出された回数をカウントしたいが、他の人がそのデータにアクセスできるようにしたくない場合 (誤ってデータを変更しないように)、クロージャを使用してそれを行うことができます。

  • package mainimport (
        "fmt")func caller() func() int {
        callerd := 0
        return func() int {
            callerd++
            return callerd    }}func main() {
        next := caller()
        fmt.Println(next())
        fmt.Println(next())
        fmt.Println(next())}
  • 結果出力:
1
2
3
  • 利用闭包包装函数和创建中间件
    Go 中的函数是一等公民。这意味着您不仅可以动态创建匿名函数,还可以将函数作为参数传递给函数。例如,在创建 Web 服务器时,通常会提供一个功能来处理Web 请求到特定的路由。
package mainimport (
  "fmt"
  "net/http")func main() {
  http.HandleFunc("/hello", hello)
  http.ListenAndServe(":3000", nil)}func hello(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "<h1>Hello!</h1>")}

在上面例子中,函数 hello() 被传递给 http.HandleFunc() 函数,并在该路由匹配时调用。
虽然这段代码不需要闭包,但如果我们想用更多逻辑包装我们的处理程序,闭包是非常有用的。一个完美的例子是我们可以通过创建中间件来在我们处理程序执行之前或之后做一些其它的工作。
什么是中间件?
中间件基本上是可重用功能的一个奇特术语,它可以在设计用于处理 Web 请求的代码之前和之后运行代码。在 Go 中,这些通常是通过闭包来实现的,但在不同的编程语言中,可以通过其他方式来实现。
在编写 Web 应用程序时使用中间件很常见,而且它们不仅可用于计时器(您将在下面看到一个示例)。例如,中间件可用于编写代码验证用户是否登录过一次,然后将其应用到你的所有会员专页。
让我们看看一个简单的计时器中间件在 Go 中是如何工作的。

package mainimport (
  "fmt"
  "net/http"
  "time")func main() {
  http.HandleFunc("/hello", timed(hello))
  http.ListenAndServe(":3000", nil)}func timed(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
  return func(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    f(w, r)
    end := time.Now()
    fmt.Println("The request took", end.Sub(start))
  }}func hello(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "<h1>Hello!</h1>")}

timed() 函数接受一个可以用作处理函数的函数,并返回一个相同类型的函数,但返回的函数与传递它的函数不同。返回的闭包记录当前时间,调用原始函数,最后记录结束时间并打印出请求的持续时间。同时对我们的处理程序函数内部实际发生的事情是不可知的。
现在我们要做的就是将我们的处理程序包装在 timed(handler) 中并将闭包传递给 http.HandleFunc() 函数调用。

  • 利用闭包使用 sort 二分搜索
    使用标准库中的包也经常需要闭包,例如 sort 包。这个包为我们提供了大量有用的功能和代码,用于排序和搜索排序列表。例如,如果您想对一个整数切片进行排序,然后在切片中搜索数字 7,您可以像这样使用 sort 包。
package mainimport (
  "fmt"
  "sort")func main() {
  numbers := []int{1, 11, -5, 7, 2, 0, 12}
  sort.Ints(numbers)
  fmt.Println("Sorted:", numbers)
  index := sort.SearchInts(numbers, 7)
  fmt.Println("7 is at index:", index)}

结果输出:

Sorted: [-5 0 1 2 7 11 12]7 is at index: 4

如果要搜索的每个元素都是自定义类型的切片会发生什么?或者,如果您想找到第一个等于或大于 7 的数字的索引,而不仅仅是 7 的第一个索引?
为此,您可以使用 sort.Search() 函数,并且您需要传入一个闭包,该闭包可用于确定特定索引处的数字是否符合您的条件。
sort.Search() is a binary search
sort.Search 函数执行二分搜索,因此它需要一个闭包,该闭包在满足您的条件之前对任何索引返回 false,在满足后返回 true。
让我们使用上面描述的示例来看看它的实际效果;我们将搜索列表中第一个大于或等于 7 的数字的索引。

package mainimport (
    "fmt"
    "sort")func main() {
    numbers := []int{1, 11, -5, 8, 2, 0, 12}
    sort.Ints(numbers)
    fmt.Println("Sorted:", numbers)

    index := sort.Search(len(numbers), func(i int) bool {
        return numbers[i] >= 7
    })
    fmt.Println("The first number >= 7 is at index:", index)
    fmt.Println("The first number >= 7 is:", numbers[index])}

结果输出:

Sorted: [-5 0 1 2 8 11 12]The first number >= 7 is at index: 4
The first number >= 7 is: 8

在这个例子中,我们的闭包是作为第二个参数传递给 sort.Search() 的简单函数。
这个闭包访问数字切片,即使它从未被传入,并为任何大于或等于 7 的数字返回 true。通过这样做,它允许 sort.Search() 工作而无需了解什么您使用的基础数据类型是什么,或者您试图满足什么条件。它只需要知道特定索引处的值是否符合您的标准。

  • 用闭包+defer进行处理异常
package mainimport (
    "fmt")func handle() {
    defer func() {
        err := recover()
        if err != nil {
            fmt.Println("some except had happend:", err)
        }
    }()
    var a *int = nil
    *a = 100}func main() {
    handle()}

结果输出:

some except had happend: runtime error: invalid memory address or nil pointer dereference

recover函数用于终止错误处理流程。一般情况下,recover应该在一个使用defer关键字的函数中执行以有效截取错误处理流程。如果没有在发生异常的goroutine中明确调用恢复过程(调用recover函数),会导致该goroutine所属的进程打印异常信息后直接退出
对于第三方库的调用,在不清楚是否有panic的情况下,最好在适配层统一加上recover过程,否则会导致当前进程的异常退出,而这并不是我们所期望的。

                                      

以上が品質メモを共有する: Go クロージャーの使用方法を学ぶの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はlearnku.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。