Go言語は標準の net/http が結構よくできてるので、WEBフレームワークはなくてもいいだとかそういう話がありますし、 net/http をラップした俺俺フレームワークが大量に作られています。

というわけで、俺も遅ればせながら俺俺フレームワークを作りました。

読み方は「シードル」。お酒の名前つけるのが慣習ですからね。よくあるこんな感じ。

package main

import (
  "github.com/yuin/cidre"
  "net/http"
)

func main() {
    app := cidre.NewApp(cidre.DefaultAppConfig())
    root := app.MountPoint("/")

    root.Get("show_welcome", "welcome", func(w http.ResponseWriter, r *http.Request) {
        app.Renderer.Text(w, "Welcome!")
    })

    app.Run()
}

特徴

  • よくあるSinatraチックなAPI
  • できるだけ標準インタフェースを使用。いろんな既存ライブラリとの相性が良い。
  • 他の薄いフレームワークではオプションな機能も一部内包。

    • セッション、フラッシュメッセージ
    • レイアウト機能をサポートした html/template のラッパー
  • フック機能を提供していて、より柔軟に外部から拡張可能。

開発経緯

そもそもPythonistaの御多聞にもれず、2と3のはざまでもだえる中でGo言語書くことが多くなってたんですね。んでWEBもGo言語でさらっと書きたい、と。

Go言語のWEBフレームワークはいっぱいあって、軽量だとMartiniだとかGinだとかnegroniだとか、重量級だとbeegoだとかrevelだとか。俺の好みとしてやっぱりシンプルなものが好きなので軽量フレームワークを使いたいところだけど、Martiniはtoo magicだし、Ginは40 times fasterってのが詐欺っぽいし、negroniはツールであってフレームワークじゃないと言っているしで、あんまりしっくりくるのがありませんでした。

なので自分が最低限必要と思う機能を組み込んだフレームワークを作ったわけです。ミドルウェアで対応できるけど組み込まれてたほうが楽だし。SPAが流行ってるって言ってもさらっと作るときはフラッシュメッセージが楽だし、設定は外出ししときたいし、とか。

基本的に http.Handler (もしくは http.HandlerFunc )で構成されるので他のいわゆる「ミドルウェア」と呼ばれているものもすんなり組み込めます。せっかく組み込みライブラリがよくできてるんだから、なるべくフレームワーク特有のことは覚えたくないのもある。

テンプレートエンジンやSessionストアやロガーはInterfaceなので差し替え可能です。

あと、適当に今風のプロジェクトページ作りました。

開発中に思ったこと

俺俺フレームワークを書きたい方のために、cidreを書いてる時に思ったこと、検討したことを書いておきます。

Contextの持ち方

どのフレームワークも Context という構造体がだいたいある。これは入れ子になってる http.Handler 間でデータを受け渡すのが主目的だ。

でContextの考え方は3種類ある。

  • 1. http.Handler インタフェースを使わず独自インタフェースをつくって引数として渡す。
  • 2. Gorilla context のようなスレッドローカル変数を使う。
  • 1. http.Handler#ServeHTTP の引数である http.ResponseWriterhttp.Request のどこかに埋め込む。

1はGinやnegroniなど大半のフレームワークが採用している方法。これはこれでシンプルでよい。ただし独自インタフェースになる。

2はスレッドローカルにするためにgoなのにLock, Unlockが走りまくるのが難点だが見た目すごくクリーン。

3は生成時にトリックが必要だけど標準インタフェースを使えて、ロックも発生しない。というわけでcidreは3の方式をとっている。Goで外部からオブジェクトを埋め込むためには

  • それがインタフェースで
  • Public

じゃないといけない。というわけで http.Request#Body に埋め込んでいる。

拡張性

Writing HTTP Middleware in Go という記事があるように、 http.Handler をPythonのWSGIミドルウェアのように扱う、というのは標準的な考え方だろう。

ただ、結局この方式はただのフィルタであって柔軟性がない。HTTPボディを書く前に処理を差し込みたい、とかできない。正確にはできないことないけどめんどくさい(独自ResponseWriterを作って次のミドルウェアに渡すことになる)。Martiniでは独自ResponseWriterにコールバックが設定できるようになっていてHTTPボディ書く前にヘッダ書くというのができる。

でも結局そういうポイントって随所にあって、統一的に扱える仕組みがあったほうがよいと思う。のでHookの仕組みをつくってサーバ起動時、とかいろんなところをフックできるようにしてある。

設定オブジェクト

これは何を今さら、な話で設定を表すオブジェクトをどう扱うか、ということ。例えば以下のようなstructがあるとして

type Config struct {
    Host string
    Port int
    Timeout time.Duration
}

これにどうデフォルト値を適応するかっていうこと。

スクリプト言語ならundefinedなりnullなりnilなり未初期化を表す共通の値があるので、よいのだがCやGoではintは初期値0だし、0と設定したのか未設定なのかわからない。

なので以下のようなデフォルト設定を返す関数をつくってそれに設定を追加していく形がよいと思う。

func DefaultConfig(init ...func(*Config)) *Config {
    self := &Config {
        Host: "localhost",
        Port: "8080",
        Timeout: 180 * time.Second,
    }
    if len(init) > 0 {
        init[0](self)
    }
    return self
}

config := DefaultConfig()
config.Timeout = 0

あと利便性のためこういう書き方もできるようにしてある。

app := NewApp(DefaultConfig(func (config *Config){
    config.Timeout = 0
})

といろいろあるけどこんなところで。

今後について

今後も細々メンテしていくつもりですし、ミドルウェアなんかも追加していきたいなあと思っています。やっぱGo言語はさらっと書けるそれなりに速いし、いいっす。


フロントエンド開発者向けのAlfred Workflow という記事を見かけたので、それ iceberg でもできる かも よ!ということでいくつか作ってみました(仕様上実装できないものもあったのでiceberg自体も改修して v0.9.4としてリリース しています)。

簡単にできそうなものをピックアップしたつもりが以外と苦労したかもしれない…

iceberg-ip

image

IPアドレス一覧を表示するコマンド。

iceberg-worldtime

image

世界主要都市の現在時刻を表示する。頑張ってサマータイム対応したのだけど、これがめんどくさかった…

iceberg-encodedecode

image

Base64, URL, HTML entityでのエンコード結果を表示。

というわけで

これらのコマンドはLuaのみで書かれています。

以下の weather コマンドのようにHTTPで結果を取得してそれをリストに表示することもできます。加えてmigemoに対応しているので絞り込みも簡単です。

image

Windows7以降はOS自体にWin+Rより高度なコマンドラインランチャー的な機能が内蔵されたこともあって、こういうのがあるとコマンドラインランチャを使う意義みたいなのも高まるかな、と。


ようやく自宅サーバや開発環境仮想マシンの構築手順を Ansible 化した。

この手のツールはハマりポイント+よく使うイディオムを抑えるのが大事。Pythonプロダクトらしく非常にドキュメントが充実しているのだけど、充実しすぎているのでポイントを自分のためにもまとめておくことにする。

例はおもに Ansible のドキュメントから引用させていただきました。

設定ファイルを書きかえる(一行)

lineinfile モジュールを使う。使い方は以下のような形になる。

name: enalbe sudo without password if user belongs to the wheel group
lineinfile: "dest=/etc/sudoers state=present regexp='^%wheel' line='%wheel ALL=(ALL) NOPASSWD: ALL'"

stateabsent にすれば削除することができる。動きとしては regexp にマッチする行をみつけたら line に書きかえる、という動作になる。またデフォルトでは regexp にマッチする行がない場合最終行に追加される。

上記のほかにも regexp にマッチする業の前後に追記するなど柔軟な動作が可能。

ファイルコピーを行う(再帰的)

copy モジュールを使えばファイルコピーできるのは当たり前なのだが、じゃあディレクトリの場合どうするかというとてっとりばやくは以下のようにするとよい。

shell: rsync -a /path_to/source/  /path_to/dest/ creates=/path_to/dest/hoge

このとおり、 rsync してしまうのがよい。上記はターゲットマシン上どうしでの rsync で、playbookを実行しているホスト→ターゲットマシンで実行する場合は以下。

local_action: command rsync -a /path_to/source/ {{ inventory_hostname }}:/path_to/dest/

local_action を使うとターゲット上でなくplaybookを実行しているホスト上でコマンドを実行できる。

make installする

make install するソフトウェアをどうするか。一つ目のパターンはよく見る形で以下のように1個ずつタスクにする。

- name: "wget hoge src"
  command: wget -O http://example.com/hoge.tar.gz  creates=hoge.tar.gz

- name: "expand src"
  command: tar xvfz hoge.tar.gz creates=hoge

# 続く…

私の場合こっちというのは以下のような形で1個にまとめてしまう。たかが make install に上記のようにつらつらタスクを書きまくるのはしんどいので。

name: install python3.3
shell: >-
  wget http://www.python.org/ftp/python/3.3.2/Python-3.3.2.tgz &&
  tar zxvf Python-3.3.2.tgz &&
  rm -f Python-3.3.2.tgz &&
  cd Python-3.3.2 &&
  ./configure --prefix=/usr/local/python3.3.2 --enable-shared &&
  make &&
  paco -D make install
  chdir=/usr/local/src creates=/usr/local/python3.3.2/bin/python3.3

もちろん、途中でこけると中途半端なことになるのだがそうそうコケないのでこれでよいと思っている。

変数を使いこなす

ターゲット固有の情報はデフォルトで収集される。内容を見たければ ansible hostname -m setup と実行すればよい。

他のホストの情報は以下のようにすればアクセスできる。

{{ hostvars['test.example.com']['ansible_distribution'] }}

変数は実行時にコマンドラインオプションで上書きできる。

ansible-playbook release.yml --extra-vars "version=1.23.45 other_variable=foo"

ループを使いこなす

Ansibleは結構複雑なループが使える。ただ実用的な範囲で言うと以下ぐらいを抑えておくとよいのでは。

with_items による単純ループ

name: add several users
user: name={{ item }} state=present groups=wheel
with_items:
   - testuser1
   - testuser2

with_items + 辞書によるループ

name: add several users
user: name={{ item.name }} state=present groups={{ item.groups }}
with_items:
  - { name: 'testuser1', groups: 'wheel' }
  - { name: 'testuser2', groups: 'root' }

コマンド実行結果を with_items で回す

- name: retrieve the list of home directories
  command: ls /home
  register: home_dirs

- name: add home dirs to the backup spooler
  file: path=/mnt/bkspool/{{ item }} src=/home/{{ item }} state=link
  with_items: home_dirs.stdout_lines
  # same as with_items: home_dirs.stdout.split()

with_sequence による整数範囲ループ(Pythonの range のようなもの)

file: dest=/var/stuff/{{ item }} state=directory
with_sequence: start=4 end=16 stride=2

do-until(+sleep)ループ

action: shell /usr/bin/foo
register: result
until: result.stdout.find("all systems go") != -1
retries: 5
delay: 10

ファイルglobでループ

copy: src={{ item }} dest=/etc/fooapp/ owner=root mode=600
with_fileglob:
  - /playbooks/files/fooapp/*

条件分岐を使いこなす

これまた条件分岐もいろいろできることはあるのだが、以下のパターンを抑えておくとよいと思う。

単純な変数による分岐

- name: "shutdown Debian flavored systems"
  command: /sbin/shutdown -t now
  when: ansible_os_family == "Debian"

コマンド実行結果による分岐

tasks:
  # まずはコマンドを実行してresultに格納
  - shell: /usr/bin/foo
    register: result
    ignore_errors: True

  # タスクが失敗した場合
  - debug: msg="it failed"
    when: result|failed

  # タスクにより更新された場合
  - debug: msg="it changed"
    when: result|changed

  # タスクが成功した場合
  - debug: msg="it succeeded"
    when: result|success

  # タスクがskipされた場合
  - debug: msg="it was skipped"
    when: result|skipped

  # タスクの標準出力によって分岐
  - shell: echo "hi does not found"
    when: result.stdout.find('hi') != -1

複数ターゲットが関連するタスクを実行する

ローリングアップデートのように複数ターゲットが連動して動くタスクがある。その場合、playbookを実行するマシンを核としてタスクを作ればよい。

- hosts: webservers
  serial: 5

  tasks:
    - name: take out of load balancer pool
      local_action: command /usr/bin/take_out_of_pool {{ inventory_hostname }}

    - name: actual steps would go here
      yum: name=acme-web-stack state=latest

    - name: add back to load balancer pool
      local_action: command: /usr/bin/add_back_to_pool {{ inventory_hostname }}

local_action モジュールはplaybookを実行しているホストで実行するコマンドを定義する。上記の例だとWEBサーバを5並列でLB切り離し→更新→LB組み込みを実行する。


この程度を理解していればだいたいやりたいことはできるはず。あとはモジュール一覧を頭にいれるだけですね。ついつい shell でゴリ押ししそうになるのでぐっとそこをこらえて…