Go ライブラリによる CGIプログラム内ルーチング

共有レンタルサーバではcgiでしか動かせない場合があります。webアプリのgoサーバ配下版とapache版 cgiでは同じ様にプログラミングできるのかちょっと不安だったので、とりあえずルーチング(プログラム内での分岐・ページ処理起動)に関して調べてみました。  

対象としたライブラリは、他と組み合わせて使うタイプ、かつ標準ライブラリと互換性が高いと言われる、次です。

      bmizerany/pat・・・シンプル。ソースコードは200行ぐらい
      httpRoute    ・・・軽量・高速がうり
      gorilla/mux  ・・・多機能がうり
      routes       ・・・patライクなインタフェース。静的ファイル他に対応

前提として、サーバ側では

* apacheの動いているサーバで、ドキュメントルート以下のどこにもcgiファイルを置けるようになっていて、

* apacheのhttpd.conf は編集できないとしても、はじめから

```
        DocumentRoot "/var/www/html"
        <Directory "/var/www/html" >
            AllowOverride All
            Require all granted
            Options ExecCGI FollowSymLinks
        </ Directory>
```

みたいになってるとし、以降ではディレクトリ /var/www/html/cgi0/ に cgiファイルを置くものとします。



標準ライブラリでのルーチング
------------------------------
```go:routeStd.go
package main
import (
        "fmt"
        "net/http"
        "net/http/cgi"
)
func viewHandler( w http.ResponseWriter, r *http.Request ){
    w.Header().Set( "Content-Type","text/plain; charset=utf-8" )
    fmt.Fprintln( w,"-------- Standard Library ---------" )
    fmt.Fprintln( w,"Method:", r.Method  )
    fmt.Fprintln( w,"URL:", r.URL.String() )
    fmt.Fprintln( w,"URL.Path:", r.URL.Path  )
    fmt.Fprintln(w, "hello:", name )
}
func main(){
    http.HandleFunc( "/cgi0/",viewHandler ) // 登録A
    cgi.Serve( nil )
}
```

として、コンパイル(go build -ldflags "-s -w" -o routeStd.cgi routeStd.go )した routeStd.cgiを /var/www/html/cgi0/ に置きます。


１．ローカルのブラウザから "http://サーバのIPアドレス/cgi0/routeStd.cgi" をアクセスすれば、次が出力されます(192.168.1.10は my環境でのサーバアドレス)。

        -------- Standard Library ---------
        Method: GET
        URL: http://192.168.1.10/cgi0/routeStd.cgi
        URL.Path: /cgi0/routeStd.cgi


２．様々なURLリクエストを１つの cgiファイルで受けとれるように、.htaccessで mod rewriteしてみます。たとえば  /var/www/html/cgi0/.htaccess で

        RewriteEngine On
        RewriteBase /
        RewriteRule ^(cgi01|cgi02|cgi03)/(.*)   cgi0/routeStd.cgi

として ".../cgi0/cgi02/dummy/"をアクセスすれば、次が出力されます。

        -------- Standard Library ---------
        Method: GET
        URL: http://192.168.1.10/cgi0/cgi02/dummy/
        URL.Path: /cgi0/cgi02/dummy/

３．mod rewriteではなく、リンクを作ってリクエストを１つのcgiに集めてみます。たとえば    /var/www/html/cgi0/cgi02/ において "ln -s /var/www/html/cgi0/routeStd.cgi"　として(絶対アドレス指定で)リンクを作り、　".../cgi0/cgi02/routeStd.cgi" をアクセスすると次が出力されます。　

        -------- Standard Library ---------
        Method: GET
        URL: http://192.168.1.10/cgi0/cgi02/routeStd.cgi
        URL.Path: /cgi0/cgi02/routeStd.cgi

ただし、ファイルやディレクトリの owner, permission等の調整が必要かもしれません。また、この方式は他の高機能なライブラリでパラメータ指定を使う場合には、リンク作業に難ありですね。


bmizerany/pat でのルーチング
-------------------------------------------
``` go:routePat.go
package main
import (
        "fmt"
        "net/http"
        "net/http/cgi"
        "github.com/bmizerany/pat"
)
func viewHandler( w http.ResponseWriter, r *http.Request ) {
        ...標準ライブラリとほぼ同じ...
}
func main(){
    router := pat.New()
    router.Get( "/cgi0/", http.HandlerFunc(viewHandler) ) // 登録A
//  router.Get( "/cgi0/cgi02/:name/:id/", http.HandlerFunc(viewHandler) ) // 登録D
    cgi.Serve( router )
}
```

とすると、１～2について、標準ライブラリの場合と同じ結果を得ます。


４．.htaccessは２と同じ条件で、mainで登録Dにしてブラウザから ".../cgi0/cgi02/xyxy/123/" をアクセスすると、次が出力されます。

        -------- PAT router ---------
        Method: GET
        URL: http://192.168.1.10/cgi0/cgi02/xyxy/123/?%3Aid=123&%3Aname=xyxy&
        URL.Path: /cgi0/cgi02/xyxy/123/



httpRouterでのルーチング
-------------------------------------------
```go:routeHttp.go
package main
import (
        "fmt"
        "net/http"
        "net/http/cgi"
        "github.com/julienschmidt/httprouter"
)
func viewHandler( w http.ResponseWriter, r *http.Request , ps httprouter.Params) {
    ... 標準ライブラリとほぼ同じ ...
}
func main(){
    router := httprouter.New()
    router.GET( "/cgi0/", viewHandler )  // 登録A
//  router.GET( "/cgi0/routeHttp.cgi", viewHandler )// 登録B
//  router.GET( "/cgi0/cgi02/dummy/", viewHandler ) // 登録C
//  router.GET( "/cgi0/cgi02/:name/:id/", viewHandler )// 登録D
    cgi.Serve( router )
}
```

１．標準ライブラリでの１と同様に mod rewriteなしで、ローカルのブラウザから ".../cgi0/routeHttp.cgi" をアクセスすると

        404 page not found      

となります(cgiは起動されているが、内部で処理すべきハンドラーが見つからない状態)。そこで、mainで登録Aではなく登録Bを使うとめでたく(当たり前!)、次が出力されます。

        -------- httpRouter ---------
        Method: GET
        URL: http://192.168.1.10/cgi0/routeHttp.cgi
        URL.Path: /cgi0/routeHttp.cgi


２．標準ライブラリでの２と同様に .htaccessでの mod rewriteを 

        RewriteRule ^(cgi01|cgi02|cgi03)(.*)  cgi0/routeHttp.cgi

として、ブラウザから ".../cgi0/cgi02/dummy/" をアクセスした出力は、登録AでもBでも　"404 page not found" となりますが、登録Cにすれば、次が出力されます。

        -------- httpRouter ---------
        Method: GET
        URL: http://192.168.1.10/cgi0/cgi02/dummy/
        URL.Path: /cgi0/cgi02/dummy/

４．.htaccessは２と同じ条件で、登録Dにしてブラウザから ".../cgi0/cgi02/xyxy/123/" をアクセスすると、次が出力されます。

        -------- httpRouter ---------
        Method: GET
        URL: http://192.168.1.10/cgi0/cgi02/xyxy/123/
        URL.Path: /cgi0/cgi02/xyxy/123/

* httprouter.Paramsから name や id の情報は取れますが、r.Formにはそれらの情報は入らないようです。
* cgiは起動されますから、プログラム側で(New)Builder機能を使えば違う登録形にできるのかもしれません(よくわかりません)。
* 4つライブラリのうちこれだけ viewHandlerに第3パラメータを必要としていますが、どのみち(cgi限定なら必要ないけど)コンテクストを導入するとして、そこにこのパラメータ値を突っ込むことにして、wrapする関数を用意すれば他のライブラリと同じ形にできることでしょう。
* GitHubの issuesにルーチングに関して問題が上がっていますが、他のライブラリでも同様なことがあるか否かは不明です。


gorilla/muxでのルーチング
-------------------------------------------
``` go:routeGori.go
package main
import (
        "fmt"
        "net/http"
        "net/http/cgi"
        "github.com/gorilla/mux"
)
func viewHandler( w http.ResponseWriter, r *http.Request ) {
    ... 標準ライブラリとほぼ同じ ...
}
func main(){
router := mux.NewRouter()
router.HandleFunc("/cgi0/", viewHandler).Methods("GET") // 登録A
//  router.HandleFunc("/cgi0/routeGori.cgi",viewHandler).Methods("GET") // 登録B
//  router.HandleFunc("/cgi0/cgi02/dummy/",viewHandler).Methods("GET")  // 登録C
//  router.HandleFunc("/cgi0/cgi02/{name}/{id}/",viewHandler).Methods("GET")// 登録D
    cgi.Serve( router )
}
```

とすると、httpRouterの場合と同じ結果を得ます。( r.Formにnameやidの情報が入らないのも同様)


routesでのルーチング
-------------------------------------------
``` go:routeRts.go
package main
import (
        "fmt"
        "net/http"
        "net/http/cgi"
        "github.com/drone/routes"
)
func viewHandler( w http.ResponseWriter, r *http.Request ){
    ... 標準ライブラリとほぼ同じ ...
}
func main(){
    router := routes.New()
    router.Get( "/cgi0/", viewHandler )             // 登録A
//  router.Get( "/cgi0/routeRts.cgi", viewHandler ) // 登録B
//  router.Get( "/cgi0/cgi02/dummy/", viewHandler ) // 登録C
//  router.Get( "/cgi0/cgi02/:name/:id/", viewHandler ) //登録D
    cgi.Serve( nil )
}
```

とすると、１～２については、httpRouter や gorilla/muxの場合と同じ結果となり、４については patと同じ結果を得ます。

おしまい
-------------------------------------------

ルーチングに限って言えば特定の条件下で上記各ライブラリ使用内では cgiプログラムと非cgiプログラムは、(簡単な使い方であれば)ほぼ同じ形にできるんじゃないかと。当たり前のことなのかもしれませんが、私は goも webプログラムも理解が今一つで不安だったので少しだけですが調べてみました。


その他

* 機能的には Gorilla/mux ＞ httpRouter  ？  routes ＞ pat なので、この逆順にシンプルなライブラリが速いのでは、と思いましたが、ライブラリが用意している(あるいはしていない)データ取得の手間なども含めて総合すると、決してそうとは言えないみたいです。

* 本稿は CentOS7, apache2.4, Go1.5.1上での操作に基づいています。他の環境では違った事になるかもしれません。また、ここに掲げたプログラムは動作を調べるためだけのものであり、決して「正しい」ものではありません。

参考 (各ライブラリの特徴あり)
https://www.nicolasmerouze.com/guide-routers-golang/
http://www.alexedwards.net/blog/a-mux-showdown