Go-GTKでCUIとGUIをつなぐ的なアプリを書いてみた

プログラマーは往々にしてCUI世界の住人なのですが、

ごくたまに、ファイルをドラッグ&ドロップで指定できると便利、ということがあります。

doloopwhile/dap · GitHub

スクリーンショット

f:id:doloopwhile:20141005180042p:plain

インストール

go get github.com/doloopwhile/dap

使い方

ドラッグアンドドロップでファイルをバックアップ・ディレクトリにコピーする

mkdir -p $HOME/backup
dap | xargs cp {} $HOME/backup

なぜ作ったか

Debian系OSには(dragbox)https://packages.debian.org/ja/sid/dragboxというツールがあるようです。しかし、どういうわけか私のXubuntuでは動きませんでした。

そこで、(Go-GTKのD&Dの例)https://github.com/mattn/go-gtk/blob/master/example/dnd/dnd.goを元に作ってみました。Go-GTKは、デモが充実しており、実用的に使えそうな段階に入っているようです。

Goライブラリを作ったら、とりあえずgodocdown

小さなGoライブラリをつくった時、ほとんど自分しか使わないような規模なら、わざわざREADMEに「Usage」「Example」「Functions」なんて書くのは面倒ですよね。

そんな時は、godocdownで自動生成してしまうのがオススメです。 godocと同等の内容をGitHub用Markdownで生成してくれます。

例:doloopwhile/go-merror · GitHub

go get github.com/robertkrimen/godocdown/godocdown
godocdown > README.md

まぁ、わざわざgodocdownでREADMEを作らなくても、 ほっておいてもgodocは、自動で生成されるのですが、READMEを空っぽにしておくのも寂しいので。

地味に待ち遠しいDockerの新機能

DockerfileのENV コマンドで、複数環境変数を一度に指定できるようになるらしい。 Allow ENV to set multiple variables in one layer · Issue #2333 · docker/docker · GitHub

従来は複数環境変数を設定するには、ENVを繰り返さなければなりませんでした。

ENV GOPATH /root/go
ENV CGO_CFLAGS-I/opt/fzero/include
ENV CGO_LDFLAGS -L/opt/fzero/lib64 -Wl,-rpath=/opt/fzero/lib64

この場合、各ENVごとにレイヤーが生成されてしまい、docker pushの際に通信回数が増えるなど無駄でした。

それが1行で書ける=1レイヤーで済むようになるらしいです。 イメージ:

ENV GOPATH=/root/go CGO_CFLAGS=-I/opt/fzero/include CGO_LDFLAGS="-L/opt/fzero/lib64 -Wl,-rpath=/opt/fzero/lib64"

Goでも独自型をrangeしたい!chanを使う

Pythonでは__iter__メソッドを定義すれば((Rubyでは.eachメソッドを定義してEnumerableをincludeすれば、C#ではGetEnumerator()メソッドを定義すれば、 PHPではIteratorを実装すれば))、 晴れて独自のコレクション型でも、foreach文などでループできるようになるわけですが、でもGoにはそんな仕組みはありません。

まあ、妥協案としては、関数を引数にとる.Eachを作ることはできます。

sum := 0
myCollection.Each(func(x int) {
  sum += x
})

「でも、ぼくがつくったさいきょうの○○型をfor - range文で使いたいんだ!」

解法:channelを返す.Iter()

知ってる人は知っている、rangeにはchannelが渡せます。 A "for" statement with a "range" clause iterates through all entries of an array, slice, string or map, or values received on a channel.

he Go Programming Language Specification - The Go Programming Language

これを利用して、deckarep/golang-setSet型は、for文で使えるように.Iter()メソッドを定義してます。

package main

import (
    "fmt"
    mapset "github.com/deckarep/golang-set"
)

func main() {
    s := mapset.NewSet()

    s.Add(7)
    s.Add(11)
    s.Add(13)
    s.Add(11) // Setだから、重複は排除される

    c := s.Iter()
    fmt.Printf("%V\n", c) // %!V(<-chan interface {}=0xXXXXXXXXXX)

    for x := range s.Iter() { // range文で使える!
        fmt.Println(x)
    }
    // 7
    // 11
    // 13
}

やりましたね!

genericsが無いGolangで独自コレクションを作る機会は少ないでしょうけど、 channelがrangeできること自体は、知っていると得かもしれませんよ。

Goによるデザインパターン - Strategy パターン (2)

Strategy設計の失敗

前回で、HTMLとテキストでレポートを出力するコードを書きました。その中でFormatter interfaceを定義し、その具象型としてPlainTextFormatter HTMLFormatterを定義するというStrategyパターンを採用しました。 しかし、気になる点が無いではありません。

package main

import (
    "fmt"
)

type Formatter interface {
    OutputStart()
    OutputHead(text string)
    OutputBodyStart()
    OutputLine(line string)
    OutputBodyEnd()
    OutputEnd()
}

type Report struct {
    Title     string
    Text      []string
    Formatter Formatter
}

func (r *Report) Output() {
    r.Formatter.OutputStart()
    r.Formatter.OutputHead(r.Title)
    r.Formatter.OutputBodyStart()
    for _, line := range r.Text {
        r.Formatter.OutputLine(line)
    }
    r.Formatter.OutputBodyEnd()
    r.Formatter.OutputEnd()
}

type PlainTextFormatter struct{}

// (メソッドの実装は中略)

type HTMLFormatter struct{}

// (メソッドの実装は中略)

Formatter interface には6つのメソッドが含まれます。しかし、この切り分け方は適切だったのでしょうか? 6つというのは多すぎる気がします。また、今後新しいフォーマットを採用する際に、不備が発覚するかもしれません(フッタ―にもタイトルを出力したくなるかも!)。

Strategyパターンを使う際は、ストラテジの範囲と渡すべきデータを見極める必要があります。

ContextをStrategyに渡す

今のコードのtitletextを直接渡す方法では、 1. Strategyのどのメソッドがどのデータを必要とするか、覚えなくてはならない 2. 必要ないと思ったデータも実は必要になるかもしれない(フッタ―にもタイトルを出力) 3. 全く新しいデータが登場するかもしれない(日付や提出者も記載したい) という問題があります。

そんな時は、Context(呼び出し側)をStrategyの引数として渡します。 Contextをそのまま渡すこともできますが(下のコードで言えば*report)、Contextもinterfaceにして渡した方がテストなどで便利になるでしょう(Report)。

// template_method.4.go
package main

import (
    "fmt"
    "time"
)

type Formatter interface {
    OutputStart(r Report)
    OutputHead(r Report)
    OutputBodyStart(r Report)
    OutputLine(r Report, line string)
    OutputBodyEnd(r Report)
    OutputEnd(r Report)
}

type Report interface {
    Title() string
    Text() []string
    Date() time.Time
}

type report struct {
    title     string
    text      []string
    date      time.Time
    formatter Formatter
}

func (r *report) Title() string {
    return r.title
}

func (r *report) Text() []string {
    return r.text
}

func (r *report) Date() time.Time {
    return r.date
}
func (r *report) Output() {
    r.formatter.OutputStart(r)
    r.formatter.OutputHead(r)
    r.formatter.OutputBodyStart(r)
    for _, line := range r.text {
        r.formatter.OutputLine(r, line)
    }
    r.formatter.OutputBodyEnd(r)
    r.formatter.OutputEnd(r)
}

type HTMLFormatter struct{}

func (*HTMLFormatter) OutputStart(r Report) {
    fmt.Println("<html>")
}

func (*HTMLFormatter) OutputHead(r Report) {
    fmt.Println("<head>")
    fmt.Printf("<title>%s</title>\n", r.Title())
    fmt.Println("</head>")
}

func (*HTMLFormatter) OutputBodyStart(Report) {
    fmt.Println("<body>")
}

func (*HTMLFormatter) OutputLine(_ Report, line string) {
    fmt.Printf("<p>%s</p>\n", line)
}

func (*HTMLFormatter) OutputBodyEnd(r Report) {
    fmt.Printf("<p>Updated: %s</p>", r.Date().Format("2006-01-02 15:04:05"))
    fmt.Println("</body>")
}

func (*HTMLFormatter) OutputEnd(Report) {
    fmt.Println("</html>")
}

func main() {
    report := &report{
        title:     "月次報告",
        text:      []string{"順調", "最高"},
        formatter: &HTMLFormatter{},
    }
    report.Output()
}

Strategyの実例

標準ライブラリのsortパッケージは、Strategyパターンの一例です。 sort.Sortsort.InterfaceというStrategyを取るようになっています。

Goのinterface - 使う側で定義する

Goのinterfaceは「使う側」が定義するもの

前回Formatter interface、Formatterを使ってレポートを出力するReport struct、 Formatterの実装PlainTextFormatterHTMLFormatterを定義しました。

type Formatter interface {
    OutputStart()
    OutputHead(text string)
    OutputBodyStart()
    OutputLine(line string)
    OutputBodyEnd()
    OutputEnd()
}

type Report struct {
    Title     string
    Text      []string
    Formatter Formatter
}

type PlainTextFormatter struct{}

// (中略)PlainTextFormatterのメソッド定義

type HTMLFormatter struct{}

// (中略)PlainTextFormatterのメソッド定義

Goのinterfaceは、Javaなどのインターフェイスや基底クラスとは異なります。

Javaインターフェイスは、具象型を定義するとき(class 〜 implements 〜)と、 変数を定義するときの両方に定義する必要があります。 一方、Goでは具象型の定義ではinterfaceの指定の必要はありません。

Go Java
具象型の定義 なし! class PlainTextFormatter implements Formatter {}
変数定義 var fmt Formatter = &PlainTextFormatter{} Formatter fmt = new PlainTextFormatter();

Goのinterfaceは純粋に変数(受け手・使用する側)での型を指定するものです。 その点、interfaceC++のTraitsや関数型言語での型クラスに似ており、 interfaceを使ったコードは総称型プログラミングのコードだとと言えます。

interfaceのtips

interfaceは「使用する側」のためのものですから、すべてのメソッドを列挙する必要はありません。必要なものだけ指定すればよいのです。

// Formatterの必要なメソッドだけ取り出したinterface
type HeadFormatter interface {
    OutputStart()
    OutputHead(text string)
}

type HeadReport struct {
    Title     string
    HeadFormatter HeadFormatter
}

r := &Report{}
r.HeadFormatter = &PlainTextFormatter{} // 当然、代入できる

また、interfaceは「使用する側」のためのものですから、1回しか使わないinterfaceには名前を付けずに済ませることができます。

type HeadReport struct {
    Title     string
    HeadFormatter interface {
        OutputStart()
        OutputHead(text string)
    }
}

この、1. 必要なメソッドだけを2.無名interfaceとして定義する 方法は積極的に使うべきです。たとえば以下のような効能があります。

  • 具象型を定義しているパッケージをimportする必要が無くなり、コードが粗結合になる
  • メソッド数が最小限になるので、具象型を実装しやすい
  • 特にテスト時にモックオブジェクトを作りやすくなる

もちろん、http.ResponseWriterのような頻出・メソッド数が多いinterfaceは型を定義する側が提供した方が良いこともあります。

Goによるデザインパターン - Strategy パターン (1)

例:レポートを出力するクラス。

レポートをHTMLで出力するstruct*1を作ったあなた。上司からプレーンテキストでも出力してくれと言われてしまいました。とりあえず、フォーマットを引数で指定するようにしたのですが・・・

*1:レポートの内容はフィクションです

続きを読む