[Golang] Function深化

2021-11-21 hit count image

GolangでFunction(関数)をもうちょっと詳しく調べて、関数を活用する色んな方法について説明します。

概要

今回のブログポストではGolangでFunction(関数)について詳しく見て、関数を使う色んな方法について説明します。このブログポストで紹介するコードは次のリンクで確認できます。

関数の基礎

Golangで関数を扱う基礎的な方法は以前のブログポストを参照してください。

可変引数関数

Golangでは関数で次のように可変引数(Variable argument list)を使うことが出来ます。

fmt.Println()
fmt.Println(1)
fmt.Println(1, 2, 3, 4, 5)

Golangでは次のように...キーワードを使って可変引数関数を作ることが出来ます。

func FUNCTION_NAME(param ...TYPE) TYPE {
  // Code block
}

これを確認するためmain.goファイルを生成して次のように修正します。

package main

import "fmt"

func sum(nums ...int) int {
  fmt.Printf("nums type: %T\n", nums)

  sum := 0
  for _, num := range nums {
    sum += num
  }
  return sum
}
func main() {
  fmt.Println(sum(1, 2, 3, 4, 5))
  fmt.Println(sum(10, 20))
  fmt.Println(sum())
}

これを実行すると下記のような結果が表示されます。

# go run main.go
nums type: []int
15
nums type: []int
30
nums type: []int
0

defer

Golangはdeferと言う遅延実行構文を提供してます。deferは関数が終了される直前実行されることが保証されます。

defer STATEMENT

これを確認するためmain.goファイルを次のように修正します。

package main

import "fmt"

func print() {
  defer fmt.Println("World!")
  fmt.Println("Hello")
}

func main() {
  print()
}

これを実行すると次のような結果が表示されることが確認できます。

# go run main.go
Hello
World!

deferはファイルを読むためファイルを開くなど、おもじOSリソースを返すとき使います。これを確認するためmain.goファイルを次のように修正します。

package main

import "os"

func readFile() {
  f, err := os.Open("file.txt")

  if err != nil {
    return
  }

  f.Close()
}

func readFileDefer() {
  f, err := os.Open("file.txt")
  defer f.Close()

  if err != nil {
    return
  }
}

func main() {
  readFile()
  readFileDefer()
}

deferを使わない場合、Close関数前で、エラーが発生すると、Close関数はコールされません。

func readFile() {
  f, err := os.Open("file.txt")

  if err != nil {
    return
  }

  f.Close()
}

しかし、deferを使うとエラーが発生しても、Close関数のコールが保証されます。

func readFileDefer() {
  f, err := os.Open("file.txt")
  defer f.Close()

  if err != nil {
    return
  }
}

関数タイプ変数

Golangでは関数もタイプで使うことが出来ます。ここで関数タイプ変数とは関数を値で持つ変数を言います。関数もメモリにアドレスを持ってるので、変数に割り当てることが出来ます。

関数タイプは次のように関数シグネチャー(Function signature)で表現します。

// Function
func add(a, b int) int {
  return a + b
}

// Signature
func (int int) int

これを確認するため、main.goファイルを次のように修正します。

package main

import "fmt"

func add(a, b int) int {
  return a + b
}

func multiple(a, b int) int {
  return a * b
}

func getOperator(op string) func(int, int) int {
  if op == "+" {
    return add
  } else if op == "*" {
    return multiple
  } else {
    return nil
  }
}

func main() {
  var op func(int, int) int

  op = getOperator("+")
  result := op(10, 20)
  fmt.Println(result)

  op = getOperator("*")
  result = op(10, 20)
  fmt.Println(result)
}

これを実行すると次のような結果が表示されます。

# go run main.go
30
200

関数シグネチャーのエイリアスタイプ

関数シグネチャーにエイリアスタイプを使うともっと簡単に関数タイプを使えます。

// Function signature alias type
type OperateFunc func(int, int) int

func getOperator(op string) OperateFunc {
}

これを確認するためmain.goファイルを次のように修正します。

package main

import "fmt"

type OperateFunc func(int, int) int

func add(a, b int) int {
  return a + b
}

func multiple(a, b int) int {
  return a * b
}

func getOperator(op string) OperateFunc {
  if op == "+" {
    return add
  } else if op == "*" {
    return multiple
  } else {
    return nil
  }
}

func main() {
  var op OperateFunc

  op = getOperator("+")
  result := op(10, 20)
  fmt.Println(result)

  op = getOperator("*")
  result = op(10, 20)
  fmt.Println(result)
}

これを実行すると次のような結果が表示されます。

# go run main.go
30
200

エイリアスタイプを使うと、関数シグネチャーをもっと簡単に使えることが分かります。

関数リテラル

Golangでも関数リテラル(匿名関数)を使うことが出来ます。

f := func(a, b int) int {
}

これを確認するためmain.goファイルを次のように修正します。

package main

import "fmt"

type OperateFunc func(int, int) int

func getOperator(op string) OperateFunc {
  if op == "+" {
    return func(a, b int) int {
      return a + b
    }
  } else if op == "*" {
    return func(a, b int) int {
      return a * b
    }
  } else {
    return nil
  }
}

func main() {
  var op OperateFunc

  op = getOperator("+")
  result := op(10, 20)
  fmt.Println(result)

  op = getOperator("*")
  result = op(10, 20)
  fmt.Println(result)
}

これを実行すると次のような結果が表示されます。

# go run main.go
30
200

Golangでは一般的に関数は状態を持つことができませんが、関数リテラルは内部に状態を持つことができます。これを確認するためmain.goファイルを次のように修正します。

package main

import "fmt"

func main() {
  i := 0

  f := func() {
    i += 10
  }

  i++

  f()

  fmt.Println(i)
}

これを実行すると次のような結果が表示されます。

# go run main.go
11

一般関数は外部で宣言された変数にアクセスがでkないですが、関数リテラルは外部に宣言された変数にアクセス(キャプチャ)して使うことができます。ここでキャプチャ(Captured)された値は値コピーではなくリファレンスコピーです。つまり、ポインターがコピーされると言えます。

これを確認するためmain.goファイルを次のように修正します。

package main

import "fmt"

func CaptureLoop1() {
  f := make([]func(), 3)
  fmt.Println("CaptureLoop1")

  for i := 0; i < 3; i++ {
    f[i] = func() {
      fmt.Println(i)
    }
  }

  for i := 0; i < 3; i++ {
    f[i]()
  }
}

func CaptureLoop2() {
  f := make([]func(), 3)
  fmt.Println("CaptureLoop2")

  for i := 0; i < 3; i++ {
    v := i
    f[i] = func() {
      fmt.Println(v)
    }
  }

  for i := 0; i < 3; i++ {
    f[i]()
  }
}

func main() {
  CaptureLoop1()
  CaptureLoop2()
}

これを実行すると次のような結果が表示されます。

# go run main.go
CaptureLoop1
3
3
3
CaptureLoop2
0
1
2

CaptureLoop1forに変数iのインスタンスを直接参照してるので、iの最後の値である3が全て出力されます。しかし、CaptureLoop2iの値をvにコピーしてvインスタンスを参照していてvはforループで毎回新しく生成されるので、0~2まで全ての値が出力されることが確認できます。

依存性注入

Golangでは関数タイプを使って依存性注入(DI - Dependency Injection)を使うことができます。依存性注入とはビジネスロジックを内部に存在することではなく外部からロジックを注入されることを意味します。

package main

import "fmt"

type PrintConsole func(string)

func PrintHello(p PrintConsole) {
  p("Hello, World!")
}

func main() {
  PrintHello(func(msg string) {
    fmt.Println(msg)
  })
}

これを実行してみると次のような結果が表示されます。

# go run main.go
Hello, World!

PrintHelloPrintConsoleがどの役割(ビジネスロジック)をやるか分からないです。PrintHelloPrintConsoleを通じて依存性が注入されたと言えます。

完了

これで関数についてもっと深く見て見ました。それで関数を活用する色んな方法についても見て見ました。ここで紹介した方法を使ってもっと素敵なコードを作成して見てください。

私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!

Posts