かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Go 言語勉強ログ その 1

さて、先日 Go 言語の環境を整えたので早速やっていこうと思います。

blog.okazuki.jp

とりあえず A Tour of Go を見ながら思ったことを書いていきます。実際に勉強しようと思う人は A Tour of Go を見ましょう。素晴らしい翻訳をしてくれた人たちに感謝ですね!

プログラムのエントリーポイント

プログラムのエントリーポイントは main パッケージの main 関数のようです。 実際に %GOHOME%\src\sample フォルダに main.go を新規作成して以下のような処理を書くと Hello world と表示されます。

package main

import (
    "fmt"
)

func main() {
    fmt.Print("Hello world")
}

実行結果

Hello world

どうも Java と同じでファイル名の先頭で package を宣言して import で読みこむパッケージを指定するみたいです。 Java あたりから行われてきたエントリーポイントのメソッド(関数)ですらクラスに所属するという形ではなく、その後に流行ったスクリプト言語系に近いノリを感じます。でも型がある!

import 文に関しては

import "fmt"
import "hogehoge"

みたいにわけて書けるみたいですが、以下のようにまとめて書くのがいいらしいです。理由はなんだろう。

import (
  "fmt"
  "hogehoge"
)

変数・定数の定義

Go では変数や定数が定義できます。大まかに 3 つあるという理解です。

package main

import (
    "fmt"
)

func main() {
        // 宣言 + 初期化
    var s = "Hello world"
        // 宣言 + 初期化 (関数内じゃないと使えない)
    i := 0
        // 宣言と初期化を別の行で
    var f float64
    f = 3.14

    fmt.Printf("%v, %v, %v", s, i, f)
}

const にすると定数です。

package main

import (
    "fmt"
)

func main() {
    const s = "Hello world"
    s = "Update" // Error!
    fmt.Printf("%v", s)
}

基本型

大体どの言語にもあるような基本型を持ってる。

bool
string
int
int8
int16
int32
int64
uint
uint8
uint16
uint32
uint64
uintptr
byte # int8 の別名
rune # int32 の別名。他の言語でいう char
float32
float64
complex64
complex128

C# との違いとして気付くのが名前的な部分として char じゃなくて rune なのと、C# では確かクラスライブラリにある複素数型を基本型として持ってる。

int, uint, uintptr は C# とは違って 32bit/64bit 環境でサイズが変わるみたい。確か C/C++ あたりがそうだったかな?記憶は怪しい。 恐らく、その環境で一番早く動くものを選択するためだと思うけど、大き目な数字(21億以上なので普通はあんまりないと思う)を扱いたいときとかは int と書くよりも int64 と明示的にサイズを指定したほうが良さそうに感じた。 でも最近は 64bit がほとんどだと思うのでそんなに心配しなくてもいいかな?

初期値は初期化しないと未定みたいなものがある C 言語とは違って 0 や false や空文字が割り当てられる。

型変換

Type conversions と書いてあるけどキャストですね。Go ではキャストって言わないのかな。

書き方は 型名(変換したい値) のように書く。

var floatValue = 3.14
var i = int(floatValue)
fmt.Printf("int(%v) -> %v\n", floatValue, i) // int(3.14) -> 3

関数の定義

関数は既に main で定義してるのでなんとなくわかりますが、以下のような感じに書きます。

func sayHello() {
    fmt.Print("Hello world")
}

引数を受け取る場合は、変数宣言と同じように型名を後ろに書く感じで指定します。

package main

import (
    "fmt"
)

func main() {
    sayHello("Kazuki")
}

// 引数を受け取る
func sayHello(name string) {
    fmt.Printf("Hello world!! %v", name)
}

複数の引数を受け取る場合はカンマ区切りで書く。

package main

import (
    "fmt"
)

func main() {
    sayHello("Kazuki", 37)
}

func sayHello(name string, age int) {
    fmt.Printf("Hello world!! %v!! %v years old", name, age) // Hello world!! Kazuki!! 37 years old
}

面白いのが引数に同じ型が連続する場合は最初のほうのは型名を省略できるところ。 下のプログラムは age と salary が同じ型なので age のほうの型を省略してます。

package main

import (
    "fmt"
)

func main() {
    sayHello("Kazuki", 37, 40)
}

func sayHello(name string, age, salary int) {
    fmt.Printf("Hello world!! %v!! %v years old. %v", name, age, salary)
}

関数の戻り値は関数の後ろに型を書きます。

package main

import "fmt"

func main() {
    fmt.Print(sayHello("Kazuki", 37, 40))
}

// 整形した文字列を返すようにした
func sayHello(name string, age, salary int) string {
    return fmt.Sprintf("Hello world!! %v!! %v years old. %v", name, age, salary)
}

戻り値は複数返すことにも対応してます。C# でもタプルを返すことで疑似的に複数戻り値に対応してたけど、こっちは言語組み込みって感じなのかな。

package main

import "fmt"

func main() {
    s, kamo := sayHello("Kazuki", 37, 40)
    if kamo {
        fmt.Println("Kamo!!")
    }

    fmt.Print(s)
}

// 整形した文字列を返すようにした, salary が 40 以上はカモってことで
func sayHello(name string, age, salary int) (string, bool) {
    return fmt.Sprintf("Hello world!! %v!! %v years old. %v", name, age, salary), salary >= 40
}

実行するとこんな感じ。

Kamo!!
Hello world!! Kazuki!! 37 years old. 40

ふむふむ。if 文も書いてみましたが、条件を書くところに括弧がいらないのが衝撃でした。

A Tour of Go の例では swap 関数が紹介されてましたが、とても簡単に swap 出来てました。いいね! C 言語のポインターの章での定番問題みたいな奴でしたが、まぁこういう形で実装できるほうが絶対いいですもんね。

package main

import "fmt"

func swap(a, b string) (string, string) {
    return b, a
}

func main() {
    a, b := swap("first", "second")
    fmt.Printf("%v, %v", a, b)           // second, first
}

戻り値に名前を付ける?

戻り値に名前が付けれるみたいです。 C# でいうところの out 引数みたいなやつっぽいですね。

戻り値の型を書くところに変数名も添えるだけでOK。あとは関数内で代入するといいみたいです。C# では out 引数に値を代入しないとエラーになりますが、Go ではエラーにならないみたいですね。

package main

import "fmt"

func tryParse(boolString string) (r bool, success bool) {
    if boolString == "true" {
        r = true
        success = true
        return
    }
    if boolString == "false" {
        r = false
        success = true
        return
    }

    success = false
    return
}

func main() {
    r, success := tryParse("true")
    fmt.Printf("result = %v, success = %v\n", r, success) // -> result = true, success = true

    r, success = tryParse("正しい")
    fmt.Printf("result = %v, success = %v\n", r, success) // -> result = false, success = false
}

制御構造

分岐系が if, switch で繰り返しが for がある。while はない。

if と for を試すために FizzBuzz を書いてみた。

package main

import "fmt"

func main() {
    for i := 1; i <= 30; i++ {
        if i%15 == 0 {
            fmt.Print("FizzBuzz, ")
            continue
        }

        if i%3 == 0 {
            fmt.Print("Fizz, ")
            continue
        }

        if i%5 == 0 {
            fmt.Print("Buzz, ")
            continue
        }

        fmt.Printf("%v, ", i)
    }
}

if と for に括弧が必要ないのが慣れない。

ちょっとびっくりしたのが while 文で書くようなことは for の初期化と後処理の部分を省略して for ; x < 10; { ... } みたいに書くのかと思ったら、この状態からさらにセミコロンの省略が可能。

package main

import "fmt"

func main() {
    i := 0
    sum := 0
    for i < 10 {
        sum += i
        i++
    }
    fmt.Printf("sum = %v", sum) // sum = 45
}

これを書く途中で気になったのがインクリメントの結果に戻り値が無いということ?? sum += i++ はエラーになった。エラーの中身はシンタックスエラーなので、もしかしたら += のところには書けないのかもしれない。そのうち書いてあるドキュメントに当たるだろう。

もう 1 つびっくりしたのが無限ループをスタイリッシュ?に書けるところ。

for {
}

なるほど。for true { ... } かと思ってた。

if 文には セミコロンで区切って for 文みたいに初期化処理を書ける。そこで宣言した変数のスコープは if 文の中になる。else の中でももちろん有効

package main

import "fmt"

func main() {
    var x = 10
    if twiceValue := x * 2; x > 0 {
        fmt.Print(twiceValue)
    }
}

else if はどうなる…? 試してみた結果、一連の else if で宣言されたもののうち自分より前で宣言されてれば使える。まぁ自然な動き。

package main

import "fmt"

func main() {
    var x = -5
    if twiceValue := x * 2; x > 0 {
        fmt.Print(twiceValue) // ここでは tripleValue は当然使えない
    } else if tripleValue := x * 3; x < -10 {
        fmt.Printf("triple: %v", tripleValue) // OK
    } else {
        fmt.Printf("twice: %v, triple: %v", twiceValue, tripleValue) // OK
    }
}

switch は break が無くて OK。個人的にはこの割り切りの方が好き。

package main

import "fmt"

func main() {
    for i := 1; i < 30; i++ {
        switch i {
        case 1:
            fmt.Print("いち!")
        case 2:
            fmt.Print("にー!")
        default:
            fmt.Printf("たくさん!!")
        }
    }
}

結果は、なんか頭悪い感じになった。

いち!にー!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!たくさん!!

case は定数じゃなくてもいいからこういうことも出来る。なれるまでこの書き方を思いつかなさそうだ。

package main

import (
    "fmt"
    "math/rand"
)

func main() {
    for i := 0; i < 100; i++ {
        switch i {
        case rand.Intn(100): // !?
            fmt.Print("Lucky!!")
        default:
            fmt.Print("Normal!!")
        }
    }
}

switch の後の条件を省略すると switch true になる。何を言っているのかわからなかったが if else をシンプルに書けるということで FizzBuzz がこうなる。

package main

import (
    "fmt"
)

func main() {
    for i := 1; i < 100; i++ {
        switch {
        case i%15 == 0:
            fmt.Print("FizzBuzzz, ")
        case i%3 == 0:
            fmt.Print("Fizz, ")
        case i%5 == 0:
            fmt.Print("Buzz, ")
        default:
            fmt.Printf("%v, ", i)
        }
    }
}

これはいいね!

Defer

他の言語でいう finally や C# の using や Java でいう try with resources に近いことを実現するようなものっぽい。finally なんかはブロックに対して後処理を付け加えるイメージだけど Go の Defer は関数に対する後処理を書くようなイメージ。

defer return の前で実行してほしい処理 と任意の場所に書く感じ。ちょっとわかりにくい気がするのだがどうだろう。 でも、こんな風に状況に応じて後始末みたいな処理を呼び分けれるのはわかりやすいと思う。

package main

import (
    "fmt"
)

func f(i int) {
    if i%2 == 0 {
        fmt.Println("偶数用リソース確保!!")
        defer fmt.Println("偶数用リソース解放!!")
    } else {
        fmt.Println("奇数用リソース確保!!")
        defer fmt.Println("奇数用リソース解放!!")
    }

    fmt.Printf("何か処理: %v\n", i)
}

func main() {
    for i := 0; i < 4; i++ {
        fmt.Println("---")
        f(i)
    }
}

結果

---
偶数用リソース確保!!
何か処理: 0
偶数用リソース解放!!
---
奇数用リソース確保!!
何か処理: 1
奇数用リソース解放!!
---
偶数用リソース確保!!
何か処理: 2
偶数用リソース解放!!
---
奇数用リソース確保!!
何か処理: 3
奇数用リソース解放!!

複数回呼んだときには最後に呼ばれたものから実行される。変数なんかは、ちゃんと defer した時の値が使われるみたい。

package main

import (
    "fmt"
)

func main() {
    for i := 0; i < 4; i++ {
        fmt.Println(i)
        defer fmt.Printf("derer: %v\n", i)
    }
}

結果は

0
1
2
3
derer: 3
derer: 2
derer: 1
derer: 0

途中で値を書き換えても、その時の値。

package main

import (
    "fmt"
)

func main() {
    i := 0
    defer fmt.Print(i)
    i++
}
// 実行結果は 0 になる

とりあえず A Tour of Go の Packages, variables, and functions. と Flow control statements: for, if, else, switch and defer まで読んだので次は More types: structs, slices, and maps だ。お昼食べたら続きを読もう。