🦫

[Golang]defined typeのメリット・デメリット

2024/06/14に公開

はじめに

goで作ってるAPIサーバの文字列情報がすべてstring管理でしんどみを感じていたので、defined typeを使い始めた。
使い方について説明してるところはあるけどメリット・デメリットについて書いてるページはなさそうだったので書いていくよ。

この記事を読んで欲しい人

  • defined typeが気になってるけどイマイチ踏み込めない人
  • defined typeをなぜ使うのかよくわからない人

defined typeってなぁに?

端的に言うと「型に任意の型名をつける機能」。

type UserID string

func main() {
	var userID UserID = "abcd123"
	fmt.Printf("UserID : %s", userID)
}

// 【出力】
// UserID : abcd123

詳細はここでは割愛。

defined typeのメリット

型の制約がかかるようになるため、ミスった時にコンパイルエラーが出せる

defined typeを使わない場合

例えば「string型のUserIDと氏名を入力して標準出力する」関数がある時、
そのままstringを使うと

func main() {
	userID := "abcd123"
	name := "田中太郎"
	fmtOutput(userID, name)
}

func fmtOutput(
	userID string,
	name string,
) {
	fmt.Printf("私の名前は %s!! ユーザーIDは[%s]さ!!", name, userID)
}

// 出力
// 私の名前は 田中太郎!! ユーザーIDは[abcd123]さ!!

という感じになりますが、呼び出し側ではUserIDName引数指定の順序で管理する必要があります。
このくらいのメソッドであればそうそうミスもしないと思いますが、

func fmtOutput(
    userID string,
    name string,
    address string,
    mailAddress string,
    ...(たくさんのstring引数)
) {

ってなったらどうでしょう?僕だったら200%順序ミスります。
神の集中力やAIの力添えによってミスらないにしても「順序が合ってるか」を確認する手間があるためストレスはあるはずです。
(引数がゾロゾロ並ぶような関数設計が良く無いのはここでは棚上げでお願いします)

defined typeを使う場合

上記のプログラムについて、defined typeを利用する場合は以下のようになります。

+ type UserID string
+ type Name string

func main() {
-	userID := "abcd123"
-	name := "田中太郎"
+	var userID UserID = "abcd123"
+	var name Name = "田中太郎"
	fmtOutput(userID, name)
}

func fmtOutput(
-	userID string,
-	name string,
+	userID UserID,
+	name Name,
) {
	fmt.Printf("私の名前は %s!! ユーザーIDは[%s]さ!!", name, userID)
}

万が一、UserIDとNameを間違えて指定した場合、

func main() {
	var userID UserID = "abcd123"
	var name Name = "田中 太郎"
	fmtOutput(name, userID)
}

コンパイルエラーが出るのでビルドの時点でミスに気が付けます。

./prog.go:13:12: cannot use name (variable of type Name) as UserID value in argument to fmtOutput
./prog.go:13:18: cannot use userID (variable of type UserID) as Name value in argument to fmtOutput

独自定義型に固有のメソッドが追加できる

例えば、「名前にひらがなが含まれているか調べるメソッド」を追加する場合、

func (n Name) InHiragana() bool {
	for _, r := range string(n) {
		if unicode.In(r, unicode.Hiragana) {
			return true
		}
	}
	return false
}

でOK。
バリデーションや型変換など、型固有のロジックをカプセル化する場合にとても有用。

ginやgormでもよしなにキャストしてくれる

メリットとは言えないけど、構成の端っこにいがちなginやgormもモデルさえちゃんと設定しておけばよしなにキャストしてくれるので、defined typeに変更することでミドルウェアが障壁になることは無い。
(他のパッケージについては知らないけど内部でreflectionしてたりすれば大体意識しなくても大丈夫だと思う)

defined typeのデメリット

別の型として扱われるため、stringsパッケージなどの利用がちょっとめんどくさい

元々の型とは別の型として扱われるため、プリミティブ型を利用するようなパッケージ関数を利用する際に元の型にキャストしてやる必要があり、ちょっとだけめんどくさい。

// コンパイルエラー
strings.Contains(name, "田中")
// 実行可能
strings.Contains(string(name), "田中")

とはいえ、メリットがデカすぎるので大した問題では無い。

まとめ

defined typeを利用することで、可読性が向上しミスしても早期に発見できる。

ガンガン使おうdefined type!!

参考

https://go.dev/ref/spec#Type_inference
https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md
https://zenn.dev/nobishii/articles/defined_types

Discussion