← 記事一覧へ

Go 学習記録(8):関数・defer・エラー

Go 基礎学習の第 8 回。文法、実例、学習の過程を記録します。

関数

Go は通常の関数、無名関数、クロージャ、メソッドを提供します。

関数は第一級の値で、変数に代入したり、引数や戻り値にしたりできます。

メソッドを持つ定義された関数型は、インターフェースを満たせます。

1. 関数を宣言する

基本の形式:

func 函数名(参数列表) (返回值列表) {
    函数体
}

引数は省略でき、複数ならカンマで区切ります。

戻り値も省略でき、複数の値を返せます。戻り値のある関数では、通常の終了経路でそれらを返す必要があります。

簡単な例:

func add(a, b int, c float32) (int, error) {
    return a + b, nil
}

Go は値渡しで、引数のコピーを受け取ります。引数への再代入では、呼び出し側の変数は変わりません。

スライスの記述子は、コピー後も基底配列を共有する場合があります。共有要素の変更は、呼び出し側に見えます。

拡張で新しい基底配列が必要になった場合、その新しい配列への変更は、古い配列には影響しません。

戻り値に名前を付けることもできます。関数内の変数になるので、別の宣言は不要です。

例:

func add(a, b int) (sum int, err error) {
    sum = a + b
    return sum, err
    //也可以直接写return
}

2. 可変長引数

渡す値の数が変わるときは、可変長引数を使います。

基本の形式:

func 函数名(参数列表...类型) (返回值列表) {
    函数体
}

簡単な例:

func add(a ...int)(sum int, err error) {
    for _, value := range a {
        sum += value
    }
}
c := 1
d := 2
sum, _ := add(a, b, 3, 4)
fmt.Println(sum)

関数内の a は int のスライスです。a[0]、a[1] やループで要素にアクセスできます。

3. 第一級の値としての関数

関数を引数、戻り値、変数として扱えます。

柔軟性と再利用性を高めます。

3.1. 変数へ代入する

例:

func add(a ...int)(sum int, err error) {
    for _, value := range a {
        sum += value
    }
}

func main(){
    //这里将add函数作为参数赋值给变量funcVar
    funcVar := add
    c := 1
    d := 2
    sum, _ := funcVar(a, b, 3, 4)
    fmt.Println(sum)
}

3.2. 関数を返す

例:

func cal(op string) func(){
    switch op {
    case "+":
        return func(){
            fmt.Println("这是加法")
        }

    case "-":
        return func(){
            fmt.Println("这是减法")
        }
    default:
        return func(){
            fmt.Println("这不是加法也不是减法")
        }
    }
}

//因为返回的是函数,所以在调用的时候需要加上()
cal("+")()

3.3. 関数を引数にする

書き方を示すための簡単な例です。

func add(a, b int) int{
    fmt.Printf("sum is %d\n", a + b)
}
func cal(y int, myfunc func(int, int)) {
    myfunc(y, 2)
}

cal(1, add)

//输出结果为sum is 3
//调用cal函数传递add函数成为myfunc, 然后1+2

3.4. 無名関数

宣言名のない関数で、値として渡したり返したりできます。

値が必要な場所で直接定義できます。

//匿名函数作为参数进行传递
func add(a, b int) int{
    fmt.Printf("sum is %d\n", a + b)
}
func cal(y int, myfunc func(int, int)) {
    myfunc(y, 2)
}

cal(1, func(a, b int){
    fmt.Printf("total is %d\n", a + b)
})

//输出结果为total is 3
//调用cal函数传递临时定义的匿名函数函数成为myfunc, 然后1+2


//匿名函数作为变量进行赋值
func cal(y int, myfunc func(int, int)) {
    myfunc(y, 2)
}
localFunc := func(a, b int){
    fmt.Printf("local is %d\n", a + b)
}

cal(1, localFunc)

//输出结果为local is 3
//调用cal函数传递localFunc成为myfunc, 然后1+2,跟上边的效果是一样的

4. クロージャ

呼び出すたびにカウンタを増やす関数を考えます。

一つの方法は、パッケージ変数を増やして返すことです。

パッケージ変数を使う例:

var local int
func add() int{
    local += 1
    return local
}

for i := 0; i < 5; i++ {
    fmt.Println(add())
}

共有されたグローバル状態ができるため、独立したカウンタやリセットを扱いにくくなります。

クロージャを使う方法があります。

func auotoAdd() func() int {
    local := 0
    return func() int {
        return local
    }
}

nextFunc := auotoAdd()
for i := 0; i < 5; i++ {
    fmt.Println(nextFunc())
}
//输出结果为0 1 2 3 4
//想要归零的话,只需要重新调用auotoAdd()函数即可

クロージャは、外側の関数が終了した後も、周囲のスコープの変数へアクセスできる関数です。

例えば、無名関数が外側の関数のローカル変数を捕捉します。

5. defer の用途

defer は、関数が戻る際に呼び出す処理を登録します。資源の後処理によく使います。目的は finally に似ていますが、動作の規則は異なります。

ファイルやロックなどは、成功・失敗にかかわらず後処理しないと、資源が漏れる場合があります。

// 一个在Java中的例子
try {
    // 可能会发生异常的代码
} catch (Exception e) {
    // 异常处理代码
} finally {
    // 无论是否发生异常,都会执行的代码
}

Java の finally は取得処理から離れることがあります。Go の defer は、取得の近くに後処理を書けます。

基本の使い方:

var mu sync.Mutex

mu.lock()
defer mu.unlock() //defer后面的代码会在return之前执行

取得の直後に defer を置くと、後処理を確認しやすくなります。

defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("4")
//输出结果为4 3 2 1

defer はスタックのように、後から登録した順に実行されます。

遅延実行するクロージャから、名前付きの戻り値を変更できます。

// 在这个例子中,defer 推迟执行的是一个匿名函数
// deferReturn 函数被调用,准备返回值 10。
// 在返回之前,defer 推迟执行的匿名函数被执行。
// 匿名函数中,result 被增加 1,因此 result 的值从 10 变为 11。
// deferReturn 函数返回修改后的 result 值,即 11。
// 因此,尽管 deferReturn 函数的返回语句是 return 10,但由于 defer 推迟执行的函数在返回之前修改了 result 的值,所以最终的输出结果是 11。这也展示了在 Go 语言中,defer 可以修改函数的返回值。

// 搞不懂就看源码

func deferReturn() (result int){
    defer func(){
        result++
    }()
    return 10
}

ret := deferReturn()
fmt.Println(ret) //输出结果为11

6. error、recover、panic

重要な概念は三つです。

1、error

2、panic

3、recover

通常の失敗には error 値を使い、例外で普通の制御フローを組み立てないようにします。

他の言語では、処理を try-catch で囲むことがあります。 {: .prompt-tip }

Go の関数は、通常 error を返して成功・失敗を知らせます。呼び出し側が扱うため、「if err != nil」がよく現れます。

明示的なエラー処理は、防御的な実装の一部です。

panic は通常の error と別です。現在の goroutine を巻き戻し、defer を実行します。適切な defer 内の recover で停止できますが、回復しなければプログラムが終了します。

6.1、error

errors パッケージで error 値を作れます。例:

import (
    "fmt"
    "errors"
)

func A() (int, error) {
    return 1, errors.New("这是一个错误")
}

func main() {
    if _, err := A(); err!= nil {
        fmt.Println(err)
    }
}

6.2、panic

panic は組み込みです。回復されない panic はプログラムを終了させるため、通常の error の代わりには使いません。

別の例:

import (
    "fmt"
    "errors"
)

func A() (int, error) {
    panic("this is a panic")
    fmt.Println("能执行到这吗?")
    return 1, errors.New("这是一个错误")
}

func main() {
    if _, err := A(); err!= nil {
        fmt.Println(err)
    }
}

// 在上面的例子中,panic会导致程序退出,所以fmt.Println("能执行到这吗?")不会被执行,只会打印错误栈

panic は慎重に使います。ここでは起動時の失敗を考えます。

サービスは起動前に、設定、ログ、データベースなどの必要な依存を準備します。

必須の条件を満たせないときは、明確に起動を失敗させます。panic も一つの方法ですが、error を返すだけで十分な場合もあります。

稼働中のサーバーで未回復の panic が起きると、プロセスが停止し、重大な障害になります。

実行時の検査からも、例えば次の場合に panic が起きます。

  1. インデックスが範囲外

  2. nil ポインタの間接参照

……

適切な defer の中で recover を呼ぶと、panic を停止できます。ただし、panic が起きた文から再開するわけではありません。

recover は、panic に渡された値をインターフェース値として返します。

6.3、recover

例:

// 在go中map的使用需要进行初始化,否则会报错,这是一个会被动触发panic的场景

func A() {
    defer func(){
        if err := recover(); err!= nil {
            fmt.Println("recoverd is A(): ", err)
        }
    }()
    var names map[string]string
    names["xiaoming"] = "go工程师"
    return 0, errors.New("这是一个错误")
}
// 上面的代码会输出recoverd is A(): assignment to entry nil map

ほかの注意点:

  1. panic が起きた goroutine の defer 関数から、直接 recover を呼びます。

  2. panic より前に defer を登録しておく必要があります。

  3. 回復しても panic の場所から処理は再開しません。

  4. 複数の defer は、登録と逆の順序で実行されます。

この記事の日付について

旧サイトに記載された日付を保持しています。実際に書き始めた時期は、これより早い可能性があります。

同じシリーズ · Go の基礎学習

前の記事Go 学習記録(7):配列・スライス・map・リスト ←次の記事Go 学習記録(9):構造体とメソッド →