Go/関数
< 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では
...
はスライスに後置するところが違います。
- 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
という可変長引数を受け取り、その中から最小値と最大値を探し出しています。min
とmax
の初期値をmath.MaxInt64
とmath.MinInt64
に設定することで、探索範囲を整数型の最大値と最小値の範囲に制限しています。for
ループを使用して、与えられた引数の値を1つずつ調べ、最小値と最大値を更新していきます。最終的に、更新された最小値と最大値をreturn
文で返します。 - 次に、
diff
関数では、2つの引数m
とn
を受け取り、それらの差を計算しています。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 | ...
は、リストされた要素のいずれかに制限されます。
- 任意の型(an arbitrary type)
- ジェネリック関数は、制約条件で許可されたすべての型でサポートされる操作のみを使用できます。
- 一般的な関数や型を使用するには、型の引数を渡す必要があります。
- 型推論では、一般的なケースでは、関数呼び出しの型引数を省略することができます。
数値型に限定したジェネリック関数
編集- 数値型に限定したジェネリック関数
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版に軍配が上がります。
脚註
編集- ^ Type Parameters Proposal 2021年9月30日閲覧。.
- ^ Go 1.18 Release Notes §Generics 2022年5月31日閲覧。.