この記事は 2021年Goアドベントカレンダー2 の11日目の記事です。
はじめに
今年はGoでのWebアプリケーション開発をがっつりしてきました。
今回はそんな実開発で使って/見て学んだ無名関数の利用パターンを書いておこうと思います。
無名関数(Anonymous function)とは
関数を変数として扱うものです。
こんな感じで使えます。
hello_anonymous_function.go
func main() {
// 引数なし
sayWorld := func() {
fmt.Println("world")
}
fmt.Println("hello")
sayWorld()
// 引数あり
say := func(str string) {
fmt.Println(str)
}
say("hoo")
say("bar")
// 即時実行
func(str string) {
fmt.Println(str)
}("hoge")
}
// Output:
// hello
// world
// hoo
// bar
// hoge
他にもClosureという機能もありますが実開発で使わなかったので本記事では触れてないです。
利用パターン
前述のような「機能」の説明は多く見かけますが
どんなシーンで使えるのか、ということにフォーカスしていきます。
パターン1: 引数で利用
DIの実現方法の一つ、メソッドインジェクションでの利用です。
method_injection_test.go
func wrap(
ctx context.Context, // 実践的には context を設定することが多いので例でも記載。
fn func(ctx context.Context) error, // ここがインジェクションする部分。
) error {
fmt.Println("do pre-process")
if err := fn(ctx); err != nil {
return err
}
fmt.Println("do post-process")
return nil
}
func hello(ctx context.Context) error {
fmt.Println("hello world")
return nil
}
func ExampleMethodInjection1() {
ctx := context.Background()
// 定義済の関数を引数に渡すことができる。
// 戻り値のerrorハンドリングは説明のため省略。
wrap(ctx, hello)
// Output:
// do pre-process
// hello world
// do post-process
}
func ExampleMethodInjection2() {
ctx := context.Background()
hello := "hello world"
// 引数部分にも無名関数を使うと関数外の変数(この場合はhello)も利用できる。
wrap(ctx,
func(ctx context.Context) error {
fmt.Println(hello)
return nil
})
// Output:
// do pre-process
// hello world
// do post-process
}
テストでよく使うt.Run はこの形式ですね。
利用シーン
- DBのSELECTやAPIのレスポンスを1件1件処理するような callback方式。
- OpenTeremetryのspanの開始/終了やDBのトランザクションの開始/終了など、お決まりの処理を共通化&隠蔽。
- DDD (というかレイヤードアーキテクチャ) において、メソッドを渡すことでレイヤを守る。
- infra層でusecase層のメソッドを実行する(これはcallback方式)。
- domain層でinfra層のメソッドを使う。
- domain層で「IDを渡してエンティティを取ってくる」というロジックを使いたい場合など。
パターン2: 戻り値で利用
引数があれば戻り値も、ということで。
postponed_test.go
// returnMethod はmain-process を実行後、postponed 関数をreturnする。
func returnMethod() func(context.Context) error {
fmt.Println("main-process")
return func(ctx context.Context) error {
fmt.Println("postponed")
return nil
}
}
func ExamplePostponed() {
ctx := context.Background()
postponed := returnMethod()
postponed(ctx)
// Output:
// main-process
// postponed
}
利用シーン
setup時に deffer file.Close()
のような後処理が必要なメソッド返して利用側で deffer してもらう。
return_close_method.go
func setupDB() (*db.RDB, func()){
// いろんな設定
rdb := db.Open()
return rdb, rdb.Close()
}
「コミット後にエンキューする」という処理を実装したい時にエンキューロジックもトランザクション内のメソッドに書いておくことができる(コードの見通しがよい)。
- パターン1でトランザクションを実装していた場合に、ロジック記載箇所を1メソッド内にまとめられます。
enqueue_after_commit_test.go
func RunInTransaction(fn func()) {
fmt.Println("begin")
fn()
fmt.Println("commit")
}
func ExamplePostponed2() {
var postponed func()
RunInTransaction(
func() {
fn := exec()
postponed = fn
})
postponed()
// Output:
// begin
// save data
// commit
// enqueue
}
// exec はいろんなデータ加工と保存を行う。
// エンキューするロジック含めてここに全て処理が載ってる。
// "コミット後の処理が必要"という知識も表現できる。
func exec() func() {
// いろんな処理
fmt.Println("save data")
return func() {
fmt.Println("enqueue")
}
}
パターン3: メソッド内で利用
メソッド内で定義して、そのままメソッド内で使うケースです。
利用シーン
メソッド内で何度も使うため、メソッド内で簡易的につくる。
- 言葉の通り&一般的な使い方なのでサンプル割愛します。
早期リターンを使ってifネストを浅くする
decreate_nest_if.go
// manyIf はめちゃIf多い。
func manyIf() error {
if x != nil {
if x.A {
if err := x.ExceA(); err != nil {
return err
}
} else if x.B {
if err := x.ExceB(); err != nil {
return err
}
} else {
if err := x.ExceOther(); err != nil {
return err
}
}
}
if y != nil {
if y.C {
if err := y.ExceC(); err != nil {
return err
}
} else if y.D {
if err := y.ExceD(); err != nil {
return err
}
} else {
if err := y.ExceOther(); err != nil {
return err
}
}
}
return nil
}
// decreaseNestIf は無名関数でIf減らした。
func decreaseNestIf() error {
if err := func()error{
if x == nil {
return nil
}
if x.A{
return x.ExecA()
}
if x.B{
return x.ExecB()
}
return x.ExecOther()
}(); err != nil {
return err
}
if err := func() error {
if y == nil {
return nil
}
if y.C {
return y.ExecC()
}
if y.D {
return y.ExecD()
}
return y.ExecOther()
}(); err != nil {
return err
}
return nil
}
意味でくくる、変数スコープを小さくする。
- プロダクトコードよりもテストコード、サンプルコード向けのTipsです。
- テストコードの
t.Run()
をこの目的で使ったり。 - サンプルコードで長ーい処理を書く時にインデントがついて見やすくなるので使ったりします。
inline_anonymous_function_test.go
func ExampleInlineAnonymousFunctoin() {
// xxxx をする。
func() {
a := "hello"
fmt.Println(a)
}()
// yyyy をする(xxxxの処理、yyyyの処理の範囲が明確)。
func() {
// 無名関数内のスコープなのでaを再定義できる。
// a1,a2 とかにしなくてよい。
a := "world"
fmt.Println(a)
}()
// Output:
// hello
// world
}
パターン4: structフィールドで利用
利用シーン
プライベートメソッドのMock化
- レイヤードアーキテクチャでいう、usecaseのロジックがモリモリになってくるとテストケースを書くのがしんどくなってきます。
- 関連するrepository、usecaseなどのMockを全部定義してテストもしんどい。。。
- こういった時にはusecaseのフィールドに無名関数を持たせることで個々のテストに分割することがあります。
- 以下のサンプルコードのように実装すると、Execテスト時には無名関数部分は全てMock化してテスト、個々のImplメソッドは個別にテスト、とすることでテストケースを作りやすくなります。
- 若干可読性は下がるので自分は複雑な機能のみ使ってます。
- Execメソッドをロジック単位にまとめ直すリファクタリングが必要になったので、結果的にExecの見通しが良くなったりしました。
abstract_method.go
type Usecase struct {
fetchAndValidate func(string) (int, error)
loggic1 func(int) (string, error)
loggic2 func(string) (string, error)
loggic3 func(string) error
}
func NewUsecase() *Usecase {
uc := &Usecase{}
uc.fetchAndValidate = uc.fetchAndValidateImpl
uc.loggic1 = uc.loggic1Impl
uc.loggic2 = uc.loggic2Impl
uc.loggic3 = uc.loggic3Impl
return uc
}
func (u *Usecase)Exec(s string)error {
num, err := u.fetchAndValidate(s)
if err != nil {
return err
}
str1, err := u.loggic1(num)
if err != nil {
return err
}
str2, err := u.loggic2(str1)
if err != nil {
return err
}
return u.loggic3(str2)
}
func (u *Usecase) fetchAndValidateImpl(s string) (int, error) {
// 色々複雑なロジック
return 0, nil
}
func (u *Usecase) loggic1Impl(num int) (string, error) {
// 色々複雑なロジック
return "", nil
}
func (u *Usecase) loggic2Impl(s string) (string, error) {
// 色々複雑なロジック
return "", nil
}
func (u *Usecase) loggic3Impl(s string) error {
// 色々複雑なロジック
return nil
}
終わりに
無名関数で可読性が向上したり、テストしやすくなったりと、結構無名関数好きになってきました。
節度を守りつつ快適な無名関数ライフを送るための一助になれば幸いです。