Go 関数
Go において、関数は一等市民であり、関数は Go の最も基礎的な構成要素であり、Go の核心です。
宣言
関数の宣言形式は以下の通りです。
func 関数名 ([パラメータリスト]) [戻り値] {
関数本体
}関数を宣言するには 2 つの方法があります。1 つ目は func キーワードを使用して直接宣言する方法、2 つ目は var キーワードを使用して宣言する方法です。以下に示します。
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}関数シグネチャは関数名、パラメータリスト、戻り値で構成されます。以下に完全な例を示します。関数名は Sum で、int 型の 2 つのパラメータ a、b があり、戻り値の型は int です。
func Sum(a int, b int) int {
return a + b
}非常に重要な点として、Go の関数はオーバーロードをサポートしていません。以下のコードはコンパイルできません。
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}
func NewPerson(name string) *Person {
return &Person{Name: name}
}Go の理念は、シグネチャが異なる場合は完全に異なる 2 つの関数であるため、同じ名前を付けるべきではなく、関数のオーバーロードはコードを混乱させ、理解しにくくするというものです。この理念が正しいかどうかは意見が分かれるところですが、少なくとも Go では関数名だけでその機能がわかり、どのオーバーロードかを探す必要がありません。
パラメータ
Go のパラメータ名は名称を省略できますが、これは通常、インターフェースや関数型の宣言時に使用されます。ただし、可読性を高めるために、パラメータにはできるだけ名称を付けることを推奨します。
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}同じ型のパラメータの場合、型は 1 回だけ宣言すれば済みますが、条件としてそれらは隣接している必要があります。
func Log(format string, a1, a2 any) {
...
}可変長パラメータは 0 個以上の値を受け取れ、パラメータリストの末尾に宣言する必要があります。最も典型的な例は fmt.Printf 関数です。
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}值得一提的是、Go の関数パラメータは値渡しです。つまり、パラメータを渡す際に実参の値をコピーします。スライスや map を渡す際に大量のメモリがコピーされるのではないかと心配するかもしれませんが、これらのデータ構造は本質的にポインタであるため、その心配は無用です。
戻り値
以下は簡単な関数の戻り値の例です。Sum 関数は int 型の値を返します。
func Sum(a, b int) int {
return a + b
}関数が戻り値を持たない場合、void は不要で、戻り値を指定しないだけで済みます。
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}Go は複数の戻り値を持つことを許可しています。この場合、戻り値を括弧で囲む必要があります。
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0 を除数として使用することはできません")
}
return a / b, nil
}Go は名前付き戻り値もサポートしています。パラメータ名と重複することはできません。名前付き戻り値を使用する場合、return キーワードでどの値を返すかを指定する必要はありません。
func Sum(a, b int) (ans int) {
ans = a + b
return
}パラメータと同じように、同じ型の複数の名前付き戻り値がある場合、重複する型宣言を省略できます。
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}名前付き戻り値がどのように宣言されていても、常に return キーワード後の値が最優先されます。
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
// c, d は返されません
return a + b, a * b
}匿名関数
匿名関数とはシグネチャのない関数のことです。例えば以下の関数 func(a, b int) int は名称を持たないため、関数本体の直後に括弧を付けて呼び出す必要があります。
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}関数を呼び出す際、パラメータが関数型である場合、名称は重要ではなくなるため、匿名関数を直接渡すことができます。以下に示します。
type Person struct {
Name string
Age int
Salary float64
}
func main() {
people := []Person{
{Name: "Alice", Age: 25, Salary: 5000.0},
{Name: "Bob", Age: 30, Salary: 6000.0},
{Name: "Charlie", Age: 28, Salary: 5500.0},
}
slices.SortFunc(people, func(p1 Person, p2 Person) int {
if p1.Name > p2.Name {
return 1
} else if p1.Name < p2.Name {
return -1
}
return 0
})
}これはカスタムソート規則の例です。slices.SortFunc は 2 つのパラメータを受け取ります。1 つ目はスライス、2 つ目は比較関数です。再利用性を考慮しない場合、匿名関数を直接渡すことができます。
クロージャ
クロージャ(Closure)という概念は、一部の言語では Lambda 式とも呼ばれ、匿名関数とともに使用されます。クロージャ = 関数 + 環境参照です。以下の例をご覧ください。
func main() {
grow := Exp(2)
for i := range 10 {
fmt.Printf("2^%d=%d\n", i, grow())
}
}
func Exp(n int) func() int {
e := 1
return func() int {
temp := e
e *= n
return temp
}
}出力
2^0=1
2^1=2
2^2=4
2^3=8
2^4=16
2^5=32
2^6=64
2^7=128
2^8=256
2^9=512Exp 関数の戻り値は関数で、ここでは grow 関数と呼びます。これを 1 回呼び出すごとに、変数 e は指数関数的に増加します。grow 関数は Exp 関数の 2 つの変数 e と n を参照しています。これらは Exp 関数のスコープ内で生成され、通常の場合、Exp 関数の呼び出しが終了すると、これらの変数のメモリはスタックからポップされて解放されます。しかし、grow 関数がそれらを参照しているため、解放されず、ヒープにエスケープします。Exp 関数のライフサイクルが終了していても、変数 e と n のライフサイクルは終了しておらず、grow 関数内でこれら 2 つの変数を直接変更できます。grow 関数はクロージャ関数です。
クロージャを利用すると、フィボナッチ数列を生成する関数を非常に簡単に実装できます。コードは以下の通りです。
func main() {
// 10 個のフィボナッチ数
fib := Fib(10)
for n, next := fib(); next; n, next = fib() {
fmt.Println(n)
}
}
func Fib(n int) func() (int, bool) {
a, b, c := 1, 1, 2
i := 0
return func() (int, bool) {
if i >= n {
return 0, false
} else if i < 2 {
f := i
i++
return f, true
}
a, b = b, c
c = a + b
i++
return a, true
}
}出力は以下の通りです。
0
1
1
2
3
5
8
13
21
34遅延呼び出し
defer キーワードを使用すると、関数の呼び出しを遅延させることができます。関数が戻る前に、これらの defer で記述された関数は順次実行されます。以下の例をご覧ください。
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}出力
2
1defer は関数に戻る前に実行されるため、defer を使用して関数の戻り値を変更することもできます。
func main() {
fmt.Println(sum(3, 5))
}
func sum(a, b int) (s int) {
defer func() {
s -= 10
}()
s = a + b
return
}複数の defer で記述された関数がある場合、スタックのように後入れ先出しの順序で実行されます。
func main() {
fmt.Println(0)
Do()
}
func Do() {
defer fmt.Println(1)
fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
fmt.Println(5)
}0
2
5
4
3
1遅延呼び出しは通常、ファイルリソースの解放、ネットワーク接続の閉じなどの操作に使用されます。もう 1 つの使い方は panic のキャッチですが、これはエラー処理のセクションで取り上げる内容です。
ループ
明示的に禁止されているわけではありませんが、通常 for ループ内で defer を使用することは推奨されません。以下に示します。
func main() {
n := 5
for i := range n {
defer fmt.Println(i)
}
}出力は以下の通りです。
4
3
2
1
0このコードの結果は正しいですが、プロセスが正しくない可能性があります。Go では、defer を 1 つ作成するごとに、現在のゴルーチンにメモリスペースを申請する必要があります。上記の例が単純な for n ループではなく、複雑なデータ処理フローであり、外部リクエスト数が突然増加した場合、短時間に大量の defer が作成され、ループ回数が非常に多いか、回数が不確定な場合、メモリ使用量が突然急増する可能性があります。このような状況を一般にメモリリークと呼びます。
パラメータの事前計算
遅延呼び出しにはいくつかの直感に反する詳細があります。例えば以下の例です。
func main() {
defer fmt.Println(Fn1())
fmt.Println("3")
}
func Fn1() int {
fmt.Println("2")
return 1
}この落とし穴は非常に隠蔽的で、筆者も以前この問題で半日原因を特定できませんでした。出力が何かを予想してみてください。答えは以下の通りです。
2
3
1多くの人は以下の出力を予想するかもしれません。
3
2
1使用者の意図に従えば、fmt.Println(Fn1()) の部分は関数本体の実行が終了した後に実行されることを意図しているはずです。fmt.Println は実際に最後に実行されますが、Fn1() は予想外です。以下の例では状況がより明確です。
func main() {
var a, b int
a = 1
b = 2
defer fmt.Println(sum(a, b))
a = 3
b = 4
}
func sum(a, b int) int {
return a + b
}この出力は必ず 3 であり、7 ではありません。クロージャを使用して遅延呼び出しの代わりにした場合、結果はまた異なります。
func main() {
var a, b int
a = 1
b = 2
f := func() {
fmt.Println(sum(a, b))
}
a = 3
b = 4
f()
}クロージャの出力は 7 です。では、遅延呼び出しとクロージャを組み合わせるとどうなるでしょうか。
func main() {
var a, b int
a = 1
b = 2
defer func() {
fmt.Println(sum(a, b))
}()
a = 3
b = 4
}今回は正常で、7 が出力されます。次に変更して、クロージャをなくします。
func main() {
var a, b int
a = 1
b = 2
defer func(num int) {
fmt.Println(num)
}(sum(a, b))
a = 3
b = 4
}出力は再び 3 に戻ります。上記のいくつかの例を比較すると、このコード
defer fmt.Println(sum(a,b))は実際には
defer fmt.Println(3)と等価であることがわかります。Go は最後まで待ってから sum 関数を呼び出すのではなく、sum 関数は遅延呼び出しが実行される前に呼び出され、fmt.Println にパラメータとして渡されます。まとめると、defer が直接作用する関数に関して言えば、そのパラメータは事前計算されるということです。これが最初の例の奇妙な現象の原因です。特に遅延呼び出しで関数の戻り値をパラメータとして渡す場合には注意が必要です。
