Golang Cafe #1 まとめ

2013/10/27に開催された「Golang Cafe #1」についてのまとめです。
Go環境の準備については、Golang Cafe #0(準備編)にまとめてあります。

今回のお題

How to Write Go Codeをやる。
(WorkspaceとTestの辺りまで)

WorkspaceとPackage

WorkspaceとPackageの考え方

あるディレクトリをルートとして(今回はc:\gocafe)そのディレクトリ以下をワークスペースとします。
ワークスペース内には以下の3つのディレクトリを配置します。

  • src ソースコードを配置
  • pkg ビルドされたパッケージを配置
  • bin ビルドされた実行コマンドを配置

最低限必要なのはsrcディレクトリだけで、pkgおよびbinディレクトリは必要に応じて勝手に作成されます。

go toolを使いやすくするために、このワークスペースのパスを環境変数に追加し、binディレクトリにパスを通しておきます。

GOPATH=c:\gocafe
PATH=%PATH%;%GOPATH%\bin;

※実際は環境変数に追加しておきます。


srcディレクトリ配下は、1ディレクトリ1パッケージ(プロジェクト)という形になります。
各ルートパッケージ名は、リモートリポジトリを使用する場合は、

github.com/username/パッケージ名/

そうでない場合は、

jp.co.hoge/パッケージ名/

の形がお薦めらしい。


パッケージ直下にはソースコードを配置します。
パッケージ名とソースコードのファイル名は同じでなくてもよく、ソースコードのpackage宣言とパッケージ名が同じであれば良い。
例えば以下のような形でも良い。

GOPATH/github.com/username/hello/sample.go

sample.go

package hello

import "fmt"

func main() {
    fmt.Printf("Hello, world\n")
}

但しメインパッケージは例外で、パッケージ宣言はmainで、パッケージ名.exeが作成されます。
また、パッケージには階層を持たせることができます。
Java言語のようにパッケージ階層とディレクトリ階層は一致させておく必要がありますが、ソースコードのpackage宣言は自分が所属するパッケージ名だけになります。

GOPATH/github.com/username/mylib1/mylib2/mylib3/sample.go
package mylib3

import "fmt"

func myfunc() {
    fmt.Printf("myfunc\n")
}
簡単なプログラム

helloディレクトリを作成しsample.goを作成します。

GOPATH/src/github.com/username/hello/sample.go
package main

import "fmt"

func main() {
    fmt.Printf("Hello, world!\n")
}

GOPATHディレクトリでコマンドラインから

$ go install github.com/username/hello

または

$cd GOPATH/src/github.com/username/hello
$go install

を実行すると、GOPATH/binにhello.exeが作成されます。
ディレクトリの先頭のsrcは不要です。自動的にsrcディレクトリ以下を調べるようです。パス指定する場合は./src/を付けます。)
そしてGOPATH/binには既にパスを通してあるので、

$ hello
Hello, world!

と結果が得られます。

簡単なライブラリ

mylib1ディレクトリを作成しsample.goを作成します。

GOPATH/src/github.com/username/mylib1/sample.go
package mylib1

func Div(x int, y int) float64 {
    // go言語でのキャストはJavaやC#とは異なり型ではなく変数の方を括弧で囲みます。
    return float64(x) / float64(y)
}

そしてhello/sample.goを修正します。

package main

import (
    "fmt"
    "github.com/username/mylib1"
)

func main() {
    fmt.Printf("Hello, world! Div(%v, %v)=%v", 5, 2, mylib1.Div(5, 2))
}

GOPATHディレクトリでコマンドラインから

$ go build github.com/username/mylib1

とするとエラー等がある場合はここで表示されます。しかし出力は何もありません。
git installまたはこのパッケージを使用するパッケージでgo installを実行するとpkgディレクトリが作成されmylib1.aが出力されます。

$ hello
Hello, world! Div(5, 2)=2.5

と結果が得られれば成功です!
因みにここまで終わった段階でワークスペース以下のディレクトリ構成は以下のようになっています。

bin/
    hello.exe
pkg/
    windows_amd64/github.com/username/mylib1.a
src/
    github.com/username/hello/sample.go
    github.com/username/mylib1/sample.go

パッケージはpkg直下にOSおよびアーキテクチャディレクトリが作成されその配下に出力されます。

Testing

Go言語にはデフォルトでtestingパッケージが用意されており、今回はこちらを利用します。

testingパッケージを利用する場合のお約束事
  • テストするパッケージと同じパッケージに配置する
  • ファイル名は_test.goで終わるようにする
  • TestXxxxの関数名にする、Xxxxはテスト対象の関数名
  • 引数はt *testing.T
簡単なテスト

ここでは上記で説明した「簡単なライブラリ」に対するテストを考えます。
mylib1ディレクトリにsample_test.goを作成します。

GOPATH/src/github.com/username/mylib1/sample_test.go
package mylib1

import "testing"

func TestDiv(t *testing.T) {
    // if文の中に代入式を書くことが出来ます。
    // スコープはifブロック中でelseの中でも参照できます。
    if x := Div(5, 2); x != 2.5 {
        t.Errorf("Div(%v, %v) = %v, want %v", 5, 2, x, 2.5)
    }
}

GOPATHディレクトリでコマンドラインから

$ go test github.com/username/mylib1

または

$cd GOPATH/src/github.com/username/mylib1
$go test

を実行しテストが通ると、

ok      github.com/username/mylib1     0.458s

のように表示され、失敗すると、

--- FAIL: TestDiv (0.00 seconds)
        sample_test.go:9: Div(5, 2) = 3.5, want 2.5
FAIL
FAIL    github.com/taknb2nch/mylib1     0.429s

のように表示されます。
ここで非常に分かりにくいのが、複数のテストが存在して一部のテストが失敗した場合、失敗したテストの出力しかされません。成功したテストの"成功しました"という出力はありません。
そういう場合は-vオプションを指定することですべて出力されるようです。(オプションの詳細な説明はこちら

$ go test -v
=== RUN TestDiv
--- FAIL: TestDiv (0.00 seconds)
        sample_test.go:12: Div(5, 2) = 3.5, want 2.5
=== RUN TestAdd
--- PASS: TestAdd (0.00 seconds)

また複数のパッケージをテストする場合は、

$ go test github.com/username/mylib1 github.com/username/mylib1/mylib2

のように複数のパッケージをスペースで区切って指定します。

go test -short

とすればプログラム中でt.Skipped()の値を参照して処理を振り分けられるようです。
テスト出力には他にもFatal、LogあるいはSkipなどがありますが、JUnitNUnit等と比べると正直使いやすいとはいえません。
(ちなみにLog、Skipはgo test -vとする必要があるようです)

Benchmark

Go言語ではベンチマークをとることもできるようです。
テストの場合とお約束が若干違って

  • BenchmarkXxxxの関数名にする、Xxxxはベンチマーク対象の関数名
  • 引数はb *testing.B

となります。

簡単なベンチマーク

ここでは上記で説明した「簡単なライブラリ」に対するベンチマークを考えます。
mylib1ディレクトリのsample_test.goに追加します。

func BenchmarkDiv(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Div(5, 2)
    }
}

GOPATHディレクトリでコマンドラインから

$ go test -bench . github.com/username/mylib1

または

$cd GOPATH/src/github.com/username/mylib1
$go test -bench .

を実行すると、

PASS
BenchmarkDiv    1000000000               2.82 ns/op
ok      github.com/username/mylib1     3.549s

と表示されます。このベンチマークでは1000000000回ループして2.82ns/1回ということになります。
注意すべき点は、テストコードが存在している場合、すべてのテストがOKでないとベンチマークは実行されません。

Examples

もう一つExamplesというのがあるようです。いまいち用途を理解できていません。
こちらのお約束も少し違って、

  • ExampleXxxxの関数名にする、Xxxxは適当な名前
  • 引数はなし

となります。

簡単なExamples

mylib1ディレクトリのsample_test.goに追加します。

func ExampleHello1() {
    fmt.Println("hello")
    // Output: hello
}

func ExampleHello2() {
    fmt.Println("hello, and")
    fmt.Println("goodbye")
    // Output:
    // hello, and
    // goodbye
}

GOPATHディレクトリでコマンドラインから

$ go test -v github.com/username/mylib1

または

$cd GOPATH/src/github.com/username/mylib1
$go test -v

を実行すると、

=== RUN: ExampleHello1
--- PASS: ExampleHello1 (0)
=== RUN: ExampleHello2
--- PASS: ExampleHello2 (0)

と表示されます。要は関数内で標準出力に出力された内容がOutput:以降の内容と等しいかどうか判定しているようです。
結局はよくわからないよね、で終わってしまった。
ドキュメントを読み込んでみないといけませんね。