RACCOON TECH BLOG

株式会社ラクーンホールディングスのエンジニア/デザイナーから技術情報をはじめ、世の中のためになることや社内のことなどを発信してます。

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という仕組みについて解説してみようと思います。

一緒にラクーンのサービスを作りませんか? 採用情報を詳しく見る

関連記事

運営会社:株式会社ラクーンホールディングス(c)2000 RACCOON HOLDINGS, Inc