「関数」
書籍:プログラミング言語Go
第5章 「関数」 の要点と思われる箇所を自分のメモ用にまとめました。
5.1 関数宣言
- 関数宣言は、名前、パラメータのリスト、省略可能な結果のリスト、本体から構成される。
.go
func name(parameter-list) (result-list) {
body
}
- パラメータ同様結果も名前を付けることができ、名前付けをしたパラメータはその型のゼロ値のローカル変数を宣言することになる。
- 結果リストを持つ関数は、原則 (panic の呼び出しで終わるか、break のない無限 for ループで終わるのでなければ。) return 文で終わらないといけない。
- パラメータにはブランク識別子 _ を使うこともでき、パラメータが使われないことを強調するために使える。
.go
func first(x int, _ int) { return x }
- 関数のシグニチャはパラメータの型の列と結果の型の列が同じであれば同じ。パラメータ名などは関数の型に影響しない。
- Goにはデフォルトパラメータという概念や、名前で引数を指定する方法はない。
- 引数はコピーを受け取る値渡し。しかし、ポインタ、スライス、マップ、関数、チャネルなどの何らかの種類の参照が引数に含まれていれば、関数内処理が呼び出し元の引数に影響を与えうる。
- 本体のない関数宣言を見かけた場合は、Go以外の言語で実装されている関数を示し、関数のシグニチャを定義している。
5.2 再帰
- 関数は自分自身の関数を自分自身の関数内で呼び出す再帰呼び出しが可能。ツリー操作などをする場合に便利。
- outline の再起呼び出しでは、呼び出し先は stack のコピーを受け取る。呼び出し先が stack に要素を追加しても呼び出し元に見える要素は修正しない。呼び出し元のstackは呼び出す前と変わらない。
.go
func outline(stack []string, n *html.Node) {
if n.Type == html.ElementNode {
stack = append(stack, n.Data) // push tag
fmt.Println(stack)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
outline(stack, c)
}
}
- 多くのプログラミング言語は固定長の関数呼び出しスタック(大きさ64KB-2MBまでが普通)を使っている。再起呼び出しの深さに制限を課すので大きなデータ構造の再起によるスタックオーバーフローに注意が必要だが、Goは可変長スタックを使っているためスタックオーバーフローの心配はない。
5.3 複数戻り値
- 関数は複数戻り値を返すことができる。よくあるのが望まれた計算結果と、エラーもしくは処理が成功したかどうかのboolを返す関数。
- 多値呼出 (複数戻り値の関数呼出) は複数のパラメータを持つ関数呼出にも使える。デバッグに便利。製品コードではめったに使わない。
.go
func hoge() string, bool {
...
}
fmt.Println(hoge())
val, ok := hoge()
fmt.Println(val, ok) // 上記の Println と同じ出力が得られる。
- 多値関数の結果は名前が重要になる。戻り値にも名前を付けると戻り値の内容が理解しやすくなる。
.go
func Size(rect image.Rectangle) (width, height int) {
- 慣習的に最後の bool は結果の成功を示すので名前は不要。error も多くの場合何の説明も必要としない。
- 名前付きの結果を持つ関数内では return 文のオペランドは省略可能。空リターンと呼ばれ、名前付き結果の変数のそれぞれを正しい順序で返す短い表記方法。
.go
func CountWordsAndImage(url string) (words, images int, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
...
- 空リターンはコードの重複を減らすがコードの理解を容易にするわけではない。例えば戻りがエラーのケースなのか正常なケースなのか分かりにくくなりうる。空リターンは控えめにしたほうが良い。
5.4 エラー
- エラーはパッケージAPIやアプリケーションのUIの重要な一部。失敗は予期される振る舞いの一つ。
- 予期される振る舞いが失敗の場合、慣習的に関数の戻り値の最後が失敗を意味する値となる。
- エラー時の error 以外の戻り値に意味がある場合はドキュメントに書くことが重要。
- Goの例外機構は、バグを示す本当に予期されていないエラーを報告するためのもの。頑強なプログラムを構築するためのルーチンで予期されるエラーには使われない。
- 予期せぬバグのみ例外とする理由は、例外処理は制御フローとエラー記述をもつれさせる傾向にあり、望まぬ結果をもたらしやすく、例外が報告された際に問題を理解しにくくさせやすいため。
- Goプログラムはエラーに対処するために if や return などの普通の制御フローを使う。
5.4.1 エラー処理戦略
- **自作関数内でエラーを返す際は、重要な情報を補完する。**findLinks の Parse エラーでは、パーサでエラーが起きたこと、パースドキュメントのURLを補完している。
.go
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil,fmt.Errorf("parsing %s as HTML: %v", url err)
}
- 究極的にエラーがmainで処理される場合、全体的な失敗に対する根本問題から明確な因果の連鎖を提供すべきである。
- 一時的あるいは予想できない問題を表すエラーに対しては失敗した操作を再び試みることに意味があるケースがある。その場合、諦める前に時間を置いて再試行するなどが意味を持つ。
- 処理を進めるのが不可能な状態になった場合、エラー表示しプログラムを停止する。ただし、mainパッケージ内でのみ行うべきでライブラリなどはプログラムを停止してはいけない。
- 処理を継続することに意味があるのなら、エラーを記録し、制限された機能で処理を続ける。
- エラー処理を忘れてもそのプログラム以外の別の手段でリカバリーできるような場合にはエラーを無視して良い場合もある。意図的にエラーを無視する場合はドキュメントに明記すること。
5.4.2 ファイルの終わり (EOF: End of File)
- ioパッケージではファイルの終わりの状態により発生した読み込みの失敗は、io.EOF として区別されたエラーで報告される。
.go
in := bufio.NewReader(os.Stdin)
for {
r, _, err := in.ReadRune()
if err == io.EOF {
break // 読み込みを終了
}
if err != nil {
...
5.5 関数値
- Goでは関数を変数に代入可能 (関数はファーストクラス値)。関数値を代入した変数で関数を呼び出すこともできる。関数型のゼロ値はnil。
.go
func square(n int) int { return n * n }
f := squre
fmt.Println(f(3)) // 9
- 引数の型や戻り値の型が同じでないと関数値の変数に再度関数値を代入できない。
.go
func square(n int) int { return n * n }
func product(m, n int) int { return m * n }
f := squre
f = product // エラー: func(int, int) int を func(int) int へ代入できない。
5.6 無名関数
- 関数リテラルは関数宣言のように書くが、func予約語の後に名前がない。関数リテラルは式であり、その値は無名関数と呼ばれる。
strings.Map(func(f, rune) rune { return r + 1 }, "HAL-9000") - 無名関数はレキシカルな環境の全体へアクセスできる(無名関数を囲うスコープ内の変数にアクセスできる)。
.go
func squares() func() int { // (func() int) が戻りの関数。
var x int
return func() { // squares() 関数のローカル変数 x にアクセスできる。
x++
return x * x
}
}
func main() {
f := squares()
fmt.Println(f()) // 1
fmt.Println(f()) // 4
fmt.Println(f()) // 9
- squares の例は関数値が単なるコードではなく状態を持つことを示す。
- 無名内部関数はそれを囲む関数のローカル変数にアクセス可能。このような関数値はクロージャと呼ばれる技法で実装されており、Goプログラマの多くはその用語を関数値に対して使っている。
- 無名関数が再起を必要とする場合、最初に宣言する必要がある。
.go
var visitAll func(items []string)
visitAll = func(items []string) {
for _, item := range items {
if !seen[item] {
seen[item] = true
visitAll(m[item])
order = append(order, item)
}
}
}
5.7 可変個引数関数
- 可変個引数関数を宣言するには最後のパラメータの方の前に省略記号 "..." を付ける。
.go
func sum(vals ...int) {
total := 0
for _, val := range vals {
total += val
}
return total
}
- 可変個引数関数に配列を渡すことも可能。関数呼び出し時に配列の後ろに...をつける。
order = append(order, array...) - 可変個引数として interface{}型を使うと、最後の引数に対してすべての型を受け付けることができることを意味する。
5.8 遅延関数呼び出し
- defer 文は、関数やメソッド呼び出しの前に予約語 defer を付けたもので、予約語 defer を付けた関数、メソッドの呼び出しは、defer 文を含む関数が完了するまで遅延される。
- return を実行したり関数の最後に到達したりという正常な完了ではないパニックなどの異常な完了でも defer は正常に動作する。
- 遅延関数呼び出しは、遅延された順序の逆順に実行される。
- defer文はオープンとクローズ、接続と切断、ロックとアンロックのように一対になる操作で使われることが多い。デバッグでのログだし(関数に入った、出たを対にする場合)にも使う。
.go
func hoge() {
defer trace("hoge")() // traceの戻りはfuncなので、最後に()を忘れないこと。
.... // hoge() の関数本体処理
}
func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() { log.Printf("exit %s (%s)", msg, time.Since(start)) }
}
- クロージャと組み合わせると関数の処理結果を表示することなどもできる。
.go
func double(x int) (result int) { // 戻り値に名前を付けていることに注意!!
defer func() { fmt.Printf("double(%d) = %d\n", x, result) }()
return x + x
}
_ = double(4) // defer により、"double(4) = 8" が表示される。
- ループ内の defer 文は、ループが記載されている関数の終わりまで遅延関数が実行されないので注意。解決方法はループ本体の処理を関数化する。
5.9 パニック
- Goでは境界外への配列アクセスやnilポインタによる参照などのコンパイル時に検出できない誤りがあった場合、実行時にパニックになる。
- 典型的なパニックでは、通常実行は停止し、ごルーチン内でのすべての遅延関数呼び出しが行われ、ログメッセージを表示しクラッシュする。
- パニック時のログは、たいていパニック値とパニック時に動作していた関数呼び出しのスタックを指名す個々のごルーチンのスタックトレースが含まれる。
- 多言語の例外と異なり、パニックはプログラムをクラッシュさせるため、「予期されない」重大なエラーに対して使われる。
- 「予期される」エラー (誤った入力や設定、失敗したI/Oから発生する類のエラー) は error を使いきちんと処理されるべきで、パニックにすべきではない。
- パ肉が発生した場合、すべての遅延された関数は、スタックの最上位の関数の遅延された関数から始まって main 関数まで逆順に実行される。
5.10 リカバー
- recover呼び出しをするとパニックになっていた関数は止まった場所から続けることはできないが、正常にリターンする。非パニック時の recover 呼び出しは何も影響なく nil が返る。
.go
func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ... パーサ処理
}
- パニックからの無条件な回復は推奨されない。以下の理由から。
- パニック後のパッケージの変数の状態はほとんど定義されていなかったり文書化されていない。
- データ構造への重要な更新が不完全だったり、ファイルやネットワーク接続が開かれ閉じられていない、ロックが獲得され解放されていないなどありうる。
- クラッシュを仮にログファイル内の一行で置き換えた場合など、無条件な回復はバグに気づかなくさせる原因となる。
- 特にほかのパッケージのパニックからの回復は試みるべきではない。公開APIは失敗を error として報告すべき。
練習問題解答例