🐙

Goでgenericsを使って関数を共通化したり便利な関数作る

2022/12/04に公開約2,200字

やること

  • 文字列や数値のポインタを返す便利な関数を作る
  • generics を使って複数の型を受け取り処理を共通化した関数を作る

今更感がありますが、色々調べたことをまとめます。

文字列や数値のポインタを返す便利な関数を作る

例えば、一部のフィールドで nil を許容したいためにプリミティブ型のポインタを受け取るフィールドがあるとします。

type User struct {
	ID   int
	Name string
	Age  *int
}

この構造体を使うときに以下のように数値を直接ポインタとして渡すことはできないため

func main.go() {
	param := User{
		ID: 1,
		Name: "saitooooooo",
		Age: &31,
	}
}

このような T 型を許容する引数 x の関数を作って、そのままポインタを返す関数を作ります。

func Ptr[T any](x T) T {
	return &x
}

使い方は以下の通りです。

func main.go() {
	param := User{
		ID: 1,
		Name: "saitooooooo",
-		Age: &31,
+		Age: Ptr(31),
	}
}

ちなみに[T any](x T) *Tとしないで(x any) *anyでもいいんじゃないと思ったのですが、こちらでは型が合わないためコンパイルできません。

NG例
func Ptr(x any) *any {
	return &x
}

func main() {
	param := User{
		ID: 1,
		Name: "saitooooooo",
		Age: Ptr(31), // [gopls] IncompatibleAssign: cannot use Ptr(31) (value of type *any) as *int value in struct literal
	}
}

複数の型を受け取り処理を共通化した関数を作る

先ほどの例で使った構造体を拡張します。

type User struct {
	ID   int
	Name string
	Age  *int
}

type A struct {
	User User
}

type B struct {
	User    User
	Country *string
}

例えば、type Atype BUser構造体を持つフィールドがあり、

  • User構造体Ageが nil かチェックする
  • B構造体を受け取った時はCountryが nil かチェックする

のような処理を実装したい場合にAgeの nil チェックは、A 構造体だろうと B 構造体だろうと同じ処理を使いたい状況だとします。
そのような状況で以下のように実装するとエラーメッセージを共通化することができます。

func validateParam[T A | B](param T) error {
	switch v := any(param).(type) {
	case A:
		if err := validateUserAge(v.User); err != nil {
			return err
		}

	case B:
		if err := validateUserAge(v.User); err != nil {
			return err
		}
	}

	return nil
}

// こちらでUser構造体のAgeをチェックする
func validateUserAge(param User) error {
	if param.Age == nil {
		return xerrors.Errorf("User.Ageは必須です")
	}
	return nil
}

ここにB構造体の時だけCountryの nil チェックを実装します。

func validateParam[T A | B](param T) error {
	switch v := any(param).(type) {
	case A:
		if err := validateUserAge(v.User); err != nil {
			return err
		}

	case B:
		if err := validateUserAge(v.User); err != nil {
			return err
		}
+		if v.Country == nil {
+			return xerrors.Errorf("Countryは必須です: %+v", v)
+		}
	}

	return nil
}

これがどういう時に使えるかと言うと、
登録と更新の API で同じパラメータ+更新だけ ID が必要みたいな状況で
同じバリデーションロジックを使うことができるようになります。

Discussion

ログインするとコメントできます