何がしたいか
Perl や JavaScript / TypeScript など様々な言語に左辺が null / undefined なら右辺を評価するというような演算子が存在します。
# Perl my $page = $input->{page} // '1';
// JavaScript const page = input.page ?? 1;
これと同じようなことを Go で書きます。 同じといっても演算子を定義することはできないので関数を用意します。
普通にやる
いつも通りに入力値が nil でないなら値をセットするという処理を書きます。
func main() { var input *int value := 1 // ここでデフォルトにする値をセット if input != nil { value = *input } fmt.Println(value) }
何も問題はありませんが、入力値の種類が増えるとその分同じような if 文が増えます。 哲学的には何の問題もないのかもしれませんが、増えすぎると目が滑る原因にもなるでしょう。 なによりも同じような処理なので共通化したい気持ちが出てきます。
関数に分ける
Go 1.18 からはジェネリクスが使用できるので共通化した関数を定義します。
func definedOr[T any](p *T, v T) T { if p == nil { return v } return *p }
呼び出し側は以下のようにします。
func main() { var input *int = nil value := definedOr(input, 1) fmt.Println(value) }
いかにも良さそうですが、Slice や Map などが nil の時にはこれでは対応しきれません。
reflect を使う
reflect を使って中身を見るようにします。これなら問題なさそうです。
func definedOr[T any](p *T, v T) T { if p == nil { return v } rv := reflect.ValueOf(*p) if !rv.IsValid() { return v } switch rv.Kind() { case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Chan, reflect.Func: if rv.IsNil() { return v } case reflect.Invalid: return v } return *p }
以下のようなテストを型ごとに書いて試してみると、ちゃんと動いていそうなことがわかります。
func Test_definedOr(t *testing.T) { t.Run("works with slice type", func(t *testing.T) { t.Run("nil pointer returns default value", func(t *testing.T) { var nilPtr []string = nil defaultVal := []string{"default"} result := definedOr(&nilPtr, defaultVal) assert.Equal(t, defaultVal, result) }) t.Run("non-nil pointer returns its value", func(t *testing.T) { value := []string{"actual"} defaultVal := []string{"default"} result := definedOr(&value, defaultVal) assert.Equal(t, value, result) }) }) }
感想
やりたいことは達成できたので概ね満足しました。