golangでrecoverしたときの戻り値

この記事は Crowdworks Advent Calendar 2015 13日目の記事になります. クラウドワークスの業務では全然goを使っていないけど,goの話をします.

panicとrecover

goには例外がないと言われているし,そもそもあんまり panic を使う機会はないと思う.もし頻繁に panic していたら,おそらくその panic の使い方,goではあまり推奨されることじゃないので「本当にそれpanic必要なの?」と疑ったほうがいい. golangのpanicは例外ではないのか?

そして,さらにはrecover というものがあり,こいつは panic が起きた時の処理をハンドリングできる.

func hoge() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("recoverd!\n")
            // Do something
        }
    }    
    panic("hogehoge")
}

としておくと,hoge()panic に陥らない.

できれば,あまり panic を使わないで欲しいし,ましてや recover なんてもっと使わないでほしい. recover については以下の記事が詳しい. panicはともかくrecoverに使いどころはほとんどない

わかっていても使わなきゃいけない場合もある

直近で困ったのは,sql パッケージを使った時のトランザクションだった. トランザクションなので,panic が発生した場合は Rollback してもらわないと困る.

func hoge(tx *sql.Tx) {
    defer func() {
        if err := recover(); err != nil {
            tx.Rollback()
        }
    }()
    // Do something
    tx.Exec(…)
}

[Go言語] database/sqlパッケージを使ってみた

もちろん,SQLの実行結果としてエラーを受け取れる場合は多々ある.

err := tx.QueryRow("SELECT name FROM users WHERE id = ?;", id).Scan(&name)
if err != nil {
    tx.Rollback()
}

その場合は,ちゃんと Rollback を呼びだせばいいのだけれど,その他のコードで panic が呼ばれる可能性は否定できない.

そういった場合でもちゃんとトランザクションロールバックしてほしい.というわけでどうしても recover せざるを得ない.

recoverしたときの戻り値どうなるの?

戻り値を宣言しているにもかかわらず,return しなかった場合には,ふつうコンパイラに怒られる.では,recover したときは return しなくていいのだろうか? 一応コンパイラからは怒られていない.では、一体どんな値が返ってくるんだろうか?

boolの戻り値

以下のように関数を定義していたとする.

func hoge() bool {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("recoverd!\n")
        }
    }()
    // Do something
    return true
}

このとき,panicrecover されたらこの関数の戻り値はどうなるのか? やってみた.

package main

import (
    "fmt"
)

func main() {
    fmt.Printf("rescue return: %+v\n", rescue())  // => rescue return: false
}

func rescue() bool {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("panic: %+v\n", err)  // => panic: error
        }
    }()

    panic("error")

    return true
}

へー,false になるらしい.

intの場合は?

func main() {
    fmt.Printf("rescue return: %+v\n", rescue())  // => rescue return: 0
}

func rescue() int {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("panic: %+v\n", err)  // => panic: error
        }
    }()

    panic("error")

    return 10
}

つまり定義時の値を返している

上記のような例の場合,処理の流れ的にどこにも return する場所がないまま,panic / recover になってしまっている. そのときにどうやら,その型で定義したときの値を返している様子.

var i int    // => 0
var s string // => ""
var b bool   // => false

というわけで,recover で拾う場合はこのことを覚えておきましょう.何も考えずにrecover していると,うっかり正常に終了しているように見えるような値を返しているかもしれない.

そして例外的に使っているからこそ,できるだけ何が起こったかは把握しておいた方が良くて,recover した時にログくらい出しておかないとわからなくなる.

recover時に戻り値を指定する

defer 内で return しようとすると……

func main() {
    fmt.Printf("rescue return: %+v\n", rescue())  // => rescue return: false
}

func rescue() bool {
    defer func() bool {
        if err := recover(); err != nil {
            fmt.Printf("panic: %+v\n", err)    // => panic: error
            return true
        }
        return true
    }()
    return true
}

特に考慮はしてくれないらしい.というか,defer で呼び出した関数の戻り値は,親の関数の戻り値と何ら関係ないのか……. さすがに以下のようなことはできない

func rescue() bool {
    defer return func() bool {    // => syntax error: unexpected return
        if err := recover(); err != nil {
            fmt.Printf("panic: %+v\n", err)
            return true
        }
        return true
    }()
    return true
}

いや,そもそも deferreturn を呼ぶなんて無理か…….

ドキュメントを探すと,やり方はあるらしい.

func main() {
    fmt.Printf("rescue return: %+v\n", rescue())  // => rescue return: true
}

func rescue() (b bool) {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("panic: %+v\n", err)
            b = true
        }
    }()

    panic("error")    // => panic: error

    return true
}

Defer, Panic, and Recover

Deferred functions may read and assign to the returning function's named return values.

なるほど,return はできないけど,return の値を書き換えることはできると…….

returnの書き換えを使うパターン

先のトランザクションの例で,例えば以下のようにちゃんと error を返すコードを書いていたとする.

func hoge(tx *sql.Tx) (bool, error) {
    defer func() {
        if err := recover(); err != nil {
            tx.Rollback()
        }
    }()
    // Do something
    tx.Exec(…)
    return true, nil
}

この場合,recover されると,errornil で返さる.エラーが起こっているにもかかわらず,以下のようなエラーチェックをしていると(goではすごく一般的なエラーチェックコード),呼び出し元でうまいことエラーチェックできない可能性がある.

func main() {
    result, err := hoge(tx)
    if err != nil {
        log.Fatal("error: ", err.Error())
    }
    // Do something
}

こんなコードを書いていたら,全然ハンドリングされないかもしれない. なので,error はやはりきっちり返したい.というようなときに,先ほどのreturn値の書き換えを使うしかなくなる.

func hoge(tx *sql.Tx) (r bool, e error) {
    defer func() {
        if err := recover(); err != nil {
            tx.Rollback()
            r = false
            e = errors.New("unexpected error!")
        }
    }()
    // Do something
    tx.Exec(…)
    return true, nil
}

っていうかできるだけrecover使わないで書こう

上記のような例は仕方ないにしても,できる限り,recover することや,その戻り値をアテにすることは避けたほうが良い.できることなら,goの一般的な関数らしく,戻り値にerror を含めて,何か起こったらpanic するのではなくerrors.New() した方がいい.

というのは,どこで聞いても言われることらしい. http://stackoverflow.com/questions/19934641/golang-returning-from-defer

golangのpanicは例外ではないのか?

goには例外がないと言われている.確かに,よく見かける,exception とかはないんだけど,あれ? panic があるじゃん?

では panic は例外ではないのか?

我々は、処理構造を制御するためのtry-catch-finally形式の例外処理機構によって、コードが入り組んでしまうと考えています。しかも、ファイルを開けないといった、ごく一般的なエラーをさも特別なエラーであるかのように扱わせる傾向があります。

Go言語では、異なるアプローチを取りました。Go言語では戻り値として複数の値が返せるので、一般的なエラーハンドリングの場合、戻り値といっしょにエラー情報を返すことができます。エラー型は標準化されており、Goの他の機能と相まってエラーハンドリングがすっきりしたものとなります。これは、他の言語と大きく異なる点です。

それとは別に、Go言語にはエラーシグナルの発行機構と、本当に例外的な状況から回復する機構があります。エラーの発生によって、関数のコールスタックの巻き戻りが開始する中でのみ、このエラー回復メカニズムは実行可能です。このメカニズムは、大きな障害をハンドリングするのにも充分な上に、処理構造に特別な制御を行う必要もありません。また上手に使えば、エラーハンドリングのコードが読みやすくもなります。

http://golang.jp/go_faq

つまり,try-catch-finally形式の例外ではない.

冒頭でも述べたように、Goは一般的な例外処理への使用を推奨しないものの、例外処理用の専用構文を備えている。一般的な例外にはあくまで戻り値を使う。

https://ja.wikipedia.org/wiki/Go_%28%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E%29#.E4.BE.8B.E5.A4.96.E5.87.A6.E7.90.86

なるほど.ということは,「例外が存在しない」わけではなくて「一般的なtry-catchの中で発生させているような使い勝手の良いexceptionがない」というべきなのか.

厳密に言えば,panic defer recoverpanic を拾えるわけだ.ただし,ブロックのようなものを作れないので,あくまで関数全体に対して,defer recover することにはなるのだが. ただ,その機構を いつものexception みたいに使うなよ っていう意味を込めての「例外がない」なのか.

とはいえ,実現したいことは理解できる.

一般的な,try-catch内で発生させる exception については,「それ本当に例外なのか?」というのと「読みにくい」に尽きる. そこに関しては納得できる.

ただ,例外を使わずこまめに error を返していくと,こういう意見にも納得できるわけで…….

if err != nil によるエラーハンドリングを繰り返し繰り返し書かなくてはならないことがあります。 Go Blogにはif err != nil { return err }のパターンはあまり出現しない(once per page or two)と書かれていますが、 プログラムのタイプによっては(例えばいろいろな外部リソースや外部ライブラリをつなぐようなコード)かなりの頻度で if err != nil を書かざるを得ないことがあるような気がします。

http://www.yunabe.jp/docs/golang_pitfall.html#section-4

さらに,この error を書くパターンだと,スタックトレースを見るために発生源の情報を error に追記する必要があり…….

これでエラーがどこで発生したのかが分かるようになります。ただこういうことしてると実は例外で良かったんじゃないかという気分にもなりますが。

http://www.yunabe.jp/docs/golang_pitfall.html#return-nil-err--error

なので,結局割といっぱいこのパターンを書かなきゃいけないとは思うんですよね.

ただ,最初の思惑どおり,やっぱり例外として既に別処理に回されたりしていることはないので,上から順番に処理されていくという意味で読みやすくはあります.

watchfiyがnfsマウントしたファイルの変更を検知してくれないとき

browserify + watchify + gulp

browserify + gulpでビルドするときに,よく差分ビルドをするためにwatchifyを使うことを推奨している記事は結構多い.そういう環境でビルドする方法は こちら あたりを参考にしてもらえると嬉しい.

これが,watchifyを入れておかないと,ファイルの変更を検知してくれないので,毎回自分でビルドコマンドを叩かなきゃいけないし,差分ビルドにならないので遅い.そんな開発環境は嫌だ.というわけで,gulpのビルドタスクにwatchifyを組み込んでおくことは,本当にいろんなところで紹介されている.

nfsマウントしたディスク上のファイル変更が検知されない

前述のような例に沿って同じようなgulpタスクを作ってみた.ただ,特に理由はないのだけれど,vagrantで動かしていた.

jsのソースファイルは,vagrantのshared folderを使って,ホストOS側の適当なディレクトリに放り込んでいる.vagrantはそれをnfsマウントして利用していた. この場合,jsのソースファイルの編集はホストOS側で行う.しかし,サーバはvagrant側で動かしているのでjsのビルドもvagrant側で行ってほしい.

そうすると,vagrant側のgulpはwatchifyを動かしていても,ファイル変更を検知してくれなかった.

これは,watchifyに poll オプションを与える必要がある.

var w = watchify(b, {
  poll: true
});

http://stackoverflow.com/a/30743525/4545174

これでホストOS側で変更したファイルの変更検知を,watchifyが行ってくれるので差分ビルドが走るようになった.

ちなみに遅い

ビルドが遅いのではなくて,変更検知にラグがある…….これはnfsマウントしているから仕方ないのか?

同じことを思う人はいたらしい.

Watchify polling very slow on Vagrant Machine

こういう記事,あまり出回っていないので,多分近年のフロントエンド開発をする人はvagrantとか使わないのかな……. *1

*1:ここに脚注を書きます

golangでテストをする

最近golangを結構書いています.
コンパイルも割と速いし,実行速度は言うまでもなく速い.

クラスがないとか,例外がないとか,色々と言いたいことはあるでしょう.思想的には結構独特ですが,なれると普通に書けます.

今日はそんな中でテストの話をしようと思う.

やっぱりBDDがいい

golangにはテストをするための,testingというパッケージがある.

golang.jp


使い方はこの辺の記事が参考になるでしょう.

qiita.com



assertがないというのが,結構主題かもしれない.

ただ,使い方を見てみればわかる通り,これはExampleのテストでしかない.

個人的な意見なのだけれど,やっぱりパターンが複雑になればなるほど,分岐が多くなり,前準備が複雑になる.
どうしてもBDDを導入したかったので,ginkgoを入れてみた.

github.com



matcherにはgomegaというものが使われている.

github.com




使い方は説明を読んでもらえればわかるだろう.

BeforeEachAfterEachのようなコールバックも用意していて,使いやすい.

BeforeEachAfterEachは各It節の直前・直後で実行されるが,BeforeSuiteなどはtest_suiteが実行される直前に呼ばれるだけである.



テスト用のDBを切り替える

前提

さて,ginkgoの準備はできた.

golangは必ずしもwebサービスを作るものではないが,とりあえずwebサーバーで,裏側にDBを持っているようなものを想定しよう.
その場合,テスト用のDBをどうするかというのが結構な問題になる.

たとえば,Railsのようなフレームワークであれば,database.ymlの設定を,Railsが勝手に読み込んでくれる.
だから,テスト時には接続されるDBが自動的にテスト用のDBになる.


通常開発しているDBをテスト用に使ってしまっても構わないが,やっぱりデータをクリアするのは面倒だし,なによりテストしながら開発することを考えると,入れておいたデータが消えてしまうのは嫌だ.
開発用のデータというのは,できるだけ低コストで準備したいから,テスト毎に消されるなんて面倒が増えてしょうがない.

というわけで,テスト用のDBに切り替えられるようにしよう.

DBの接続設定を書く

まず,DBに接続している部分のコードを見てみよう.
たぶん,

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)
func main() {
    db, err := sql.Open("mysql", "root:hogehoge@/sample_project?charset=utf8")
    if err != nil {
        panic(err.Error())
    }
}

こんなコードがかいてあるんじゃないだろうか.

ここを,まず環境変数によって切り替えられるようにする.

package db
import (
    "os"
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)
func Database() *sql.DB {
    username := os.Getenv("DB_USER")
    password := os.Getenv("DB_PASSWORD")
    database := os.Getenv("DB_NAME")
    db, err := sql.Open("mysql", username + ":" + password + "@/" + database + "?charset=utf8")
    if err != nil {
        panic(err.Error())
    }
    return db
}


これで環境変数からDB接続情報が取れるようになった.
direnvあたりを使って環境変数を管理しておくと楽だろう.

テストのBefore節で環境変数を書き換える

上記の関数を使ってDB接続する限り,接続先は環境変数に記述されているものとなる.
つまり,環境変数で与えられるDB_NAMEさえ書き換えてしまえば,ローカルの好きなDBにつなぐことができる.

ただし,環境変数そのものを入れ替えるので,テスト実行中に別のプロセスで開発したりしていないことを前提としている.


model_test.goにて,

package model_test
import (
    "os"
    "database/sql"
    "./database"
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
)

var _ = Describe("List", func() {
    var (
        currentdb string
        table *sql.DB
    )
    BeforeEach(func() {
        testdb := os.Getenv("DB_TEST_NAME")
	currentdb = os.Getenv("DB_NAME")
        os.Setenv("DB_NAME", testdb)
    })
    AfterEach(func() {
        table = database.Database()
        table.Exec("truncate users;")
	table.Close()
        os.Setenv("DB_NAME", currentdb)
    })
}

こんな感じにしておく.
これで環境変数としてDB_TEST_NAMEにテスト用DBの名前を入れておけば大丈夫.
ちゃんと終わったら元の値に戻しているので,終わったら普段通りに開発用DBに繋がるようになっている.


ちなみに,このコードは,DBの接続を切り替えるだけなので,ちゃんとCREATE DATABASEや,必要なテーブルのmigrationは終わらせてある前提.

そのへんはgooseあたりが使いやすかった.

liamstask / goose — Bitbucket




mockとstub

もう一点,かなり悩ましい項目の一つにStubやMockがある.


structに包まれて,interfaceとして実装している,いわゆるインスタンスメソッド的な立ち位置の関数は,実はinterfaceごとを上書きしてやればmockできる.

こんな感じで.

qiita.com



そこまでしなくても,testifyを使うと,mockというpackageがあったりする.

github.com



ただ,どちらもstructのメンバー関数しか上書きできない.


グローバル関数をstubしたいときはどうするのか?


いいやり方はこのくらいしか見つからなかった.

matope.hatenablog.com


csrf = checkCSRF

func checkCSRF() bool {
     // hogehoge
}

まぁこんな感じで,関数ポインタを使うように本体の実装を書き換える必要がある.

で,テスト側では

    JustBeforeEach(func() {
        csrf = func() bool { return true }
    }
    // hogehoge

こうしてグローバル変数に入った関数ポインタを書き換えてやる.


もしテスト中にstubを解除したければ,関数ポインタの値を適当な変数に入れて持っておけば良さそう.


まだこのくらいしか書いていない

テストっていうのは状況を準備するのが大変だ.
これだけの複雑な状況をテストのたびに作りなおすのが結構だるくて….

RSpecでいうところのfactory_girlみたいなものも,探して,確かにあったんだけど,それじゃないような……感.github.com


ActiveRecordみたいなものをもたずに,自分でDBから必要なデータを取ってきて,オブジェクトを組み立てているので,factory_girlでデータそのものを作ってもらっても,結局組み立て部分のコードがかさむ…….


というところで,まだまだ悩み中です.

ただ,golangはgodocがかなり充実していて,適当なライブラリであってもREADMEよりgodocに詳しく書いてあって,ドキュメントに関してはあまり困らなくて,すごく嬉しい.

swift2.0への変更で詰まったところ

これをswift2.0対応させてみたので,詰まったところを書いておく.

h3poteto.hatenablog.com


Xcode7を入れたあと,Xcode6.4で開発していたプロジェクトを開くと,初回に古いシンタックスをすべて置換してくれる.
ちなみにこれ,一度閉じてしまった場合でも,Edit->Convert->To Latest Swift Syntax でいける.

だいたい,使ってない変数を消してくれたり,varだけど代入していないものをletになおしてくれたりする.

そういうのがひと通り終わっても,コンパイルが通らなかったので以下メモ.

CGBitmapInfo

let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.NoneSkipFirst.rawValue)
CGBitmapContextCreate(
    nil,
    Int(ceil(pixelSize.width)),
    Int(ceil(pixelSize.height)),
    CGImageGetBitsPerComponent(originalImageRef),
    0,
    colorSpace,
    bitmapInfo)

こんなコードを書いていたのだけれど,

error:Cannot convert value of type 'CGBitmapInfo' to expected argument of type UInt32

こんなことを言われる.

どうやらCGBitmapContextCreateの引数の型が変わったらしい.
幸いなことにCGBitmapInfoはrawValueというUInt32型を構造体が保持しているので,そいつを取り出してやれば良い.

let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.NoneSkipFirst.rawValue)
CGBitmapContextCreate(
    nil,
    Int(ceil(pixelSize.width)),
    Int(ceil(pixelSize.height)),
    CGImageGetBitsPerComponent(originalImageRef),
    0,
    colorSpace,
    bitmapInfo.rawValue)


同じことがstackoverflowに質問されていたので,回答しておいた.stackoverflow.com


Dictionaryの型

var shintani: String?
let params: Dictionary<String, String> = [
    "yes" : "asumiss"
]
shintani = "ryoko"
let dict: Dictionary<String, AnyObject> = [
    "asumi" : params,
    "shintani" : shintani
]

これはAnyObjectで怒られる.

Type of expression is ambiguous without more context

そもそもString?型を突っ込んでいることに問題があるので(nullが入る可能性がありDictonaryとしては良くない),どこかでunwrapする必要がある.

var shintani: String?
let params: Dictionary<String, String> = [
    "yes" : "asumiss"
]
shintani = "ryoko"
let dict: Dictionary<String, Any> = [
    "asumi" : params,
    "shintani" : shintani!
]

viewControllersで取ってきたものの型チェック

型チェックがきつくなったので,viewControllersでとってきたものの型をちゃんとキャストしておかないといけない.

例えばこんなコードを書いていたとする.

if let controllers = appDelegate.rootController.viewControllers {
  for navController in controllers {
    if let target = navController.topViewController {
      // targetに対する処理
    }
  }
}

これだとtopViewControllersの呼び出しでコンパイルエラーになる.

if let controllers = appDelegate.rootController.viewControllers {
  for navController in controllers {
    if let controller = navController as? UINavigationController {
      if let target = controller.topViewController {
        // targetに対する処理
      }
    }
  }
}

こんな感じで,as UIViewContrllersしないとtopViewControllerとかできない

ATSをdebugで無効化

これがなかなかやっかいで…….
自作のサーバープログラムと通信するようなアプリを作っている場合,開発環境ではテストサーバーにつなぐことがほとんどだと思う.

だけど,開発環境のサーバーなんて,もちろんSSLなんて通しているはずがない.
っていうかオレオレ証明書ですら挟むのめんどくさい.

そもそもローカルでrails sしただけのサーバーに繋ぎたいし.


というわけで,debugモードの時だけATSを無効化したいんです.


qiita.com


これを参考に,ドメイン指定によるATS無効化をやることにしよう.


/etc/hostsに

127.0.0.1  whalebird.localdomain

と書いておいて,このドメインだけATSを無効化しました.


@testable便利

以前はこういう問題があったんですけど,testableのおかげでこういうことせずにクラスを呼び出すことができました.h3poteto.hatenablog.com


割と楽です.