2017/06/03


幾らか言いたい事があったので。

Go言語感想文 - なるせにっき

序 最近、敵情視察を兼ねた仕事ととしてGoでアプリケーションを書いていた。このアプリケーションがどんなものかはそのうち id:tagomoris さんがどこかで話すと思うけれど、この コンポーネント ...

http://naruse.hateblo.jp/entry/2017/06/02/203441

GoroutineとChannel

Goroutineはようするにスレッドなんですが、文法と実装の支援でより気軽に使えるのが他の言語との違いでしょうか。なので、Goroutineをどれだけほいほい使うべきかというコスト感覚を身につけることがとても大事な気がします。Rubyなどとは気持ちを切り替えていく必要があるでしょう。ぼくはまだ切り替えきれていません。

Goroutine はスレッドではありません。Goroutine はコルーチンでありスレッドです。ランタイムが必要に応じてスレッドで実行するかコルーチンで実行するかをインテリジェントに切り替えます。ユーザが意識する必要はありません。無秩序に大量に作る様な事がないのであれば気軽に作成して良いはずです。

テストについて

アサーションをコピーしなければならない理由は一つのテストケースの中で異なるテストが混在しているか、単に同様のテストがコピーして作られているのが原因ではないでしょうか。

極端な例かもしれませんが、例えば以下の FizzBuzz 関数をテストするとします。

package fizzbuzz

import "fmt"

func FizzBuzz(n int) (stringerror) {
    if n < 1 || n > 100 {
        return "", fmt.Errorf("invalid number: %v", n)
    }
    switch {
    case n%15 == 0:
        return "FizzBuzz"nil
    case n%3 == 0:
        return "Fizz"nil
    case n%5 == 0:
        return "Buzz"nil
    default:
        return fmt.Sprint(n), nil
    }
}

1未満や100を超える値の場合はエラーとなり、それ以外は通常通り FizzBuzz の結果を返します。これをのっぺりとテストすると

func TestFizzBuzz(t *testing.T) {
    var input int

    got, err := FizzBuzz(-1)
    if err == nil {
        t.Fatalf("should be error for %v but not:"-1)
    }
    got, err := FizzBuzz(1)
    if err != nil {
        t.Fatalf("should not be error for %v but: %v"1, err)
    }
    if got != "1" {
        t.Fatalf("want %q, but %q:""1", got)
    }
    got, err := FizzBuzz(3)
    if err != nil {
        t.Fatalf("should not be error for %v but: %v"1, err)
    }
    if got != "Fizz" {
        t.Fatalf("want %q, but %q:""Fizz", got)
    }
}

この様に Fatalf のコピーになりかねません。まだ Buzz や FizzBuzz のテストも出来ていませんから、これからコピペが大量に作られる訳です。確かに Ruby の DSL は強力で、この様な退屈なテストを短い構文で記述する事が出来ます。しかし例外のない Go においては if 文が頻発し得ます。そこで Go ではテーブルドリブンテスト(Table Driven Tests)が推奨されています。

TableDrivenTests · golang/go Wiki · GitHub

Home Articles Blogs Books BoundingResourceUse cgo ChromeOS CodeReview CodeReviewComments CodeTools C...

https://github.com/golang/go/wiki/TableDrivenTests
以上のテストを Table Driven Tests に置き換えると以下の様になります。
func TestFizzBuzz(t *testing.T) {
    tests := []struct {
        input int
        want  string
        err   bool
    }{
        {input: -100, want: "", err: true},
        {input: -1, want: "", err: true},
        {input: 0, want: "", err: true},
        {input: 1, want: "1", err: false},
        {input: 2, want: "2", err: false},
        {input: 3, want: "Fizz", err: false},
        {input: 4, want: "4", err: false},
        {input: 5, want: "Buzz", err: false},
        {input: 6, want: "Fizz", err: false},
        {input: 14, want: "14", err: false},
        {input: 15, want: "FizzBuzz", err: false},
        {input: 16, want: "16", err: false},
        {input: 100, want: "Buzz", err: false},
        {input: 101, want: "", err: true},
    }

    for _, test := range tests {
        got, err := FizzBuzz(test.input)
        if !test.err && err != nil {
            t.Fatalf("should not be error for %v but: %v", test.input, err)
        }
        if test.err && err == nil {
            t.Fatalf("should be error for %v but not:", test.input)
        }
        if got != test.want {
            t.Fatalf("want %q, but %q:", test.want, got)
        }
    }
}

このテストでは今後テストケースを増やしても if が増える事はありません。つまり t.Fatal も増えません。一つのテストケースの中でテストされる結果はおおよそ同様の物となるはずです。そうでないならばそれはテストがユニットテストになっていないのだと思います。

その他

switchべんり ← おまえほんとうにそれでいいのか ** selectやswitchの中でbreakすると外側のforまで届かないのでbreak すればよいけど、結局goto使う

Golang に限らずですが、最近の言語では Labeled Break という物があります。

exit_loop:
    for {
        s := foo()
        switch s {
        case "exit":
            break exit_loop
        }
    }
Goはnull安全ではない←構造体のポインタを扱い始めると気になってくる

重箱ぽくなりますが、構造体フィールドに直接アクセスしなければレシーバが nil かどうかで判定出来ます。

package main

import "fmt"

type Foo struct {
    v int
}

func (f *FoodoSomething() string {
    if f == nil {
        return "ぬるぽ"
    }
    return "のっとぬるぽ"
}

func main() {
    var f *Foo

    fmt.Println(f.doSomething()) // ぬるぽ

    f = new(Foo)
    fmt.Println(f.doSomething()) // のっとぬるぽ
}

まぁ、f が nil である事も条件を切り分ける為の一つの状態なので、これはあまり使わない手法ではあります。Go が null 安全だとは言ってないです。


2017/06/02


認証を持たないウェブアプリケーションをいざ認証に対応させようと思うと案外面倒でモチベーションを無くしてしまうなんて事もよく起きうる話です。特に社内向けのアプリケーションを作っていたら本番で使う事になってしまって、なんて話は良くある話です。開発で本番 DB を見るのはちょっと...。でも既存のコードをゴリゴリと触りたくない。そんな場合にログイン認証部分だけマイクロサービス化できると気持ちも幾分和らぎます。今日はそんなちょっと便利なサーバ「loginsrv」を紹介したいと思います。

GitHub - tarent/loginsrv: JWT login microservice with plugable backends such as OAuth2, Github, htpasswd, osiam

loginsrv is a standalone minimalistic login server providing a JWT login for multiple login backends.

https://github.com/tarent/loginsrv

loginsrv は JWT トークンを使って安全にユーザ識別をやりとり出来る単体のマイクロサービスです。既存のコードを JWT トークンに対応させるだけで認証機能を代行してくれます。また一度この対応を行っておけば loginsrv を使わない実装になったとしても簡単に取り換えられるという訳です。導入方法を紹介して行きます。

loginsrv の起動方法は以下の通り。

Usage of loginsrv:
  -backend value
        Deprecated, please use the explicit flags
  -cookie-domain string
        The optional domain parameter for the cookie
  -cookie-expiry duration
        The expiry duration for the cookie, e.g. 2h or 3h30m. Default is browser session
  -cookie-http-only
        Set the cookie with the http only flag (default true)
  -cookie-name string
        The name of the jwt cookie (default "jwt_token")
  -github value
        Oauth config in the form: client_id=..,client_secret=..[,scope=..,][redirect_uri=..]
  -grace-period duration
        Graceful shutdown grace period (default 5s)
  -host string
        The host to listen on (default "localhost")
  -htpasswd value
        Htpasswd login backend opts: file=/path/to/pwdfile
  -jwt-expiry duration
        The expiry duration for the jwt token, e.g. 2h or 3h30m (default 24h0m0s)
  -jwt-refreshes int
        The maximum amount of jwt refreshes. 0 by Default
  -jwt-secret string
        The secret to sign the jwt token (default "random key")
  -log-level string
        The log level (default "info")
  -login-path string
        The path of the login resource (default "/login")
  -logout-url string
        The url or path to redirect after logout
  -osiam value
        Osiam login backend opts: endpoint=..,client_id=..,client_secret=..
  -port string
        The port to listen on (default "6789")
  -simple value
        Simple login backend opts: user1=password,user2=password,..
  -success-url string
        The url to redirect after login (default "/")
  -template string
        An alternative template for the login form
  -text-logging
        Log in text format instead of json

認証方法が幾らか用意されています。

種別説明
htpasswdMD5やSHA1、Bcryptでパスワードがエンコードされたファイル。
OSIAMRESTで使用できる認証管理サーバ。
Simple引数でユーザとパスワードを指定。
OAuth2ご存じ OAuth2。現在は組み込みプロバイダとしてGitHubのみサポート。

社内で .htaccess で運用されていた物を使うなんて事も可能です。

JWT トークンの対応には秘密キーが必要です。openssl コマンド等で生成しますがここでは割愛。この秘密キーを loginsrv と共存させる事でユーザ識別のやり取りを可能にします。loginsrv は -jwt-secret という引数で受け取る事も出来ますが、LOGINSRV_JWT_SECRET という環境変数で受け渡す事もできます。

package main

import (
    "fmt"
    "net/http"
    "os"

    "github.com/dgrijalva/jwt-go"
)

var privateHtml = `
こんにちわ %s さん<br />
<a href="/login?logout=true">ログアウト</a>
`

var publicHtml = `
<a href="/login">ログイン</a>
`

func main() {
    secret := os.Getenv("LOGINSRV_JWT_SECRET")
    http.HandleFunc("/"func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("content-type""text/html; charset=utf8")
        if c, err := r.Cookie("jwt_token"); err == nil {
            token, err := jwt.Parse(c.Value, func(*jwt.Token) (interface{}, error) {
                return []byte(secret), nil
            })
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
            if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
                fmt.Fprintf(w, privateHtml, claims["sub"])
                return
            }
        }
        fmt.Fprintln(w, publicHtml)
    })
    http.ListenAndServe(":8888"nil)
}

jwt_token というクッキーを秘密鍵で照合し資格情報 claim を表示しています。本来ならば loginsrv に依存させない様に LOGINSRV_JWT_SECRET ではなく引数などから秘密鍵を貰って下さい。

あとはこのアプリケーションを loginsrv と一緒に起動させます。せっかくなので goreman (foremanクローン) を使います。Procfile は以下の通り。

web1: ./app
web2: loginsrv -htpasswd file=/path/to/htaccess
gorem: gorem

ここで起動している gorem はカスタマイザブルなリバースプロキシサーバです。設定ファイル config.json は以下の通り。

{
  "app": {
    "address": "127.0.0.1:5000",
    "entries": [
      {
        "path": "/login",
        "backend": "http://localhost:6789",
        "use_path": true
      },
      {
        "path": "/",
        "backend": "http://localhost:8888",
        "use_path": true
      }
    ]
  }
}

本番では gorem の代わりに nginx や apache を使うと良いでしょう。最後に JWT の秘密鍵を .env で設定します。

LOGINSRV_JWT_SECRET=deadbeef

ここまで出来たら goreman start を実行します。ブラウザで http://localhost:5000/ を開くとログインしていない時のコンテンツが表示されます。

ページ

リンクからログイン画面に移動すると以下の画面が表示されます。

ページ

GitHub プロバイダを使う場合は GitHub 認証用のボタンが表示されます。

GitHub認証

ログインが成功すると指定のURL(デフォルトは /)に戻ってきます。

ページ

開発では .htaccess を使い、本番では OAuth2 を使うといった運用が簡単に行える様になります。今回の例ではアプリケーションをそのまま使いましたが、実際は docker から起動できる様にしておくと以降の開発が便利になるかと思います。

みんなのGo言語[現場で使える実践テクニック] みんなのGo言語[現場で使える実践テクニック]
松木雅幸, mattn, 藤原俊一郎, 中島大一, 牧大輔, 鈴木健太
技術評論社 / (2016-09-09)
 
発送可能時間:


2017/05/19


今日、LAN ケーブルを抜いた直後に msys2 の cat コマンドを実行したらハングする事に気付いた。

これはまずい。なんかに感染してる。cat コマンドと言いながらどこかインターネットにアクセスしてるんや!

と思って色々調べた。が実は cat コマンド君悪くなかった。すいませんすいません。

一部の記事では /cygdrive/ のアクセスがあるから hosts に cygdrive を足せば良いといった物もあったが、デマっぽかった。以下、調査した最終結果を書いていく。Cygwin や msys2 のコマンドは所有者や権限情報を取得する為に LDAP 経由でアクティブディレクトリに問い合わせを行う。例えば cat foo.txt と実行した場合であってもファイルの権限を UNIX エミュレーションする為に必要となる。で、このアクティブディレクトリへの問い合わせが LAN が抜けてるとタイムアウト待ちになって遅い。とにかく遅い。

解決策はここに書いてあった。

windows - Startup is really slow for all cygwin applications - Stack Overflow

Eventually I found what causes this issue, but I'm still not sure why. Cygwin works for other people...

http://stackoverflow.com/questions/28410852/startup-is-really-slow-for-all-cygwin-applications

以下はその PC が自分専用である事が前提となる。なぜかというと Cygwin や msys2 上で /etc/passwd/etc/group を生成して SID をローカルにキャッシュさせるため。なので PC に新しいユーザが追加され、その人が作ったファイルをローカルに持ってきた際に本来ならばこれらのファイルを再生成する必要がある。そういった問題点を理解した上で以下を読んで頂きたい。

まず mintty 等のターミナルを開き、以下を実行する。

$ mkpasswd -c -l > /etc/passwd
$ mkgroup -c -l > /etc/group

これにより SID に対する名前やグループ情報が書きだされる。次にネットワークの問い合わせ方法を変更する為に /etc/nsswitch.conf を修正する。

変更前

# Begin /etc/nsswitch.conf

passwd: files db
group: files db

db_enum: cache builtin

db_home: cygwin desc
db_shell: cygwin desc
db_gecos: cygwin desc

# End /etc/nsswitch.conf

変更後

# Begin /etc/nsswitch.conf

passwd: files # db
group: files # db

db_enum: cache builtin

db_home: cygwin desc
db_shell: cygwin desc
db_gecos: cygwin desc

# End /etc/nsswitch.conf

db をコメントアウトした。これでファイルの権限や所有者を検査するのにアクティブディレクトリに問い合わせしなくなる。よって LAN が抜けても遅くならない。前述の通り、新しいユーザが作ったファイルをローカルにコピーする様な事があるならば /etc/passwd/etc/group を再生成する必要がある。その必要がないのであれば、この設定を行う事で全てのコマンドが少しだけ速くなる。