Raccoon Tech Blog [株式会社ラクーン 技術戦略部ブログ]

株式会社ラクーン 技術戦略部より、tipsやノウハウなど技術的な話題を発信いたします。

golang

Go事始め (2)

開発の松尾です。
前回は環境面について解説しましたが、今回はGo言語の文法と特徴について解説してみたいと思います。

Go言語のシンプルさと特殊さ

Goは言語の文法という側面から見ると実に「シンプル」な言語です。型推論による型指定の省略、ループのための構文は"for"のみ、GCを備えメモリ管理が不要・・・・・・などなどプログラムを最低限のコード量で記述できるように設計されています。
また、一方でGoは他言語経験者の視線から眺めると少々変わった分かりにくい要素をいくつか備えています。「ポインタ型はあるがポインタ演算は無い」、「channelやmapやsliceといった特殊な組み込み参照型」、「変数の値がstackとheapのどちらに配置されるかはコンパイラが適切に決めてくれる」などなどと私自身もGoの言語仕様を理解する途上でいくつか躓くポイントがありました。

このようにGoには「シンプルで分かりやすい」部分と「Go独特の言語仕様で分かりにくい」箇所が混在しています。このような性質を踏まえつつ言語仕様を見ていくことにしましょう。

func main()

C言語で書かれたプログラムがmain関数より実行が開始されるように、またJavaの場合は指定したclass内に含まれるstaticなmainメソッドより実行が開始されるのと似て、Goはmainパッケージに定義されたmain関数より実行が開始されます。
したがって下記のようなmain関数を含まないソースコードは実行できません。
/* no_main.go */
package main import "fmt" func no_main() { fmt.Println("hogehoge") }
$ go run no_main.go
# command-line-arguments
runtime.main: undefined: main.main
mainパッケージのmain関数が定義されているファイルに対してのみ"go run"で実行することができ、"go build"で実行可能ファイルを作成することができます。

定数

定数はconstを使って定義することができます。
package main
import "fmt"
const (
	OUTER = "外側のconst"
)
func main() {
	const inner = "内側のconst"
	fmt.Printf("%s %s \n", OUTER, inner)
}
パッケージ下および関数内で定義することが可能です。
ここでは文字列を値とする整数を定義していますが、整数、浮動小数点など様々な型を定数として定義することができます。
ちなみに上記のコードでは日本語文字列を使用していますがGoが対応するマルチバイト文字列のリテラル表現はUTF-8エンコーディングに対応しています。(それ以外のエンコーディングである場合はコンパイル時のエラーとなります。)

また、Goにはenum(列挙型)に該当する文法はありませんが、iotaを使うことでenumに似た定数定義を行うことができます。
package main
import "fmt"
const (
	A = iota
	B
	C
	_  // 3をスキップ
	D
	E
)
func main() {
	const X = iota
	const Y = iota

	fmt.Printf("%d\n", A) // 0
	fmt.Printf("%d\n", B) // 1
	fmt.Printf("%d\n", C) // 2
	fmt.Printf("%d\n", D) // 4
	fmt.Printf("%d\n", E) // 5

	fmt.Printf("%d\n", X) // 0
	fmt.Printf("%d\n", Y) // 0
}
iotaの挙動はちょっと分かりづらいのですが「コンテキストが異なるiotaの値は0に戻る」(ソース内のX、Yの挙動)、「同じコンテキスト内では0から1ずつ増加する」、「iotaを使った式がそれ以下の定数に対して自動的に適用される」(ソース内のA-Eの挙動)あたりがポイントになります。

reflectパッケージ

ここで突然reflectパッケージについて説明します。
reflectパッケージは主としてGoのリフレクション機能を提供します。といってもその機能は多岐に渡ってしまうのでここではreflectパッケージを利用することで変数の型情報を得ることができる利便性について解説します。
Goの型システムには少々分かりづらいところがあるため、具体的に実行時の型がどのようになっているのかを得る手段を知ることが大変有用だと思われるためです。

さっそく使ってみましょう。
package main

import(
	"fmt"
	"reflect"
)

func main() {
	b := true
	fmt.Println(reflect.TypeOf(b))
	ui8 := uint8(100)
	fmt.Println(reflect.TypeOf(ui8))
	n := 100
	fmt.Println(reflect.TypeOf(n))
	c := 'A'
	fmt.Println(reflect.TypeOf(c))
	r := 'あ'
	fmt.Println(reflect.TypeOf(r))
	u := '\U00101234'
	fmt.Println(reflect.TypeOf(u))
	s := "文字列"
	fmt.Println(reflect.TypeOf(s))
	a := [3]int{1, 2, 3}
	fmt.Println(reflect.TypeOf(a))
}
このプログラムの実行結果は下記のようになります。
bool
uint8
int
int32
int32
int32
string
[3]int
この結果で分かることは、
  • 組込みでbool型をもつ(true or false)
  • 型指定の無い数値の定数はint型となる
  • シングルクォーテーションで囲んだ「ルーンリテラル」はUnicodeの1文字を表す書き方でint32型となる
  • 組込みでstring型をもつ
  • 配列型は[3]intのように要素数も含めてひとつの型を表す
上記のようなポイントを読み取ることができます。

goでは「:=」の構文を使うことで型指定の省略を行うことができます。しかし、何らかのコードを読んでいて変数の実際の型を知りたいなどの場合にはreflectパッケージの機能が有効に利用できます。

もうひとつ気をつけるべきポイントがあります。goでは組込み型としてuint32(符号なし32bit)、int8(符号あり8bit)などの厳密な数値型が用意されているのですが、数値定数がデフォルトでとるint型ではプラットフォーム次第でその内容が異なることがあります。
具体例を示すと、下記のコードは64bit環境であれば問題なく動作しますが、32bit環境ではコンパイルエラーとなります。
package main

import(
	"fmt"
	"reflect"
)

func main() {
	n := (1<<63) - 1 /* 64bit環境におけるintの最大値 */
	fmt.Println(reflect.TypeOf(n))
}
ここまでは明示的にreflectパッケージの機能を使用してみましたが、goではfmt.Printfの書式に「%T」を指定することで同様の出力を得ることができます。
package main

import (
	"fmt"
)

func main() {
	fmt.Printf("%T\n", true)
	fmt.Printf("%T\n", 1024)
	fmt.Printf("%T\n", "string")
	fmt.Printf("%T\n", [3]int{1, 2, 3})
}
変数の型情報を見るには上記の書き方で十分でしょうが、goでは実行時に変数の型情報を取り出すことができるところがポイントです。Javaで言えばinstanceof演算子のような振る舞いを実現するのに利用することができます。

変数

 先に型推論を利用した変数定義とreflect.TypeOfについて解説しましたが型指定も含めた変数の定義は下記のように書きます。
package main

import (
	"fmt"
)

func main() {
	var b bool = false
	fmt.Println(b)
	var c int32 = 'あ'
	fmt.Println(c)
	var s string = "あいうえお"
	fmt.Println(s)
	var a [5]int = [5]int{1, 2, 3, 4, 5}
	fmt.Println(a)
}
CやJavaの経験者であれば変数名の後に型指定があるところや一見奇妙に見える配列型の定義箇所が少々気味が悪く感じるかもしれません。これがgoの構文ですので慣れましょう(笑)

 また、goの慣例として定義する変数名を「短く簡潔に」定義する傾向があります。Javaなどのように「長く具体的な」命名の慣習を持つ文化に馴染みが深いと違和感が大きいとは思いますが、郷に入っては郷に従え、できるだけ学ぼうとする言語の慣習で書くべきでしょう。それによってコードを読む際にも大いに役に立ちます。

ポインタ型

goにはCに良く似たポインタ型が用意されています。
package main

import (
	"fmt"
	"reflect"
)

func main() {
	n  := 100
	np := &n
	fmt.Println(reflect.TypeOf(np))
	fmt.Println(np)
	fmt.Println(*np)
}
「&変数名」でポインタの取り出し、「*変数名」で値の取り出しとCと大きく違うところはありません。
goのポインタで一番重要なのは「ポインタ型はあるがポインタ演算が無い」という特性です。言語仕様からポインタ演算を取り除くことで得られる安全性を重視した結果とも言えるでしょう。

ポインタについての説明は本気でやると際限なく長くなりそうなのでこの程度にとどめておきます。

構造体(Struct)

goにはCによく似た構造体の定義を行うことができます。
package main

import (
	"fmt"
)

type Member struct {
	name string
	age  int
}

func main() {
	var m Member = Member{"まつお", 37}
	fmt.Printf("name = %s\n", m.name)
	fmt.Printf("age  = %d\n", m.age)
}
Cの構造体と異なってフィールドの初期化に便利なリテラルが利用できます。上記ではname、ageといったフィールドに対して変数の定義と同時に初期化を行っています。

goの構造体の作成には「new([構造体型])」という書き方もあります。
package main

import (
	"fmt"
)

type Member struct {
	name string
	age  int
}

func main() {
	m := new(Member)
	fmt.Printf("name = %s\n", m.name)
	fmt.Printf("age  = %d\n", m.age)
}
newによる初期化は構造体の領域のみを作成して「構造体のポインタ型」を返すことに注意してください。上記のコードを実行するとMember型のフィールドnameは空文字列、ageは数値の0になっていることが確認できます。

なぜこのような文法が用意されているのでしょうか?
私見ですがプログラムにおいて構造体で定義されるデータ構造は比較的「長生き」する性質があります。Cにおいてヒープに作成された構造体のポインタを持ち回り各所で処理を行っていくなどの流れは極めて基本的な手順です。この「構造体を作成してそのポインタを取り出す」といった頻出するパターンを簡略化したのがこのnewによる構造体の作成の手法なのだと思われます。

ちなみにgoでは実行時にオプションを指定してどの変数がヒープに格納されるかを確認する手段が用意されています。
/* newを使用して構造体のポインタを返すCreateMember */
package main

import (
	"fmt"
)

type Member struct {
	name string
	age  int
}

func CreateMember(name string, age int) *Member {
	m := new(Member)
	m.name = name
	m.age = age
	return m
}

func main() {
	m := CreateMember("まつお", 37)
	fmt.Printf("name = %s\n", m.name)
	fmt.Printf("age  = %d\n", m.age)
}
/* 構造体をstack上に作成して構造体のコピーを発生させるパターン */
package main

import (
	"fmt"
)

type Member struct {
	name string
	age  int
}

func CreateMember(name string, age int) Member {
	var m Member
	m.name = name
	m.age = age
	return m
}

func main() {
	m := CreateMember("まつお", 37)
	fmt.Printf("name = %s\n", m.name)
	fmt.Printf("age  = %d\n", m.age)
}
上記2パターンを下記のオプションを付加して実行してみてください。異なる結果が得られます。
下記はnewを使用したバージョンの実行例です。
$ go run -gcflags -m test1.go
# command-line-arguments
./test1.go:12: can inline CreateMember
./test1.go:20: inlining call to CreateMember
./test1.go:12: leaking param: name
./test1.go:13: new(Member) escapes to heap
./test1.go:20: CreateMember new(Member) does not escape
./test1.go:21: main ... argument does not escape
./test1.go:22: main ... argument does not escape
name = まつお
age  = 37
「new(Member) escapes to heap」の箇所からヒープに割り当てられたことが分かりました。

goのプログラム内の変数はその参照範囲を解析することでスタックに配置されるかヒープに配置されるかが自動的に判別されてコンパイルされます。非常に便利な機能なのですが反面、知らずに書くと非効率的なコードを書いてしまう危険性をはらみます。
最低限、データがスタックに置かれるかヒープに確保されるかについて多少なりとも意識的にコーディングする必要があります。

ついでにもうひとつ。構造体の出力についてはfmt.Printfに便利な書式が用意されています。
package main

import (
	"fmt"
)

type Member struct {
	name string
	age  int
}

func main() {
	var m Member = Member{"まつお", 37}
	fmt.Printf("%T\n", m)
	fmt.Printf("%+v\n", m)
	fmt.Printf("%#v\n", m)
}
main.Member
{name:まつお age:37}
main.Member{name:"まつお", age:37}
構造体のフィールドとデータを一括して表示させたい場合に役に立つでしょう。

関数

goの関数定義についてはそれほど変わったところはありません。
package main

import (
	"fmt"
)

func power(n int) int {
	return n * n
}

func main() {
	fmt.Println(power(10))
}
あまり他の言語に見られない文法としては関数が「多値」を返すように定義できるところでしょうか。
package main

import (
	"fmt"
)

func oneTwoThree() (int, int, int) {
	return 1, 2, 3
}

func main() {
	a, _, c := oneTwoThree()
	fmt.Printf("%d %d\n", a, c)
}
ちょっと無理矢理ですがint型の数値を3つ戻す関数の定義です。また、関数の呼び出し側の代入部分で分かるように_(アンダースコア)で変数名を省略して戻り値を無視することができます。

goはC++やJavaのような例外機構は備えていません。そのためgoのプログラムにおいては「失敗する可能性のある処理」については処理結果とエラー情報 を多値で返すという下記のようなパターンが頻出します。
package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Open("input.txt")
	if err != nil {
		os.Exit(1)
	}
	fmt.Printf("%#v\n", file)
}
このようにgoのエラー処理はCにおけるエラー処理の不便さとJavaなどの例外処理のようなコストの高い仕組みの中間ぐらいを狙って設計されていることが分かります。

for

goにおけるループのための構文はforのみと大変シンプルです。
package main

import (
	"fmt"
)

func main() {
	/* C言語的なfor */
	for i := 0; i < 5; i++ {
		fmt.Printf("i=%d\n", i)
	}
	/* C言語タイプのwhile */
	c := 0
	for c < 5 {
		fmt.Printf("c=%d\n", c)
		c++
	}
	/* 配列の走査にはrangeを使う */
	a := [3]int{1, 10, 100}
	for i, v := range a {
		fmt.Printf("[%d]=%d\n", i, v)
	}
	/* stringを文字単位で処理する */
	for pos, r := range "日本語" {
		fmt.Printf("[%d]=%c\n", pos, r)
	}
	/* 無限ループ */
	for {
		break
	}
}
forにいくつかの書き方パターンがあるところが独特ですね。

defer

goの特徴的な構文にdeferがあります。
deferで定義された関数呼び出しは実行されている関数の「脱出直前に呼び出される」という動作をします。
package main

import (
	"fmt"
)

func main() {
	fmt.Println("1")
	defer fmt.Println("2")
	defer fmt.Println("3")
	defer fmt.Println("4")
	fmt.Println("5")
}
Javaなどのtry-catch-finallyといった文法のfinallyに近い動作イメージです。
オープンしたファイルを関数の完了時に確実にクローズするなどのリソース処理などを簡潔に記述するのに役に立ちます。

To be continued

長くなってきたので第2回めはここでいったん終わります。
次回は少し分かりづらいgoの「参照型」や非同期処理のためのgoroutineという仕組みについて解説してみようと思います。

Go事始め (1)

開発の松尾です。
特に社内で利用しているとかそういう実績はひとつも無いんですが色々と思うところあってGoの紹介記事を書いてみます。何らかのプログラミング言語をすでに業務などで使用していてパラダイムの違う別の言語を学びたいというような方の一助となれば幸いです。

Goについて 

GoはGoogleを中心として開発されているオープンソースのプログラミング言語です。
発表された当時は「Cっぽい文法が何だかフワフワしていて気持ち悪い」という程度の感想しか持たずチラチラ見る程度の距離感で眺めていたのですが時間が経つにつれ「こういうプログラミング言語のパラダイムって今後のために超重要じゃね?」とあっさり宗旨変えして調べはじめた経緯があります。
それもこれも、Mozillaにより開発されているRustやErlang VMベースで動作するElixirなどの存在を知り、C++やJavaのように長年使われてきた実績のあるシステム言語ではカバーできない/しずらい領域が広がり始めているのかなあという空気感を感じたというのが大きな理由となっています。
必ずしもGoが将来においてメインストリームとなり得ると確信しているわけでは無いのですが、言語環境のエコシステム、並列処理、厳密な型システムといった最近の潮流を取り入れつつ、かつ言語環境が安定しているというポイントからGoを学ぶには良いタイミングなのではないかと思っています。

Goについての日本語の解説も増えてきているという印象もあるので、この記事では「細かいことは抜きにして具体的にプログラミングを書いてみる」ことができるようポイントを絞って書いてみることにします。そのため言語の細かい部分の解説については割愛します。

Goのメリット 

Goを使用するメリットにはどのようなことが考えられるでしょうか?
わたしは下記の3点が特にお気に入りです。
  1. コンパイルして実行可能ファイルを作ることができる。
  2. OSなどの環境への依存性が少なくポータビリティに優れる。
  3. 軽量な非同期処理のサポートが言語に組み込まれている。

インストール

Goが現在サポートしている環境はLinux、OS X、FreeBSD、Windowsです。
わたしがこの記事を書くにあたって使用している環境はUbuntu 13.10(64bit)です。Linux全般では大した違いは無いと思います。OS XであればパッケージがWindowsであればMSIインストーラーが提供されているため導入における問題は少ないでしょう。
また、Goのバージョンは現在の最新バージョンである1.2.1を選択しています。(※とか言ってるうちに1.2.2になってしまいましたが…)
$ curl -L https://go.googlecode.com/files/go1.2.1.linux-amd64.tar.gz | tar zxf -
$ sudo mv go /usr/local/go1.2.1
$ sudo ln -sfn /usr/local/go1.2.1 /usr/local/go
特にGo本体をどうこうする意図は無いため素直に/usr/local/go1.2.1へインストールしています。/usr/local/goへシンボリックリンクを張って環境変数PATHには/usr/local/go/binを追加します。
$ go version
go version go1.2.1 linux/amd64
goコマンドによってバージョン情報が正しく表示されれば成功です。

初期のgoではGOROOTという環境変数が必要でした。JavaでいうところのJAVA_HOMEのような役割を果たします。上記ではLinux向けのバイナリパッケージを利用しており、GOROOTは標準で/usr/local/goを指します。
$ go env GOROOT
/usr/local/go
上記のようにインストールした場所とgoの設定に相違が無い場合は特にGOROOTを指定する必要はありません。goの複数バージョンを切り替えて使うような特殊な場面では有効に機能します。

また、GOPATHという環境変数も必要です。goの外部ライブラリの格納場所を表しています。これは個人的なものなので特にどこに配置しても良いのですが、わたしは$HOME/goを指定しています。
$ mkdir $HOME/go
$ export GOPATH=$HOME/go

go

goで書かれたプログラムの実行、ビルド、テスト実行、外部パッケージ取得など様々な機能を内包したコマンドです。
$ go
Go is a tool for managing Go source code.

Usage:

        go command [arguments]

The commands are:

    build       compile packages and dependencies
    clean       remove object files
    env         print Go environment information
    fix         run go tool fix on packages
    fmt         run gofmt on package sources
    get         download and install packages and dependencies
    install     compile and install packages and dependencies
    list        list packages
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         run go tool vet on packages
gitのようにサブコマンドがいくつか定義されています。それぞれのサブコマンドのヘルプを確認するには下記のようにhelpを利用します。
$ go help run
usage: go run [build flags] gofiles... [arguments...]

Run compiles and runs the main package comprising the named Go source files.
A Go source file is defined to be a file ending in a literal ".go" suffix.

For more about build flags, see 'go help build'.

See also: go build.

go build

C言語との比較のためにベタなHello Worldプログラムを書いてみましょう。まずはC言語バージョン。
/* hello_c.c */
#include "stdio.h" main() { printf("Hello, World!\n"); }
次はGoバージョンのHello World。
/* hello_go.go */
package main import "fmt" func main() { fmt.Println("Hello, World!") }
ビルドして実行可能ファイルを作成します。goプログラムのビルドには"go build -o [出力ファイル] [ソースファイル]" を利用しています。
$ gcc -o hello_c hello.c
$ go build -o hello_go hello_go.go
これでhello_cとhello_goの2つの実行可能ファイルが作成されました。どちらも実行すると"Hello, World!"と表示するだけの極めて単純なプログラムです。
ですがここで作成された実行可能ファイルに大きな違いがあります。

 c_vs_go_on_hello_world

このようにCバージョンもGoバージョンもソースコードのサイズこそ似たようなものですが、生成されたバイナリのサイズに大きな違いがあります。Cバージョンが8.6kBに対してGoバージョンは2.2MB! Goが生成したバイナリはCバージョンのそれに比べてなんと250倍のサイズがあります。
この理由について調べるためにLinuxのELF形式の実行可能ファイルの内部情報を覗くツール"readelf"を使ってみましょう。
$ readelf -d hello_c

オフセット 0xe28 にある動的セクションは 24 個のエントリから構成されています:
  タグ        タイプ                       名前/値
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libc.so.6]
... (中略)

$ readelf -d hello_go このファイルには動的セクションがありません。
Cバージョンのプログラムでは共有ライブラリlibc.so.6への依存が表示されますが、Goバージョンでは「動的セクションが無い」というあっさりとした表示で完了します。実行時にlibc.so.6を動的に使用するかビルドしたバイナリ内に必要な静的ライブラリ全てを組み込むかの差が実行可能ファイルのサイズ差となって現れているわけですね。

ここで理解できるのはGoはその設計方針としてOSの中核をなすような基本的なライブラリ群にすら依存しないように構成されているということです。言い換えるならばOSにインストールされているライブラリ群が古かったり新しくなって互換性が損なわれた場合にもGoの動作にはいささかも影響を与えないということです。「車輪の再発明」になろうとも環境の違いをものともしない一貫性とポータビリティが最重要視されているとも言えるでしょう。

もうひとつ実験

ついでに先ほどのhello_go.goに別のパッケージに依存する処理を書き加えてビルドするとどうなるでしょうか?
/* hello_go.go */
package main import (
"fmt"
"net/http"
) func main() { fmt.Println("Hello, World!")
_, _ = http.Get("http://example.com/") }
importする対象に"net/http"というHTTPプロトコルを操作するためのパッケージを追加して適当なURLの内容をGETする操作を付け加えています。
これを先ほどの手順と同じようにビルドしてみます。
go_binary_file_size

ほんの数行の追加ですが先ほどの2.2MBから7.1MBと盛大にサイズが大きくなりました。これは"net/http"というパッケージへの依存性が追加されたため、今回のわたしの実験環境であれば、/usr/local/go/pkg/linux_amd64/net/http以下に配置されている静的ライブラリが実行可能ファイル内に組み込まれたことを表しています。
goでビルドされたバイナリにどのようなライブラリが組み込まれているかについては先ほどと同様にreadelfを使ってシンボルテーブルを覗くことで調べることができます。
$ readelf -s hello_go | grep http | grep Get
 11037: 000000000043a020   115 FUNC    GLOBAL DEFAULT    1 net/http.Get
 11038: 000000000043a0a0   213 FUNC    GLOBAL DEFAULT    1 net/http.(*Client).Get
 11063: 000000000043db60    84 FUNC    GLOBAL DEFAULT    1 net/http.Header.Get
 11247: 0000000000459850   192 FUNC    GLOBAL DEFAULT    1 net/http.(*Header).Get

go run 

まずはgoによるビルド方法を挙げてみましたが毎回毎回ビルドするのは明らかに面倒です。goではビルドから実行までの流れをサブコマンド"run"を使うことで一発で処理できます。
$ go run hello_go.go
Hello, World!
各種スクリプト言語と同等ぐらいに簡便ですね。

go test

goには標準でテスト機能が付属しており"go test"で実行することができます。
ただし、その実行単位はgoのパッケージ単位で動作するためいくつかのルールに従う必要があります。

ここではシンプルな計算のための関数を含んだmymathパッケージを作成してそのテストを行う手順について記してみます。
まずはmymathパッケージの本体です。$GOPATH/src/mymath/mymath.goにソースファイルを保存します。わたしの環境では$HOME/go/src/mymath/mymath.goに配置されることになります。
package mymath

func Power(n int) int {
	return n * n
}
ちなみに任意のpackage以下で定義された関数はその名前が英大文字で始まる場合にpublicな関数として扱われパッケージ外より呼び出すことが可能になります。関数名の先頭が英小文字である場合はprivateな関数となります。

さて、mymath.goと同じディレクトリに下記のテストファイルを配置します。"[パッケージ名]_test.go"というファイル名にして配置する必要があります。この場合は、$GOPATH/go/src/mymath/mymath_test.goですね。
package mymath

import (
	"testing"
)

func TestPower(t *testing.T) {
	expect := 256
	actual := Power(16)
	if expect != actual {
		t.Error("Power(16) != 256")
	}
}

func TestPower2(t *testing.T) {
	t.Error("error")
}
テスト対象のパッケージと同じパッケージに所属していることに注意してください。さらにテストを実行するためのtestingパッケージのimportが必要で、各テストの関数名をTest*より開始することでテスト関数であると認識されます。
また、各テスト関数はtesting.Tという型の引数をとります。他言語のUnit Testにあるようなassertionといった機能は提供されておらず、testing.T型の引数に対してError()、Fatal()といった関数を使って適宜テストの失敗を報告する構成になっています。 

上記2ファイルを規定の場所に保存していよいよテストを実行することができます。
"go test"に指定するのはファイルやディレクトリではなくパッケージ名であることに注意してください。
$ go test mymath
--- FAIL: TestPower2 (0.00 seconds)
        mymath_test.go:16: error
FAIL
FAIL    mymath  0.001s
意図的にTestPower2()が失敗するように書いたためTestPower2がFAILした旨が表示されましたが、テストの実行自体は成功しています。
パスしたテストも表示するにはオプションに-vを追加します。
$ go test mymath -v
=== RUN TestPower
--- PASS: TestPower (0.00 seconds)
=== RUN TestPower2
--- FAIL: TestPower2 (0.00 seconds)
        mymath_test.go:16: error
FAIL
exit status 1
FAIL    mymath  0.001s
パスしたテストの内容も表示されるようになりました。

go1.2からはテストのcoverage測定もオプションで指定できるようになりました。
テストが失敗するとcoverageの出力ができないためテストコードに手を入れて全てのテストがパスするように書き換えます。
package mymath

import (
	"testing"
)

func TestPower(t *testing.T) {
	expect := 256
	actual := Power(16)
	if expect != actual {
		t.Error("Power(16) != 256")
	}
}

func TestPower2(t *testing.T) {
	t.Log("pass")
}
coverage測定のためのオプション、-coverを付加して実行してみましょう。
$ go test mymath -v -cover
=== RUN TestPower
--- PASS: TestPower (0.00 seconds)
=== RUN TestPower2
--- PASS: TestPower2 (0.00 seconds)
        mymath_test.go:16: pass
PASS
coverage: 100.0% of statements
ok      mymath  0.001s 
テスト実行の出力内容に"coverage: 100% of statements"が表示されるようになりました。
"-cover"オプションに留まらず、CPUのプロファイル情報のための"-cpuprofile"、各テストを並列実行するための"-parallel"など有用なオプションが色々と用意されています。詳しくは"go help testflag"で参照することができます。

To be continued

今回はGoの環境設定から中心となるツールについての概観を行ってみました。
次回の「Go事始め (2)」では例え他言語に精通していたとしても引っかかりがちなgoの言語機能について解説してみたいと思います。

記事検索