Humanity

Edit the world by your favorite way

panic() するか non-nil error が返ったらロールバック用の処理を実行するユーティリティ構造体 Guard を作った

こんな感じに使う。

package main

import (
    "errors"
    "fmt"
)

func main() {
    fmt.Println("start")
    if err := f(); err != nil {
        fmt.Println(err.Error())
    }
    fmt.Println("end")
}

func f() (err error) {
    fmt.Println("f() start")
    g := &Guard{}
    defer func() { err = g.Rollback(recover()) }()

    g.Add(func() {
        fmt.Println("rollback 1")
    })
    // if true {
    //     panic("panic!")
    // }
    g.Add(func() {
        fmt.Println("rollback 2")
    })

    // oops, invoke all rollback functions in reverse order...
    e := errors.New("something went wrong")
    if e != nil {
        err = g.Rollback(e)
        err = g.Rollback(e) // this won't call rollback functions twice!
        return
    }

    fmt.Println("f() end")
    return
}

// Guard invokes "rollback functions" if Rollback method received non-nil value
// (e.g. recover(), non-nil error).
//
// TODO: use mutex or something if the methods may be invoked in parallel!
type Guard struct {
    // rollback functions
    rbFuncs []func()
}

// Rollback rolls back if v is non-nil.
//
//   defer func() { err = g.Rollback(recover()) }()
//
//   // or
//
//   if e != nil {
//     err = g.Rollback(e)
//     err = g.Rollback(e) // this won't call rollback functions twice!
//     return
//   }
func (g *Guard) Rollback(v interface{}) error {
    var err error
    if e, ok := v.(error); ok {
        err = e
    } else if v != nil {
        err = fmt.Errorf("%s", v)
    }
    if err != nil {
        g.RollbackForcefully()
    }
    return err
}

// RollbackForcefully calls rollback functions in reverse order.
func (g *Guard) RollbackForcefully() {
    // Call rollback functions in reverse order
    for i := len(g.rbFuncs) - 1; i >= 0; i-- {
        g.rbFuncs[i]()
    }
    // Do not rollback twice
    g.rbFuncs = nil
}

// Add adds given rollback functions.
func (g *Guard) Add(f func()) {
    g.rbFuncs = append(g.rbFuncs, f)
}

そんなに行数無いので管理面倒になるしリポジトリは作ってない。 とりあえず Volt でちょうどこんな interface を実装した struct が沢山あって (インタプリタ的な DSL を実装中で、Value は全てのベースとなる型)

type Op interface {
  // ...

  Execute(ctx context.Context, args []Value) (ret Value, rollback func(), err error)
}

いちいちロールバック用の関数のスライス作って error や panic() の処理して… ってするのはかなり面倒になりそうだったので、構造体にまとめた。 本来の処理に注力できて良い感じに関心の分離ができてる感じ。

ちなみに Volt ではちょっと変更したバージョンを使ってて、 Rollback() で error を返す所を pkg/errorsWrap() 使うように変えてる。 エラーメッセージが分かりやすくなっていい感じになった (はず…確認できてないけど)。