Go言語触ってみた その2

昨日は眠さに負けて寝ちゃったので、続きを書く。

Go言語の特徴的な点

印象に残った点を書いて行く。

Goはオブジェクト指向???

Goには、現代的なオブジェクト指向言語の大半に存在する「クラス」というものが存在しない。というか、今サラッと「オブジェクト指向言語」と書いたがそもそもGoがオブジェクト指向言語かどうかは微妙なところで、公式のFAQでも「Goはオブジェクト指向言語か?」という問いに対して「Yes and no.」と答えている。

参考: The Go Progrraming Frequently Asked Questions (FAQ)

クラスが無いから継承による型の階層も無くて、オブジェクト指向原理者が愛するサブクラスを利用した「ポリモーフィズム」みたいなものも無い。ただ、代わりにGoの型システムには特定の振る舞いを型とする「interface」や、他の型を包みこんだ新しいstructを定義する「embbedding」のような機能がある。詳しくは次で触れる。

Goの型宣言には既存の型の別名, struct, interfaceの3種類が存在

Goの型システムはものすごくシンプルに出来ている。例えば、新しい型の宣言方法は次の3種類しか存在しない。

1. まず、既存の型に別名をつける場合

type MyString string // MyStringが新しい型。

func main() {
    var str String = "I am string" // これが普通にstring型の変数strを作成した場合。
    var my_str MyString = "I am my_string" // MyString型の変数my_strも同様に作る事が出来る。
    fmt.Println(my_str) // I am my_stringが標準出力に出力される
}

上記のコードに触れる前に先にGoの文法について軽く説明しておくと、Goにおいては変数宣言の際に変数名の後に型名を書く。イメージは、var (変数名) (型名) = (値)みたいな感じ。値から型が明らかであれば、型推論が効くため(変数名) := (値)という構文を使えるが、上記の例ではMyString型の値を使う為にあえて型を明示している。

関数はfunc (関数名) (引数) (返り値) { 関数の定義 }という構文を使い、返り値は無ければ省略出来る。コードをコンパイルするとmain関数の中身が実行される。

fmt.Printlnは引数として受け取った値を標準出力に出力する関数で、試し書き中は重宝する。

上記の例では、stringの別名であるMyStringという型を新しく定義している。型の宣言は、基本的にtype (新しい型名) (既存の型)という構文で行う。MyStringは、stringと同じ様にダブルクオートを用いて値を生成出来る。別名と書いたが、キャスト可能であるだけで実際には別の型であり、例えばMyStringに対して定義したメソッドはstringに対して呼び出す事は出来ない。

メソッドについてはまた後で触れるが、Goでは任意の型に対してメソッドを定義出来るという特徴的な振る舞いを持つ。

2. structを使って新しい型を定義する場合

structを使うと、特定のフィールドの並びを型として宣言出来る。Cとかで言う構造体の用なものと思ってもらえばOK(まあ名前がstructだし)。

// Point型として、int型のフィールドxとyを持つstructを定義
type Point struct {
    x, y int
}

func main() {
    point := Point{1, 2} // 複合リテラルを用いてPoint型の値を生成
    fmt.Println(point) // {1,2}と出力される
}

structの場合も、type (型名) struct { フィールドの名前と型 }というtypeキーワードを用いた構文で型を宣言する。

structの生成にはいくつか方法があるが、一番簡単なのは複合リテラル(Composite literals)を用いた方法だと思う。上記の例の用に、型名に続く波括弧の中にフィールドの値を書く。フィールドには名前がついている為、明示的に名前を用いても良い。尚、全てのフィールドの初期値を指定する必要な無く、指定しなかったフィールドは自動で0に初期化される。

point := Point{x: 1} // {1, 0}

ちなみに、structを用いたtype (型名) strunct { フィールド }という宣言は、実はstruct { フィールド }という に別名をつけているとみなす事も出来る。これは、次のコードをみてもらうと分かり易い。

var myStruct struct{ x int } // struct{ x int }型の変数myStructを宣言 & 0で初期化
fmt.Println(myStruct) // {0}が出力される
fmt.Println(reflect.TypeOf(myStruct)) // 型名として struct{ x int } が出力される!!!

上記の例では、struct{ x int }型の変数であるmyStructを宣言し、その値や型を出力している。Goにおいては変数は宣言と同時に0やnilで初期化される為、特に指定したい値が無ければ明示的な初期化は不要。

実際、値が{0}である事や、型がstruct{ x int }である事が確認できている。ちなみに、reflctパッケージのreflect.TypeOf関数というのは変数の型を確認したい時に使える関数。

3. interfaceを使って新しい型を定義する場合

3つめの方法が、interfaceを使う方法。interfaceは、「特定のメソッドを持っている」事を型として保証する仕組みで、例えばAbsメソッドを持っていればAbser型として扱う事が出来る。

簡単な例を示す。

  1 package main
  2
  3 import "fmt"
  4
  5 type Abser interface {
  6     Abs() float64
  7 }
  8
  9 type MyFloat float64
 10
 11 func (f MyFloat) Abs() float64 {
 12     if f < 0 {
 13         return float64(-f)
 14     }
 15     return float64(f)
 16 }
 17
 18 func main() {
 19     var a Abser = MyFloat(-10.0)
 20     fmt.Println(a.Abs())
 21 }
 22

まず、5~7行目でAbserというinterface型の定義を行っている。これも、今までと同様typeキーワードを用いて行う。interfaceに続く波括弧の中では、定義されなければならないメソッドを記述する。Abserにおいては、引数が無く返り値がfloat64型であるAbsという名前のメソッドが存在しなければならない事が分かる。

次に、9行目ではfloat64型の別名としてMyFloat型を定義し、さらに11~16行目でMyfloat型に対してAbsメソッドの定義を行っている。 ここで、Goにおけるメソッド定義の構文について触れておく。最初に説明した様に、Goにおいてはどんな型にでもメソッドを定義出来て、その構文はfunc (変数名 メソッドを定義したい型名) メソッド名(引数) 返り値 { 実装 }みたいな感じになる。普通の関数の定義にめちゃくちゃ似てるんだけど、関数名の前に(変数名 メソッドを定義したい型名)というのが付く点だけが異なる。

11行目はまさにメソッド定義の形式になっていてい、MyFloatという型に対してAbsメソッドを定義している。MyFloatAbsメソッドが定義された為、MyFloat型の値はAbser型としても振る舞う事が出来て、実際に19行目でAbser型の変数であるaに代入出来ている。ちなみに、19行目のMyFloat(-10.0)という表記は-10.0MyFloat型へキャストしている。

interface型を用いる事で、ある関数の引数が特定の振る舞いを持つ(特定のメソッド呼び出しに応える)事を型レベルで静的にチェック出来る様になり、コンパイラの力を借りて開発を行う事が出来る。実行時のエラーを減らす事にも繋がり、とても良い機能だと思う。

実際、デザインパターンの文脈なんかで多くの場合に必要とされるのは「特定の振る舞いを持つ」事であり、Javaのinterfaceも同じ目的で使われる。ただ、Goにおいてはとてもシンプルな構文とシンプルなルールでinterface型が定義出来て、それが良いのだと思う。

interface型の考え方はHaskellの型クラスにもものすごく近い気がする。まあ、Haskellの場合はメソッドじゃなくて関数な訳だけど、目指すところは同じなのでは。

気づいたら夜遅くになってた

明日は早いので寝る....またもや中途半端.....悲しい....

どうでも良い話

昨日書いた設定でvimGo書いてるとファイル保存時に:Fmtが走るんだけど、その度にコードが整形されてちょっと楽しい。ただ、人の書いたコード写経してて先にimport文書いてから途中で保存したりすると、まだ使われてないパッケージは消されてしまったりしてちょっと悲しい。逆に、コード中で新しくパッケージ使ったら勝手にimport文の中に追記されるので、import文は自分で書くものでは無いのかもしれない。