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
}
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))
}
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})
}
変数
先に型推論を利用した変数定義と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)
}
また、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)
}
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)
}
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)
}
なぜこのような文法が用意されているのでしょうか?
私見ですがプログラムにおいて構造体で定義されるデータ構造は比較的「長生き」する性質があります。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)
}
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)
}
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
}
}
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という仕組みについて解説してみようと思います。