コンテンツにスキップ

Go/関数

出典: フリー教科書『ウィキブックス(Wikibooks)』
< Go

関数

[編集]

関数定義

[編集]
関数定義
func 関数名 ( [仮引数名 仮引数型{, 仮引数名 仮引数型}] ) ( 戻値の型{, 戻値の型} ) {
    /* 処理内容 */
    [return 戻値{, 戻値}]
}

のような構文です。

関数の例
package main

import "fmt"

func main() {
	fmt.Println("in main")
	subr()
}

func subr() {
	fmt.Println("in subr")
}
実行結果
in main
in subr
解説
5行目が main 関数の定義でプログラムのエントリーポイントになります。
7行目で subr 関数を呼び出しています。
10行目で subr 関数を定義しています。

引数も返数もない関数呼び出しですが、注目してもらいたいのは subr の呼び出しが subr の定義に先行していて前方参照になっているにも関わらず、問題なくコンパイル・実行できる点です。 Goでは、関数だけでなく変数や型もパッケージスコープの場合は前方参照は処理系により解決され、循環参照は診断されます。

ローカル変数

[編集]

関数の仮引数は、関数が終了するまでがスコープです。 関数の中で宣言された変数は、関数が終了するまでがスコープのローカル変数です。 if文、switch文、for文などの制御構造の第一項が簡単な宣言であった場合、そこで宣言された変数のスコープは制御構造が終わるまでです。

コード例
package main

import "fmt"

var a = "ABC"

func function1() {
	a := 0
	fmt.Println("a@function1 =", a)
}

func function2() {
	fmt.Println("a@function2 =", a)
}

func main() {
	a := 1
	fmt.Println("a =", a)
	function1()
	function2()
	fmt.Println("a' =", a)
	sum := 0
	for i := 0; i <= 10; i++ {
		sum += i
	}
	fmt.Println("sum =", sum)
}
実行結果
a = 1
a@function1 = 0
a@function2 = ABC
a' = 1
sum = 55
main と function1 では、グローバル変数 a は同名のローカル変数 a にシャドーイングされます。
function2 では、(シャドーイングされないので)グローバル変数 a が表示されています。

可変長仮引数

[編集]

Goは、関数の可変長仮引数をサポ-トしています。

コード例
package main

import "fmt"

func sum(s ...int) (n int) {
	for _, v := range s {
		n += v
	}
	return
}

func main() {
	fmt.Println(sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
	v := []int{1, 2, 4, 8, 16, 32, 64, 128, 256}
	fmt.Println(sum(v...))
}
実行結果
55 
511
解説:可変仮引数を受取る関数の定義
func sum(s ...int) (n int) {
	for _, v := range s {
		n += v
	}
	return
}
任意個のintの仮引数を受け取り、intを返す関数を定義しています。
戻値には n という名前を与えており、n の初期値はintのゼロ値の 0 になります。
尚、9行目の return は冗長に見えますが、戻値に名前をつけた場合必須です。
解説:可変仮引数の関数の呼出し
func main() {
	fmt.Println(sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
	v := []int{1, 2, 4, 8, 16, 32, 64, 128, 256}
	fmt.Println(sum(v...))
}
可変仮引数の関数の呼出しは、実引数の数が可変であることを除けば、通常の関数とかわりありません。
この例では、たまたま等差であったり法則がありますが、もちろん値は任意です。
15行目の fmt.Println(sum(v...)) は、スライスを実引数に展開する構文です。
JavaScriptスプレッド構文と似ていますが、Goでは ... はスライスに後置するところが違います。
Goではキーワードや区切子を最初にするため、同じ区切子が文脈によって意味を変えますが ... は上の例のほか、配列定義 ary := [...]int{2,3,5,7,11} でも使われます。

多値

[編集]

Goの関数は2つ以上の戻値を同時に返せます。

コード例
package main

import "fmt"

func signabs(n int) (int, int) {
	switch {
	case n < 0:
		return -1, -n
	case n > 0:
		return 1, n
	case n == 0:
		return 0, 0
	default:
		panic("what to do")
	}
}

func main() {
	s, n := signabs(123)
	fmt.Println("signabs(123)", s, n)
	s, n = signabs(-321)
	fmt.Println("signabs(-321)", s, n)
	s, n = signabs(0)
	fmt.Println("signabs(0)", s, n)
	s, _ = signabs(99)
	fmt.Println("s, _ = signabs(99):s", s)
	_, n = signabs(-134567)
	fmt.Println("_, n = signabs(-134567):n", n)
	fmt.Println(signabs(-12))
}
実行結果
signabs(123) 1 123
signabs(-321) -1 321
signabs(0) 0 0
s, _ = signabs(99):s 1
_, n = signabs(-134567):n 134567
-1 12

多値で返す関数を関数の実引数にする

[編集]

Goの関数は、多値で返す関数を別の関数の実引数に出来ます。

多値で返す関数を関数の実引数にする例
package main

import (
	"fmt"
	"math" // For math.MaxInt64 and math.MinInt64
)

func minmax(s ...int) (int, int) {
	min, max := math.MaxInt64, math.MinInt64
	for _, v := range s {
		if v < min {
			min = v
		} else if v > max {
			max = v
		}
	}
	return min, max
}

func diff(m, n int) int {
	return n - m
}

func main() {
	fmt.Println(minmax(3, 12, 4, 67, 99, -12))
	fmt.Println(diff(minmax(3, 12, 4, 67, 99, -12)))
}
実行結果
-12 99
111
解説
このコードは、可変長引数を使用して、与えられた整数列の最小値と最大値を求める minmax 関数と、最小値と最大値の差を求める diff 関数を定義し、それらを使用している main 関数を定義しています。
まず、 minmax 関数では、 s ...int という可変長引数を受け取り、その中から最小値と最大値を探し出しています。 minmax の初期値を math.MaxInt64math.MinInt64 に設定することで、探索範囲を整数型の最大値と最小値の範囲に制限しています。 for ループを使用して、与えられた引数の値を1つずつ調べ、最小値と最大値を更新していきます。最終的に、更新された最小値と最大値を return 文で返します。
次に、 diff 関数では、2つの引数 mn を受け取り、それらの差を計算しています。 return 文で n - m を返すことで、差を返しています。
最後に、 main 関数では、 minmax 関数を使用して最小値と最大値を求め、 diff 関数を使用してその差を求めています。 fmt.Println を使用して、それぞれの値を出力しています。

ジェネリック関数

[編集]

Go 1.18 (2022年3月リリース)で、ジェネリック関数が追加されました[1][2]

型パラメーター

[編集]
  • 関数は、角括弧を使っているものの、それ以外は通常のパラメータリストのように見える追加の型パラメータリストを持つことができます。func F[T any](p T) { .... }
  • これらの型パラメータは、通常のパラメータや関数本体で使用することができます。
  • 型は、型パラメータリストを持つこともできます。type M[T any] []T
  • 各型パラメータは、各通常のパラメータが型を持つように、型制約を持ちます。func F[T Constraint](p T) { .... } .
  • 型制約はインターフェース型です。
  • 新しい宣言済みの名前 any は、任意の型を許可する型制約です。
  • 型制約として使用されるインターフェイス型は、制約を満たす型引数のセットを制限するために、追加の要素を埋め込むことができます。
    • 任意の型(an arbitrary type) T は、その型に制限します。
    • 近似要素(an approximation element) ~T は、基礎となる型が T であるすべての型に制限する。
    • 共有要素(a union element) T1 | T2 | ... は、リストされた要素のいずれかに制限されます。
  • ジェネリック関数は、制約条件で許可されたすべての型でサポートされる操作のみを使用できます。
  • 一般的な関数や型を使用するには、型の引数を渡す必要があります。
  • 型推論では、一般的なケースでは、関数呼び出しの型引数を省略することができます。

数値型に限定したジェネリック関数

[編集]
数値型に限定したジェネリック関数
package main

import "fmt"

type numeric interface {
	int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}

func sum[T numeric](s ...T) (n T) {
	for _, v := range s {
		n += v
	}
	return
}

func main() {
	fmt.Println(sum(1., 1.4142135623730951, 1.7320508075688772, 2., 2.23606797749979, 2.449489742783178, 2.6457513110645907, 2.8284271247461903, 3., 3.1622776601683795))
	fmt.Println(sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
}
実行結果
22.4682781862041 
55
#可変長仮引数の例を、intだけでなく type numeric interfaceで定義した型の集合に拡張しました。

any を使った方法

[編集]
any を使った方法
package main

import "fmt"

func Print[T any](s ...T) {
	for _, v := range s {
		fmt.Println(v)
	}
}

func main() {
	Print[float64](1, 1.4142135623730951, 1.7320508075688772, 2)
	Print[int](2, 3, 5)
	Print[byte]([]byte("ABC")...)
}
実行結果
1
1.4142135623730951
1.7320508075688772
2
2
3
5
65
66
67
新しいキーワード any を使うとすべての型に出来ますが、中で呼び出す関数や演算子がすべての型を受け付ける必要があります。
この例では、fmt.Println(v)がすべての型に対応しているので any を使うことが出来ます。

interface{}も any に合致する

[編集]
interface{}も any に合致する
package main

import "fmt"

type Stack[T any] []T

func (s *Stack[T]) Push(v T) {
	*s = append(*s, v)
}

func (s *Stack[T]) Pop() (value T, ok bool) {
	if len(*s) == 0 {
		return
	}
	value = (*s)[len(*s)-1]
	*s = (*s)[:len(*s)-1]
	ok = true
	return
}

func main() {
	s := Stack[interface{}]{}
	s.Push("abc")
	s.Push(123)
	s.Push(struct {
		s string
		i int
	}{s: "a", i: 1})
	fmt.Println(s)
	for v, ok := s.Pop(); ok; v, ok = s.Pop() {
		fmt.Println(v)
	}
}
実行結果
[abc 123 {a 1}]
{a 1}
123
abc
解説
	s := Stack[interface{}]{}
interface{} も any に合致するので、異種スタックを実現できます。

Map, Reduce と Filter

[編集]
Map, Reduce とFilter
package main

import "fmt"

func Map[T1, T2 any](s []T1, f func(T1) T2) []T2 {
	result := make([]T2, len(s))
	for i, v := range s {
		result[i] = f(v)
	}
	return result
}

func Reduce[T1, T2 any](s []T1, init T2, f func(T2, T1) T2) T2 {
	result := init
	for _, v := range s {
		result = f(result, v)
	}
	return result
}

func Filter[T any](s []T, f func(T) bool) []T {
	result := make([]T, 0)
	for _, v := range s {
		if f(v) {
			result = append(result, v)
		}
	}
	return result
}

func main() {
	fmt.Println(Map([]int{2, 4, 5, 7}, func(i int) float64 { return float64(i * i) }))
	fmt.Println(Reduce([]int{2, 4, 5, 7}, 0, func(i, j int) int { return i + j }))
	fmt.Println(Filter([]int{2, 4, 5, 7}, func(i int) bool { return i > 2 }))
}
実行結果
[4 16 25 49]
18
[4 5 7]
解説
func Map[T1, T2 any](s []T1, f func(T1) T2) []T2 {
2つ以上の型パタメータを取ったジェネリック関数を定義できます。
func Reduce[T1, T2 any](s []T1, init T2, f func(T2, T1) T2) T2 {
func Filter[T any](s []T, f func(T) bool) []T {

順列・組合わせ

[編集]

順列

[編集]
順列
package main

import "fmt"

var (
	ErrNilSlice = fmt.Errorf("slice is nil")
)

func Permutation[T any](s []T, n int) (result [][]T) {
	if s == nil {
		panic(ErrNilSlice)
	}
	if n == 1 {
		for _, v := range s {
			result = append(result, []T{v})
		}
		return
	}
	for i, v := range s {
		sf := []T{}
		for j, w := range s {
			if j != i {
				sf = append(sf, w)
			}
		}
		for _, w := range Permutation(sf, n-1) {
			result = append(result, append([]T{v}, w...))
		}
	}
	return
}

func main() {
	fmt.Println(Permutation([]int{1, 2, 3}, 1))
	fmt.Println(Permutation([]int{0, 1, 2}, 2))
	fmt.Println(Permutation([]string{"abc", "def", "xyz"}, 3))
	fmt.Println(Permutation[int](nil, 2))
}
実行結果
[[1] [2] [3]]
[[0 1] [0 2] [1 0] [1 2] [2 0] [2 1]]
[[abc def xyz] [abc xyz def] [def abc xyz] [def xyz abc] [xyz abc def] [xyz def abc]]
panic: slice is nil
解説
再帰的な関数呼び出しを使って順列を求めるプログラムです。
スライスの要素型は型パラメーター化しているので、このコードで任意の型のスライスに対応できます。

組合わせ

[編集]
組合わせ
package main

import "fmt"

var (
	ErrNilSlice = fmt.Errorf("slice is nil")
)

func Combination[T any](s []T, n int) (result [][]T) {
	if s == nil {
		panic(ErrNilSlice)
	}
	if n == 1 {
		for _, v := range s {
			result = append(result, []T{v})
		}
		return
	}
	for i, v := range s {
		for _, w := range Combination(s[i+1:], n-1) {
			result = append(result, append([]T{v}, w...))
		}
	}
	return
}

func main() {
	fmt.Println(Combination([]int{1, 2, 3}, 1))
	fmt.Println(Combination([]int{0, 1, 2}, 2))
	fmt.Println(Combination([]string{"abc", "def", "xyz"}, 3))
	fmt.Println(Combination[int](nil, 2))
}
実行結果
[[1] [2] [3]]
[[0 1] [0 2] [1 2]]
[[abc def xyz]]
panic: slice is nil
解説
再帰的な関数呼び出しを使って組合わせを求めるプログラムです。
スライスの要素型は型パラメーター化しているので、このコードで任意の型のスライスに対応できます。
この2つの関数は、JavaScriptで書かれた順列・組合わせのコードを Go に移植したものですが、JavaScriptでは組込みオブジェクトArrayにメソッドを追加する形で実装し内部でもメソッドチェーンを多用しているのに対し、Go版では読み間違えのないようなforループで実装しました。単純にロジックの追いやすさの比較では、Go版に軍配が上がります。

脚註

[編集]
  1. ^ Type Parameters Proposal. https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md 2021年9月30日閲覧。. 
  2. ^ Go 1.18 Release Notes §Generics. https://tip.golang.org/doc/go1.18#generics 2022年5月31日閲覧。.