Goでスタックトレースを上書きせずにエラーをラップする方法

こんにちは。eKYC開発チームの藤本です。

eKYCのサーバーサイドではpkg/errorsパッケージを使用してGoのスタックトレースを記録しています。スタックトレースは標準のerrorsパッケージではサポートされていませんが、エラー発生時のスタックトレースがわかるとエラーの解決が楽になるので、是非記録しておきたい情報です。

pkg/errorsパッケージを使用してスタックトレースを記録するには、エラーを初期化するときにerrors.New, errors.Errorf関数を使用するか、既存のエラーに対してerrors.WithStack関数を使用します。こうすると、作成されたエラーerrに対してerr.StackTraceメソッドを呼び出すことによりスタックトレースを取得することができ、err.Formatメソッドの出力にもスタックトレースが含まれるようになります。

こうして作成したエラーをラップするとどういう挙動になるでしょうか?

errors.Wrap関数を使用した場合

pkg/errorsには、errors.Wrapというその名の通りにエラーをラップしてくれる関数があるので、これを使用して作成したエラーがどういう挙動をするのかを実際にコードを書いて確認してみます。

コード

package main

import (
    errs "errors"
    "fmt"
    "github.com/pkg/errors"
)

func main() {
    err := errors.New("error")

    we := errors.Wrap(err, "wrap")

    fmt.Println("### Error() ###")
    fmt.Printf("%v\n\n", we.Error())
 
    fmt.Println("### StackTrace() ###")
    if e, ok := we.(interface{ StackTrace() errors.StackTrace }); ok {
        fmt.Printf("%+v\n\n", e.StackTrace())
    }

    fmt.Println("### Formatter ###")
    if e, ok := we.(fmt.Formatter); ok {
        fmt.Printf("%+v\n\n", e)
    }

    fmt.Println("### Unwrap() ###")
    fmt.Printf("%v\n", errs.Is(we, err))
}

出力

### Error() ###
wrap: error

### StackTrace() ###

main.main
    /tmp/sandbox309561315/prog.go:12
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:225
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1371

### Formatter ###
error
main.main
    /tmp/sandbox309561315/prog.go:10
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:225
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1371
wrap
main.main
    /tmp/sandbox309561315/prog.go:12
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:225
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1371

### Unwrap() ###
true

Go Playground

StackTraceメソッドの出力は、errros.Wrap関数を呼び出した時点(12行目)のスタックトレースになってしまい、元々のエラーの発生箇所(10行目)がわからなくなってしまいます。Formatメソッドの出力には、ラップした時点のスタックトレースと元々のスタックトレースが両方出力されるようになります。
標準パッケージのIs関数で元々のエラーとの一致判定を行うことで、作成したエラーがUnwrapメソッドをサポートしていてラップされた状態になっていることも確認しています。(Is関数の仕様)

エラーの原因を特定するためには、元々のエラーの発生箇所を知りたいので、それがわからなくなってしまったり、余分な情報がついて読みづらくなるのはできることなら避けたいところです。

errors.WithMessage関数を使用した場合

命名からは一見わかりませんが、errors.WithMessage関数もエラーをラップしてくれるので、これを使用して作成したエラーの挙動も同様のコードを書いて確認してみます。

コード

package main

import (
    errs "errors"
    "fmt"
    "github.com/pkg/errors"
)

func main() {
    err := errors.New("error")

    me := errors.WithMessage(err, "message")

    fmt.Println("### Error() ###")
    fmt.Printf("%v\n\n", me.Error()) 

    fmt.Println("### StackTrace() ###")
    if e, ok := me.(interface{ StackTrace() errors.StackTrace }); ok {
        fmt.Printf("%+v\n\n", e.StackTrace())
    }

    fmt.Println("### Formatter ###")
    if e, ok := me.(fmt.Formatter); ok {
        fmt.Printf("%+v\n\n", e)
    }

    fmt.Println("### Unwrap() ###")
    fmt.Printf("%v\n", errs.Is(me, err))
}

出力

### Error() ###
message: error

### StackTrace() ###
### Formatter ###
error
main.main
    /tmp/sandbox076104167/prog.go:10
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:225
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1371
message

### Unwrap() ###
true

Go Playground

こちらの出力では、スタックトレースをが上書きされずにラップできていることがわかります。ただし、StackTraceメソッドは使えないため、Formatメソッドの出力結果でスタックトレースを確認する必要があります。

まとめ

エラーをerrors.Wrap関数とerrors.WithMessage関数でラップした場合の挙動を比較してみました。StackTraceメソッドが使えないというデメリットはありますが、errors.WithMessage関数を使うとスタックトレースを上書きせずに簡単にエラーをラップできるので是非使用してみてください。

Liquid が目指すものと技術組織の取り組み

こんにちは、Liquid の CTO の大岩 ( @ooiwa ) です。

Liquid という名前を聞いたことがあるでしょうか? ご存知な方は、生体認証、eKYCという言葉を思い浮かべるかもしれません。僕たちは、LIQUID eKYC というB2B2Cのサービスを作っています。一言でいうと、「従来は窓口に行く必要があった本人確認を、スマートフォンでセルフィー撮影と身分証撮影で済むようにした」サービスです。このブログ開設の日からちょうど2年前にあたる2019年7月1日にリリースされ、金融口座の開設から、携帯電話の契約、C2Cサービスでの本人確認など、幅広く使われています。

それを運営するLiquidは、「認証を空気化し、滑らかな世界をつくる」ことを目指しています。LIQUID eKYC の実現により、このビジョンへ少し近づくことができました。窓口や郵送の必要がなくなり、新しくサービスを利用するまでの時間を短縮できました。

目指すべき世界は更に滑らかなものを想像します。例えば、サービスを利用するまでの時間を短縮できたものの、より待ち時間を少なくできないか。口座作成時のみならずログイン時や端末を変えたときなども、パスワードなしにスムーズに利用ができないか。

f:id:ooiwa:20210630151435p:plain
公式HPでは、我々が抱く未来のイメージを紹介しています。

そこに近づくために、Liquidのエンジニアたち、リサーチャーたちは何に取り組んでいるのか? 第1回テックブログでは、その課題と取り組みの一部を紹介していきます。

LIQUID eKYC を末永く続けるための改善

今日で2周年を迎えたLIQUID eKYCですが、

  • セキュリティルームの構築を含む高いセキュリティ
  • いち早いスマートフォンWEBでの対応
  • リリースから今までの離脱率の改善

他にも多くのことを達成できており、ここまで至れたチームを誇りに思います。

一方で、機能追加と安定性を重視して進んできた結果、運用負荷への対応が後回しになってしまったり、当初の想定を超えた大幅な仕様変更に苦労するところも出てきています。コードベースやパフォーマンスの改善、チームとして取り組めるようなレベルアップと、一歩ずつ先に進んでいくことに取り組んでいます。

そして、このサービスは、金融を含めた様々な業界を支える認証のインフラとして、至極当たり前ですが今後長く存在し続けます。技術の発展に伴い、より楽に使えて、より高セキュリティに成長をし続ける予定です。また僕たちは、eKYCをビジョンの実現のための起点と考えています。本人確認された情報をもとに、様々なサービスが作られていくことを目指しています。データの持ち方のありようや、セキュリティのレベルを離散的に上げられる技術も検討していきます。

f:id:ooiwa:20210630151650p:plain
Miro でのデザイン協議

f:id:ooiwa:20210630151716p:plain
ある日のeKYCバックエンドおしゃべりのメモ

不正検知技術の向上

鍵となる技術の1つが、不正検知技術です。

今のeKYCでは、セルフィーを撮影する際に顔を動かしてもらったり、免許証の斜め面をとってもらったりと、少しユーザーに負担をお願いしています。また、オペレーターの管理画面での確認の時間もかかります。大多数のユーザーには関係ないものの、ごく一部の不正をチェックするためにこのプロセスが存在しています。逆に、不正を検知する技術が上がれば、それぞれの負担を軽減し、今までコストが高くてできなかった用途でも楽に認証ができる可能性があります。

Liquidでは、顔認証、画像処理による偽造顔検知(※1)、端末の使いまわし検知(※2)のあわせ技でこの問題に取り組んでいます。データのあり方の検討から、スマートフォンアプリやWEBでのUIの検討と作成、データの作成、モデルの検証、実データでの評価とループを回しています。

※1 特許出願済 2020-173696 ※2 特許出願済 2020-177094​

f:id:ooiwa:20210630151948p:plain
左から本物(Bonafide)の大岩、ディスプレイ上の大岩、お面の大岩、face swap された大岩

eKYC の次の認証を空気化する新規事業

そして、eKYCで登録された情報をどう活用し、さらなる価値を作り出していくかが、新規事業のチームの課題となります。

eKYCで顔撮影をして口座登録をしたら、ログインや送金時の認証も顔でできないでしょうか。パスワードを忘れても、面倒なやり取りで2,3日待つことなく、安全に即座に復帰ができないでしょうか。登録後のアカウントの引き渡しを防ぐすべはないでしょうか。

  • 法律や、世界の標準化の動きの把握
  • 顔認証や不正検知の技術の精度や振る舞いの見極め
  • 高いセキュリティの担保とユーザーの同意
  • ユーザーや事業者のニーズの開拓

と多くのことを考慮し、さらにこれを高速で検証しなければならず、もう一つスタートアップを作るようなものになっています。

特にここは圧倒的に人が足りないのですが、今中心メンバーとなってくれる方を絶賛募集しています。

おわりに

今回は課題と取り組みの一部を紹介しました。次回からは、様々な情報発信をして、どうこのビジョンに向かっているのか、またどんな人達がいるのかを知ってもらえればと思います。

こんな記事を書く予定です。

  • LIQUID eKYC を支える技術の紹介
  • メンバーが日々学び試している技術の紹介
  • 社内の議論のメタファーとしてよく出る本やアニメの紹介

そんなLiquidでは、一緒に技術のチャレンジをしてくれる仲間を探しています。

  • 目指すべき世界にワクワクする人
  • 技術の取り組みに興味がある人
  • (これからのブログ記事で)中の人に興味を持った人

お話しましょう、ぜひ気軽にこちらからご連絡ください!

liquidinc.asia