2.3 フローと関数

この節ではGoの中のフロー制御と関数操作についてご紹介します。

フロー制御

フロー制御はプログラム言語の中の最も偉大な発明です。なぜならこれがあるだけで、あなたはとても簡単なフローの記述でとても複雑なロジックを表現できるからです。Goではフロー制御は3つの部分から成ります:条件判断、ループ制御及び無条件ジャンプです。

if

ifはあらゆるプログラミング言語の中で最もよく見かけるものかもしれません。この文法は大雑把に言えば:もし条件を満足しなければ何々を行い、そうでなければまたもう一つ別のことをやるということです。

Goの中ではif分岐の文法の中は括弧で括る必要はありません。以下のコードをご覧ください。

if x > 10 {
    fmt.Println("x is greater than 10")
} else {
    fmt.Println("x is less than 10")
}

Goのifはすごいことに、条件分岐の中で変数を宣言できます。この変数のスコープはこの条件ロジックブロック内のみ存在し、他の場所では作用しません。以下に示します

// 取得値xを計算し、xの大きさを返します。10以上かどうかを判断します。
if x := computedValue(); x > 10 {
    fmt.Println("x is greater than 10")
} else {
    fmt.Println("x is less than 10")
}

//ここではもしこのようにコールしてしまうとコンパイルエラーとなります。xは条件の中の変数だからです。
fmt.Println(x)

この条件の時は以下のようになります:

if integer == 3 {
    fmt.Println("The integer is equal to 3")
} else if integer < 3 {
    fmt.Println("The integer is less than 3")
} else {
    fmt.Println("The integer is greater than 3")
}

goto

Goにはgoto句があります- - ぜひ賢く使ってください。gotoは必ず事前に関数内で定義したタグにジャンプします。例えばこのようなループがあったと仮定します:

func myFunc() {
    i := 0
Here:   //この行の最初の単語はコロンを最後に持ってくることでタグとなります。
    println(i)
    i++
    goto Here   //Hereにジャンプします。
}

タグの名前は大文字小文字を区別します。

for

Goにで最も強力なロジックコントロールといえば、forです。これはループでデータを読むのに使えます。whileでロジックをコントロールしても構いません。イテレーション操作も行えます。文法は以下の通りです:

for expression1; expression2; expression3 {
    //...
}

expression1expression2expression3はどれも式です。この中でexpression1expression3は変数宣言または関数のコールの戻り値のようなものです。expression2は条件判断に用いられます。expression1はループの開始前にコールされます。expression3は毎回ループする際の終了時にコールされます。

だらだら喋るよりも例を見たほうが早いでしょう。以下に例を示します:

package main
import "fmt"

func main(){
    sum := 0;
    for index:=0; index < 10 ; index++ {
        sum += index
    }
    fmt.Println("sum is equal to ", sum)
}
// 出力:sum is equal to 45

時々複数の代入操作を行いたい時があります。Goのなかには,という演算子はないので、平行して代入することができます。i, j = i+1, j-1

時々expression1expression3を省略します:

sum := 1
for ; sum < 1000;  {
    sum += sum
}

この中で;は省略することができます。ですので下のようなコードになります。どこかで見た覚えはありませんか?そう、これはwhileの機能です。

sum := 1
for sum < 1000 {
    sum += sum
}

ループの中ではbreakcontinueという2つのキーとなる操作があります。break操作は現在のループから抜け出します。continueは次のループに飛び越えます。ネストが深い場合、breakはタグと組み合わせて使用することができます。つまり、タグが指定する位置までジャンプすることになります。詳細は以下の例をご覧ください。

for index := 10; index>0; index-- {
    if index == 5{
        break // またはcontinue
    }
    fmt.Println(index)
}
// breakであれば10、9、8、7、6が出力されます。
// continueの場合は10、9、8、7、6、4、3、2、1が出力されます。

breakcontinueはタグを添えることができます。複数ネストしたループで外側のループからジャンプする際に使用されます。

forrangeと組み合わせてarrayslicemapstringのデータを読み込むことができます:

for k,v:=range map {
    fmt.Println("map's key:",k)
    fmt.Println("map's val:",v)
}

Goは"複数の戻り値"をサポートしていますが、"宣言して使用されていない"変数に対してコンパイラはエラーを出力します。このような状況では_を使って必要のない戻り値を捨てる事ができます。 例えば

for _, v := range map{
    fmt.Println("map's val:", v)
}

switch

時々たくさんのif-elseを書くことでロジック処理を行いたくなるかもしれません。コードは非常に醜く冗長になります。またメンテナンスも容易ではなくなるので、switchを使って解決することができます。この文法は以下のようなものです

switch sExpr {
case expr1:
    some instructions
case expr2:
    some other instructions
case expr3:
    some other instructions
default:
    other code
}

sExprexpr1expr2expr3の型は一致させる必要があります。Goのswitchは非常に使い勝手がよく、式は必ずしも定数や整数である必要はありません。実行のプロセスは上から下まで、マッチする項目が見つかるまで行われます。もしswitchに式がなければ、trueとマッチします。

i := 10
switch i {
case 1:
    fmt.Println("i is equal to 1")
case 2, 3, 4:
    fmt.Println("i is equal to 2, 3 or 4")
case 10:
    fmt.Println("i is equal to 10")
default:
    fmt.Println("All I know is that i is an integer")
}

5行目で、いくつもの値をcaseの中に集めています。また同時に、Goのswitchはデフォルトでcaseの最後にbreakがあることになっているので、マッチに成功した後は他のcaseが実行されることはなく、switch全体から抜け出します。ただし、fallthroughを使用することであとに続くcaseコードを強制的に実行させることができます。

integer := 6
switch integer {
    case 4:
    fmt.Println("The integer was <= 4")
    fallthrough
    case 5:
    fmt.Println("The integer was <= 5")
    fallthrough
    case 6:
    fmt.Println("The integer was <= 6")
    fallthrough
    case 7:
    fmt.Println("The integer was <= 7")
    fallthrough
    case 8:
    fmt.Println("The integer was <= 8")
    fallthrough
    default:
    fmt.Println("default case")
}

上のプログラムは以下のように出力します

The integer was <= 6
The integer was <= 7
The integer was <= 8
default case

関数

関数はGoの中心的な設計です。キーワードfuncによって宣言します。形式は以下の通り:

func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
    //ここはロジック処理のコードです。
    //複数の値を戻り値とします。
    return value1, value2
}

上のコードから次のようなことが分かります

  • キーワードfuncfuncNameという名前の関数を宣言します。
  • 関数はひとつまたは複数の引数をとることができ、各引数の後には型が続きます。,をデリミタとします。
  • 関数は複数の戻り値を持ってかまいません。
  • 上の戻り値は2つの変数output1output2であると宣言されています。もしあなたが宣言したくないというのであればそれでもかみません。直接2つの型です。
  • もしひとつの戻り値しか存在せず、また戻り値の変数が宣言されていなかった場合、戻り値の括弧を省略することができます。
  • もし戻り値が無ければ、最後の戻り値の情報も省略することができます。
  • もし戻り値があれば、関数の中でreturn文を追加する必要があります。

以下では実際に関数の例を応用しています(Maxの値を計算します)

package main
import "fmt"

// a、bの中から最大値を返します。
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func main() {
    x := 3
    y := 4
    z := 5

    max_xy := max(x, y) //関数max(x, y)をコール
    max_xz := max(x, z) //関数max(x, z)をコール

    fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
    fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
    fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 直接コールしてもかまいません。
}

上ではmax関数に2つの引数があることがわかります。この型はどれもintです。第一引数の型は省略することができます(つまり、a,b int,でありa int, b intではありません)、デフォルトは直近の型です。2つ以上の同じ型の変数または戻り値も同じです。同時に戻り値がひとつであることに注意してください。これは省略記法です。

複数の戻り値

Go言語はCに比べ先進的な特徴を持っています。関数が複数の戻り値を持てるのもその一つです。

コードの例を見てみましょう

package main
import "fmt"

//A+B と A*B を返します
func SumAndProduct(A, B int) (int, int) {
    return A+B, A*B
}

func main() {
    x := 3
    y := 4

    xPLUSy, xTIMESy := SumAndProduct(x, y)

    fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
    fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}

上の例では直接2つの引数を返しました。当然引数を返す変数に命名してもかまいません。この例では2つの型のみ使っていますが、下のように定義することもできます。値が返る際は変数名を付けなくてかまいません。なぜなら関数の中で直接初期化されているからです。しかしもしあなたの関数がエクスポートされるのであれば(大文字からはじまります)オフィシャルではなるべく戻り値に名前をつけるようお勧めしています。なぜなら名前のわからない戻り値はコードをより簡潔なものにしますが、生成されるドキュメントの可読性がひどくなるからです。

func SumAndProduct(A, B int) (add int, Multiplied int) {
    add = A+B
    Multiplied = A*B
    return
}

可変長引数

Goの関数は可変長引数をサポートしています。可変長引数を受け付ける関数は不特定多数の引数があります。これを実現するために、関数が可変長引数を受け取れるよう定義する必要があります:

func myfunc(arg ...int) {}

arg ...intはGoにこの関数が不特定多数の引数を受け付けることを伝えます。ご注意ください。この引数の型はすべてintです。関数ブロックの中で変数argintsliceとなります。

for _, n := range arg {
    fmt.Printf("And the number is: %d\n", n)
}

値渡しと参照渡し

引数をコールされる関数の中に渡すとき、実際にはこの値のコピーが渡されます。コールされる関数の中で引数に修正をくわえても、関数をコールした実引き数には何の変化もありません。数値の変化はコピーの上で行われるだけだからです。

この内容を検証するために、ひとつ例を見てみましょう

package main
import "fmt"

//引数+1を行う、簡単な関数
func add1(a int) int {
    a = a+1 // aの値を変更します。
    return a //新しい値を返します。
}

func main() {
    x := 3

    fmt.Println("x = ", x)  // "x = 3"と出力するはずです。

    x1 := add1(x)  //add1(x) をコールします。

    fmt.Println("x+1 = ", x1) // "x+1 = 4" と出力するはずです。
    fmt.Println("x = ", x)    // "x = 3" と出力するはずです。
}

どうです?add1関数をコールし、add1のなかでa = a+1の操作を実行したとしても、上述のx変数には何の変化も発生しません。

理由はとても簡単です:add1がコールされた際、add1が受け取る引数はxそのものではなく、xのコピーだからです。

もし本当にこのxそのものを渡したくなったらどうするの?と疑問に思うかもしれません。

この場合いわゆるポインタにまで話がつながります。我々は変数がメモリの中のある特定の位置に存在していることを知っています。変数を修正するということはとどのつまり変数のアドレスにあるメモリを修正していることになります。add1関数がx変数のアドレスを知ってさえいれば、x変数の値を変更することが可能です。そのため、我々はxの存在するアドレスである&xを関数に渡し、関数の変数の型をintからポインタ変数である*intに変更します。これで関数の中でxの値を変更することができるようになりました。この時関数は依然としてコピーにより引数を受け渡しますが、コピーしているのはポインタになります。以下の例をご覧ください。

package main
import "fmt"

//引数に+1を行う簡単な関数
func add1(a *int) int { // ご注意ください。
    *a = *a+1 // aの値を修正しています。
    return *a // 新しい値を返します。
}

func main() {
    x := 3

    fmt.Println("x = ", x)  // "x = 3"と出力するはずです。

    x1 := add1(&x)  // add1(&x) をコールしてxのアドレスを渡します。

    fmt.Println("x+1 = ", x1) // "x+1 = 4"を出力するはずです。
    fmt.Println("x = ", x)    // "x = 4"を出力するはずです。
}

このようにxを修正するという目的に到達しました。では、ポインタを渡す長所はなんなのでしょうか?

  • ポインタを渡すことで複数の関数が同じオブジェクトに対して操作を行うことができます。
  • ポインタ渡しは比較的軽いです(8バイト)、ただのメモリのアドレスです。ポインタを使って大きな構造体を渡すことができます。もし値渡しを行なっていたら、相対的にもっと多くのシステムリソース(メモリと時間)を毎回のコピーで消費することになります。そのため大きな構造体を渡す際は、ポインタを使うのが賢い選択というものです。
  • Go言語のchannelslicemapの3つの型はメカニズムを実現するポインタのようなものです。ですので、直接渡すことができますので、アドレスを取得してポインタを渡す必要はありません。(注:もし関数がsliceの長さを変更する場合はアドレスを取得し、ポインタを渡す必要があります。)

defer

Go言語のすばらしいデザインの中に、遅延(defer)文法があります。関数の中でdefer文を複数追加することができます。関数が最後まで実行された時、このdefer文が逆順に実行されます。最後にこの関数が返ります。特に、リソースをオープンする操作を行なっているようなとき、エラーの発生に対してロールバックし、必要なリソースをクローズする必要があるかと思います。さもなければとても簡単にリソースのリークといった問題を引き起こすことになります。我々はリソースを開く際は一般的に以下のようにします:

func ReadWrite() bool {
    file.Open("file")
// 何かを行う
    if failureX {
        file.Close()
        return false
    }

    if failureY {
        file.Close()
        return false
    }

    file.Close()
    return true
}

上のコードはとても多くの重複がみられます。Goのdeferはこの問題を解決します。これを使用した後、コードは減るばかりでなく、プログラムもよりエレガントになります。deferの後に指定された関数が関数を抜ける前にコールされます。

func ReadWrite() bool {
    file.Open("file")
    defer file.Close()
    if failureX {
        return false
    }
    if failureY {
        return false
    }
    return true
}

もしdeferを多用する場合は、deferはLIFOモードが採用されます。そのため、以下のコードは4 3 2 1 0を出力します。

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

値、型としての関数

Goでは関数も変数の一種です。typeを通して定義します。これは全て同じ引数と同じ戻り値を持つ一つの型です。

type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

関数を型として扱うことにメリットはあるのでしょうか?ではこの型の関数を値として渡してみましょう。以下の例をご覧ください。

package main
import "fmt"

type testInt func(int) bool // 関数の型を宣言します。

func isOdd(integer int) bool {
    if integer%2 == 0 {
        return false
    }
    return true
}

func isEven(integer int) bool {
    if integer%2 == 0 {
        return true
    }
    return false
}

// ここでは宣言する関数の型を引数のひとつとみなします。

func filter(slice []int, f testInt) []int {
    var result []int
    for _, value := range slice {
        if f(value) {
            result = append(result, value)
        }
    }
    return result
}

func main(){
    slice := []int {1, 2, 3, 4, 5, 7}
    fmt.Println("slice = ", slice)
    odd := filter(slice, isOdd)    // 関数の値渡し
    fmt.Println("Odd elements of slice are: ", odd)
    even := filter(slice, isEven)  // 関数の値渡し
    fmt.Println("Even elements of slice are: ", even)
}

共有のインターフェースを書くときに関数を値と型にみなすのは非常に便利です。上の例でtestIntという型は関数の型の一つでした。ふたつのfilter関数の引数と戻り値はtestIntの型と同じですが、より多くのロジックを実現することができます。このように我々のプログラムをより優れたものにすることができます。

PanicとRecover

GoにはJavaのような例外処理はありません。例外を投げないのです。その代わり、panicrecoverを使用します。ぜひ覚えておいてください、これは最後の手段として使うことを。つまり、あなたのコードにあってはなりません。もしくはpanicを極力減らしてください。これは非常に強力なツールです。賢く使ってください。では、どのように使うのでしょうか?

Panic

ビルトイン関数です。オリジナルの処理フローを中断させることができます。パニックが発生するフローの中に入って関数Fpanicをコールします。このプロセスは継続して実行されます。一旦panicgoroutineが発生すると、コールされた関数がすべて返ります。この時プログラムを抜けます。パニックは直接panicをコールします。実行時にエラーを発生させてもかまいません。例えば配列の境界を超えてアクセスする、などです。

Recover

ビルトイン関数です。パニックを発生させるフローのgoroutineを復元することができます。recoverは遅延関数の中でのみ有効です。通常の実行中、recoverをコールするとnilが返ります。他には何の効果もありません。もし現在のgoroutineがパニックに陥ったらrecoverをコールして、panicの入力値を補足し、正常な実行に復元することができます。

下の関数のフローの中でどのようにpanicを使うかご覧ください

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("no value for $USER")
    }
}

この関数は引数となっている関数が実行時にpanicを発生するか検査します:

func throwsPanic(f func()) (b bool) {
    defer func() {
        if x := recover(); x != nil {
            b = true
        }
    }()
    f() //関数fを実行します。もしfの中でpanicが出現したら、復元を行うことができます。
    return
}

main関数とinit関数

Goでは2つの関数が予約されています:init関数(すべてのpackageで使用できます)とmain関数(package mainでしか使用できません)です。この2つの関数は定義される際いかなる引数と戻り値も持ちません。packageのなかで複数のinit関数を書いたとしても、もちろん可読性か後々のメンテナンス性に対してですが、packageの中では各ファイルに一つだけのinit関数を書くよう強くおすすめします。

Goのプログラムは自動でinit()main()をコールしますので、どこかでこの2つの関数をコールする必要はありません。各packageinit関数はオプションです。しかしpackage mainは必ず一つmain関数を含まなければなりません。

プログラムの初期化と実行はすべてmainパッケージから始まります。もしmainパッケージが他のパッケージをインポートしていたら、コンパイル時にその依存パッケージがインポートされます。あるパッケージが複数のパッケージに同時にインポートされている場合は、先にその他のパッケージがインポートされ、その後このパッケージの中にあるパッケージクラス定数と変数が初期化されます。次にinit関数が(もしあれば)実行され、最後にmain関数が実行されます。以下の図で実行過程を詳しくご説明しています。

図2.6 main関数によるパッケージのインポートと初期化過程の図

import

Goのコードを書いている時は、importコマンドによってパッケージファイルをインポートすることがよくあります。私達が通常使う方法は以下を参考にしてください:

import(
    "fmt"
)

その後コードの中では以下のような方法でコールすることができます。

fmt.Println("hello world")

上のfmtはGo言語の標準ライブラリです。実はGOROOT環境変数で指定されたディレクトリの下にこのモジュールが加えられています。当然Goのインポートは以下のような2つの方法で自分の書いたモジュールを追加することができます:

  1. 相対パス

    import "./model" //カレントファイルと同じディレクトリにあるmodelディレクトリ、ただし、この方法によるimportはおすすめしません。

  2. 絶対パス

    import "shorturl/model" //gopath/src/shorturl/modelモジュールを追加します。

ここではimportの通常のいくつかの方法をご説明しました。ただ他にも特殊なimportがあります。新人を悩ませる方法ですが、ここでは一つ一つ一体何がどうなっているのかご説明しましょう

  1. ドット操作

    時々、以下のようなパッケージのインポート方法を見ることがあります

     import(
         . "fmt"
     )
    

    このドット操作の意味はこのパッケージがインポートされた後このパッケージの関数をコールする際、パッケージ名を省略することができます。つまり、前であなたがコールしたようなfmt.Println("hello world")はPrintln("hello world")というように省略することができます。

  2. エイリアス操作

    エイリアス操作はその名の通りパッケージ名に他の覚えやすい名前をつけることができます。

     import(
         f "fmt"
     )
    

    エイリアス操作の場合パッケージ関数をコールする際プレフィックスが自分たちのものになります。すなわち、f.Println("hello world")

  3. _操作

    この操作は通常とても理解しづらい方法です。以下のimportをご覧ください。

     import (
         "database/sql"
         _ "github.com/ziutek/mymysql/godrv"
     )
    

    _操作はこのパッケージをインポートするだけでパッケージの中の関数を直接使うわけではなく、このパッケージの中にあるinit関数をコールします。

results matching ""

    No results matching ""