Go言語で幸せになれる10のテクニック

  • 776
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

Go近辺を徘徊していて見つけたブログポスト。

Ten Useful Techniques in Go

Goな方々には常識なのかも知れないけど、Go初心者の私にとっては面白かったのでちょっとまとめてみる。

紹介されているのは以下の10個の項目。

  1. Use a single GOPATH
  2. Wrap for-select idiom to a function
  3. Use tagged literals for struct initializations
  4. Split struct initializations into multiple lines
  5. Add String() method for integers const values
  6. Start iota with a +1 increment
  7. Return function calls
  8. Convert slices,maps,etc.. into custom types
  9. withContext wrapper functions
  10. Add setter, getters for map access

それぞれ、内容の簡単な紹介と一言コメントしていこう。

GOPATHは一つだけ (Use a single GOPATH)

GOPATHはimportでパッケージを探しに行く時のサーチパスを指定する環境変数。通常のPATH環境変数と同じようにコロン(Windowsの場合はセミコロン)で複数指定できる。Go getで入手したパッケージは指定された最初のディレクトリに入る。

そもそも複数指定する場合があまり思いつかないが、「大規模プロジェクトでホントに必要になるまでは止めた方がいい」とのこと。おとなしく従います。

一点だけ、node.js(npm)だと、プロジェクト毎にパッケージのディレクトリ(node_modules/)が出来てそこにインストールのがデフォルトになっている。設計思想の違いってやつか。

For文の中でSelectを使う時は関数に (Wrap for-select idiom to a function)

For文の中でSelectを使ってチャネルからの入力を待つ。元blogの例があまり正しく動かないようだったのでチョイ変してみた。

func main() {

L:
    for {
        select {
        case t1 := <-time.After(time.Second):
            fmt.Println("hello", t1)
            if t1.Second()%4 == 0 {
                break L
            }
        }
    }

    fmt.Println("ending")
}

一秒ごとにタイムスタンプをもらい、秒が4の倍数なら止まるというコード。ここでラベル("L")を使ってBreakしているのは単純にBreakするとselectを抜けるだけでFor文を抜けられないから。

正しいラベルの使い方だが、「嫌い」だと。Gotoを忌み嫌う様に教えられた世代からすると判る気がする。で、代わりに以下を推奨。

func foo() {
    for {
        select {
        case t1 := <-time.After(time.Second):
            fmt.Println("hello", t1)
            if t1.Second()%4 == 0 {
                return
            }
        }
    }
}

func main() {
    foo()
    fmt.Println("ending")
}

要は、関数で包んでおいて、抜ける時は一気にReturnで抜ける。結局同じじゃないかと思うけど、Goto感が少し減るのはわかる。

構造体を初期化する時は属性名を指定しておこう (Use tagged literals for struct initializations)

例えばこういう構造体があった場合、

type T struct {
    Foo string
    Bar int
}

この初期化は以下の2つどちらでも同じ。

t := T{"example", 123}
t := T{Foo: "example", Bar: 123}

前者は順序による初期化で後者は属性名を明記しての初期化。後者だと、こういう風にもできる。

t := T{Bar: 123, Foo: "example"}

違いが出るのは構造体にメンバ追加した時で、例えば、

type T struct {
    Foo string
    Bar int
    Qux string
}

とした途端、順序による初期化は「値が足りないよ」というコンパイルエラーになる。一方、後者は何事もなくコンパイルでき、動作もする。

知らない間に構造体が変更されたり、自分で追加したのに初期化し忘れというのを避ける意味ではエラーにしてくれた方が良い気もするけど、過去互換性の観点からそう決められているらしい (http://golang.org/doc/go1compat)。

構造体の初期化は複数行に分けておこう (Split struct initializations into multiple lines)

同じく構造体の初期化ネタだが、こうじゃなくて

T{Foo: "example", Bar:someLongVariable, Qux:anotherLongVariable, B: forgetToAddThisToo}

こう書こうという話。

T{
    Foo: "example",
    Bar: someLongVariable,
    Qux: anotherLongVariable,
    B: forgetToAddThisToo,
}

まぁ、見やすさの点では当たり前といえば当たり前。気をつけなければならないのは、後者は最後のエントリにも","が要ること。

整数定数値にはString()メソッドを定義しておこう (Add String() method for integers const values)

例えば、こういう列挙型を定義してみる。

type State int

const (
    Running State = iota
    Stopped
    Rebooting
    Terminated
)

で、それをPrintしてみると

func main() {
    state := Running
    fmt.Println(state) // --> "0" が表示される
}

これは判りにくいので、こういう関数を用意する。

func (s State) String() string {
    switch s {
    case Running:
        return "Running"
    case Stopped:
        return "Stopped"
    case Rebooting:
        return "Rebooting"
    case Terminated:
        return "Terminated"
    default:
        return "Unknown"
    }
}

すると、"0"の代わりに"Running"と表示される。

うーむ、これ自分で一々書かないといけないのはあまりに残念だなぁ。将来的に言語処理系のサポートを期待。でもそういうのって「Goっぽくない」って言われちゃうのかな。

iotaを使う時は+1してから (Start iota with a +1 increment)

上の例の続きになるが、Stateを含んだこういう構造体を考えてみる。

type T struct {
    Name  string
    Port  int
    State State
}

それを使ってみる。

func main() {
    t := T{Name: "example", Port: 6666}
    fmt.Printf("%+v\n", t) // --> "{Name:example Port:6666 State:Running}"と表示
}

おっと、Stateの初期化を忘れた。が、StateがRunningになってしまっている。これはイカン。

なぜこうなるかというと、定数を初期化するのに使うiotaが0から始まるから。整数の未初期化時のデフォルト値と一致してしまっている。

これを避けるためには定数の割り当てを1からにすれば良い。

const (
    Running State = iota + 1
    Stopped
    Rebooting
    Terminated
)

これで "Running"の代わりに"Unknown"が表示される。あるいは、定数の定義をこうしておくのも別の手としてある

const (
    Unknown State = iota 
    Running
    Stopped
    Rebooting
    Terminated
)

んー、なんかGoのEnumって使いにくそう。。。

関数呼び出しでそのまま返しちゃおう (Return function calls)

こんな感じで一々内部呼び出しの結果をエラーチェックして返していたりするが、

func bar() (string, error) {
    v, err := foo()
    if err != nil {
        return "", err
    }

    return v, nil
}

普通にこう書けばいいんじゃないの? という話。

func bar() (string, error) {
    return foo()
}

これ当たり前過ぎてなにがポイントなのかわからない。Node.jsみたいに「Callbackの最初の引数はError」みたいなルールが決まっていなくて、一々チェックしがちなのかな。

SliceとかMapとか直接返さずに、型定義しちゃおう (Convert slices,maps,etc.. into custom types)

サーバのリストを返す関数を考える。

type Server struct {
    Name string
}

func ListServers() []Server {
    return []Server{
        {Name: "Server1"},
        {Name: "Server2"},
        {Name: "Foo1"},
        {Name: "Foo2"},
    }
}

名前でフィルタリングする機能を追加しようとすると、ListServers()に引数追加して、その中に機能を盛りこまなければならない。さらに別の機能を追加しようとするともう収拾がつかなくなる。

そんな時は、スライスを型定義してしまい、それにバインドした関数を複数用意すれば良い。

type Servers []Server

func (s Servers) Filter(name string) Servers {
 // filterした結果を返す
}

これもどうなのかな。Pythonとかだと普通にそういう関数を用意して使うだけなのだが、ちょっと面倒。(Pythonだと組み込みでfilterという関数があるのでそれを使うだけだが)

共通作業をまとめちゃおう (withContext wrapper functions)

各メソッドで共通な作業はまとめて関数を用意しておいてそれを使うだけにしようという話。ここでは排他制御の例が出されている。

func foo() {
    mu.Lock()
    defer mu.Unlock()

    // foo 関連の処理
}

func bar() {
    mu.Lock()
    defer mu.Unlock()

    // bar 関連の処理
}

func qux() {
    mu.Lock()
    defer mu.Unlock()

    // qux 関連の処理
}

これは無駄が多いし、変更時の影響範囲も大きいので、こうしてみる。

func withLockContext(fn func()) {
    mu.Lock
    defer mu.Unlock()

    fn()
}

func foo() {
    withLockContext(func() {
        // foo 関連の処理
    })
}

func bar() {
    withLockContext(func() {
        // bar 関連の処理
    })
}

func qux() {
    withLockContext(func() {
        // qux 関連の処理
    })
}

要は、pythonで言うところの、decorator関数だ。そしてPythonだともっとスッキリ書けるのになぁ。

Mapへのアクセス関数を用意しておこう (Add setter, getters for map access)

Mapはスレッドセーフじゃないので、複数スレッドからアクセスされる場合には排他制御する必要がある。各々使う場所でやるよりも、こんな感じでアクセス関数群を用意しておいた方が良いよね、という話。

type Storage interface {
    Delete(key string)
    Get(key string) string
    Put(key, value string)
}

まぁ、そうだろうね。しかし複数スレッドが同じデータを読み書きするのってどれくらいあるんだろう。Goroutine間ではチャネルでの通信だったはずだが。よくわからん。

まとめ

Go初心者がGoの世界を少しだけ垣間見た。あまり腑に落ちていないのもあるけど、もう少ししてから見なおしてみると違って見えるのかな。