go言語snippetと注意点

以前書いたpythonicなA*関数をままgoで書いてみました。

そのとき調査で理解したものを紹介します。

0値

goの0値(初期値を設定しないときのデフォルト値)がどうなるか、そしてどう比較するかの例です。

package main

import "fmt"

type location struct {
        x, y int
}

func main() {
        var v location;
        fmt.Printf("%v\n", v);
        //fmt.Printf("%v\n", v == location{}); // not allowed
        fmt.Printf("%v\n", v.x == 0 && v.y == 0);

        var a *location;
        fmt.Printf("%v\n", a == nil);
        var b []location;
        fmt.Printf("%v\n", b);
        fmt.Printf("%v\n", b == nil);
        var c string;
        fmt.Printf("%v\n", c);
        fmt.Printf("%v\n", c == "");
        var d map[string]float;
        fmt.Printf("%v\n", d);
        fmt.Printf("%v\n", d == nil);
}

大体予想通りです。ただ、struct同士は==で結べません。各メンバーが0値かどうかを比較することになるでしょうか。

nilのメソッド呼び出し

structのポインタはnilでもメソッドが呼び出せます。

package main

import "fmt"

type foo struct {
        val int;
}
func (obj *foo) IsNil() bool {
        return obj == nil;
}
type nilable interface {
        IsNil() bool;
}

func main() {
        var o *foo;
        fmt.Printf("%v\n", o.IsNil());
        //var i nilable;
        //fmt.Printf("%v\n", i.IsNil());
}

interfaceがnilの場合、メソッドが解決できずエラーが起きて死にます(コレは正しい動きなのでしょうか?0値返すべきじゃないか、とも思うけど)。

for rangeループ

rangeループで何が返るかを調べました。

package main

import "fmt"

func main() {
        s := "abcdef";
        for _, c := range s {
                fmt.Printf("%f\n", c);
                fmt.Printf("%f\n", c == 'd');
        }

        a := [...]string {
                "abc",
                "def",
                "ghi",
        };
        for _, l := range a {
                fmt.Printf("%f\n", l);
                fmt.Printf("%f\n", l == "ghi");
        }

        m := map[string]int {
                "abc": 10,
                "def": 20,
                "ghi": 30,
        };
        for k, v := range m {
                fmt.Printf("%f\n", k);
                fmt.Printf("%f\n", v);
                fmt.Printf("%f\n", v == 20);
        }

        g := func(size int) <-chan int {
                ch := make(chan int);
                go func () {
                        for i := 0; i < size; i++ {
                                ch <- i * 2;
                                ch <- i * 2 + 1;
                        }
                        close(ch);
                }();
                return ch;
        };
        for v := range g(3) {
                fmt.Printf("%f\n", v);
        }

}
  • 文字列と配列は、インデックスと要素
  • マップは、キーと値
  • channelは、値のみ

最後のはpythonのgenerator風にgoroutineを使っています。

def g(size):
    for i in range(size):
        yield i * 2
        yield i * 2 + 1

for v in g(3):
    println(v)

channelの型

  • "chan T"がchannelの型です。makeで使うし、送受信可能です。
  • "<-chan T"は受信専用のchannel型です。
    • イテレータ的にchannelを用いる場合、この型を返すといいでしょう。
  • "chan<- T"は送信専用のchannel型です。
ch := make(chan T); // type(ch) == chan T
var receiver <-chan T = ch;
var sender chan<- T = ch;

普通は送信か受信専用として受け渡ししてもらうのがわかりやすいでしょう。

vectorの使い方

package main

import "fmt"
import "container/vector"

type ilocation interface {
        String() string;
}
type location [2]int
func (loc *location) String() string {
        return fmt.Sprintf("%v", *loc);
}

func main() {
        v1 := vector.New(0); // v := vector.New(len) => v.Len() == len
        v1.Push(&location{5, 20});
        v1.Push(&location{50, 10});


        fmt.Printf("%v\n", v1);
        v1.Do(func (elem vector.Element) {
                fmt.Printf("%v\n", elem);
        });
        v1.Do(func (elem vector.Element) {
                fmt.Printf("%v\n", elem.(ilocation).String());
        });
        for _, elem := range v1.Data() {
                fmt.Printf("%v\n", elem.(*location).String());
        }
        for _, elem := range v1.Data() {
                fmt.Printf("%v\n", elem.(ilocation).String());
        }
        // ptr array cast not allowed?
        //for _, elem := range v1.Data().([]ilocation) {
        //      fmt.Printf("%v\n", elem.String());
        //}

        v2 := v1.Slice(0, v1.Len()); // copy
        v2.Set(0, &location{0, 0});
        fmt.Printf("%v\n", v1);
        fmt.Printf("%v\n", v2);
}

型変換

intからfloat64へなどの型変換は、以下のように行います。多くの場合、暗黙に変換されません。

a := 10; // int
b := float64(a); // float64 

heapパッケージ

pythonのheapqのようなことができるheapパッケージがあります。
ただし、キュー本体になるために必要なheap.Interfaceのメソッド群は、Vectorがほぼ同じものを持っているのですが、型などに微妙に違いがあるため、インタフェースをすりあわせる必要があり、使い方は若干複雑です。

import "fmt"
import "container/vector"
import "container/heap"

type scored struct {
        score float;
        data string;
}

type heapq struct {
        vector.Vector;
}
func (queue *heapq) Init(len int) *heapq {
        queue.Vector.Init(0);
        return queue;
}
func (queue *heapq) At(i int) *scored {
        return queue.Vector.At(i).(*scored);
}
func (queue *heapq) Less(i, j int) bool {
        return queue.At(i).score < queue.At(j).score;
}
func (queue *heapq) Push(o interface {}) {
        queue.Vector.Push(o.(vector.Element));
}
func (queue *heapq) Pop() interface {} {
        return queue.Vector.Pop();
}

func main() {
        queue := new(heapq).Init(0);
        println("ok0");
        heap.Init(queue);
        println("ok1");
        heap.Push(queue, &scored{10.0, "go"});
        println("ok2");
        heap.Push(queue, &scored{5.0, "java"});
        heap.Push(queue, &scored{7.0, "python"});
        heap.Push(queue, &scored{1.0, "c++"});
        heap.Push(queue, &scored{3.0, "c"});

        println("ok3");
        for queue.Len() > 0 {
                data := heap.Pop(queue).(*scored);
                fmt.Printf("%v\n", data);
        }
        println("ok4");
}

Vectorの要素が、vector.Elementじゃなく、interface{}だったらPush/Popの上書きいらないのに。

newとComposite literal

type Foo struct { .... }

という型について、二つのインスタンス生成式

&Foo{}

new(Foo)

はまったく同じものです。前者がスタック上に詰まれるとか、ループ中でメモリ領域が再利用されるとか、そういうことはありません

前者のリテラル形式(から参照を得る記法)では、初期データを入れられたりして便利です。しかし、このリテラル形式では生成できない型があります。たとえばポインタ型とか。そういう場合にはnewしか手段がありません。

課題

pythonとgoとでのA*探索を書いてくらべてみると、増えてる部分の多くが組み込みTupleが無いことに由来する感じがします。
tuple的ペアを作成し、vectorの大小比較を再実装したりします。これは、pythonのtupleでは自然に備わってる機能、同値判定、大小判定、mapキー利用可能性が無いからだと思います。


ほかの差では、

  • comprehensionがなく、メモリ確保後にループしなくてはいけないこと
  • べき乗演算子が無くmath.Powやmath.Sqrtをつかわなくてはいけないこと
  • リテラルはfloatだけど、mathの関数の引数や結果でつかわれてるのはfloat64であること
  • オペレータオーバーロードがないため、スコアを抽象化しようとすると、逆に単純な数値が使いにくくなること
  • interfaceに多相型ができないため、コールバックでtype assertion(ダイナミックキャスト)を多用しなくてはいけないこと

などが気になったところです。そういう点を除いてもC、Javaに比べればだいぶコンパクトに書けると思いました。

増えてる多くはtuple代わりに用意した型に関係するものなので、goに慣れればもう少し書く量を減らせるかもしれません(interfaceにジェネリクスが実装されれば、tupleライブラリができるんだろうけど...)。