型・リテラル

Go言語におけるnil stringの実現方法について解説

Go言語では、string型は初期化時に空文字が設定されますが、特定のAPI開発などでは値が未設定である状態を明示する必要があります。

この記事では、ポインタ型やカスタム型を用いてnilを扱う方法について、シンプルに解説します。

string型の基本と挙動

string型のゼロ値(空文字)の特性

Go言語では、変数の初期化時に値を設定しない場合、string型には空文字が自動的に設定されます。

つまり、宣言のみの場合は変数は空文字という既定の値を持つため、nilとは異なる扱いになります。

たとえば、次のように変数を宣言した場合、変数textには空文字が代入されていることが確認できます。

package main
import "fmt"
func main() {
	// string変数の宣言時のデフォルトは空文字
	var text string
	fmt.Printf("text: '%s'\n", text) // 出力結果は空文字となります
}
text: ''

nilと空文字の明確な違い

空文字は長さが0の文字列であり、値として扱われるため、比較や連結が可能です。

一方、nilは特定の型(例えば、ポインタ、スライス、マップなど)でのみ使用される特殊な値です。

string型自体にはnilは存在せず、nilを表現したい場合はstringのポインタを利用し、値が設定されていない状態の場合にnilとなります。

以下は、ポインタ型のstringを利用してnilと空文字の違いを示す例です。

package main
import "fmt"
func main() {
	// u1はNameフィールドにnilが設定されている
	u1 := &struct {
		Name *string
	}{}
	fmt.Printf("u1: %+v\n", u1)
	// u2はNameフィールドに空文字が設定されている
	name := ""
	u2 := &struct {
		Name *string
	}{Name: &name}
	fmt.Printf("u2: %+v\n", u2)
}
u1: &{Name:<nil>}
u2: &{Name:0xc000010230}

nil stringが求められる背景

API開発におけるリクエストパラメータの課題

APIのリクエストパラメータでは、ユーザが意図的に空文字を送信している場合と、そもそもパラメータが送信されなかった場合を区別する必要があります。

たとえば、ユーザ情報を更新するAPIにおいて、Nameフィールドが送信されなかった場合と、空文字が送信された場合で別々に処理させる方が望ましいケースがあります。

このような場合、stringのゼロ値である空文字ではなく、nilの状態を表現できるように実装する必要があります。

未設定状態を示す必要性

未設定状態を明示的に区別することは、API内部でのバリデーションやエラーハンドリングの際に有用です。

例えば、空文字では有効な入力とみなすが、値自体が送信されなかった場合はエラーを返す、といった処理を実装するケースが考えられます。

そのため、string型でありながらnilの状態を管理できる仕組みが求められているのです。

nilを扱うためのアプローチ

ポインタ型による実装方法

string型自体はnilの概念を持たないため、stringのポインタを利用することで、値が設定されていない場合にnilを保持できるようになります。

以下は、ユーザ入力を管理する構造体において、Nameフィールドをポインタ型に変更した例です。

package main
import "fmt"
// UserInput構造体はNameフィールドをポインタ型にしているため、
// 値が設定されていない場合はnilとして扱うことができます。
type UserInput struct {
	Name *string
}
func main() {
	// 1. パラメータが送信されなかった場合(nil状態)
	u1 := &UserInput{}
	fmt.Printf("u1: %+v\n", u1) // Nameはnilとなる
	// 2. パラメータが空文字の場合
	empty := ""
	u2 := &UserInput{Name: &empty}
	fmt.Printf("u2: %+v\n", u2) // Nameは空文字が設定されたポインタを参照する
}
u1: &{Name:<nil>}
u2: &{Name:0xc000010230}

カスタムNullable型の導入例

より洗練された方法として、カスタムのNullable型を作成して、値の有無を明示できる仕組みを導入する方法があります。

database/sqlパッケージで提供されるNullXxx型と同様の実装で、string型用のNullable型を作成する例を以下に示します。

package main
import "fmt"
// NullableStringはstring型の値と、その値が設定されているかどうかのフラグを保持します。
type NullableString struct {
	value *string
	isSet bool
}
// Valueは値が設定されている場合、その値を返す。未設定の場合は空文字を返す。
func (ns *NullableString) Value() string {
	if !ns.isSet {
		return ""
	}
	return *ns.value
}
// Setは新たな値を設定し、フラグをtrueにする
func (ns *NullableString) Set(val string) {
	ns.value = &val
	ns.isSet = true
}
// Unsetは値を解除し、フラグをfalseにする
func (ns *NullableString) Unset() {
	ns.value = nil
	ns.isSet = false
}
// IsNullは値が未設定かどうかを判定する
func (ns *NullableString) IsNull() bool {
	return !ns.isSet
}
// NewNullableStringは値が設定されたNullableStringを返す
func NewNullableString(val string) NullableString {
	return NullableString{value: &val, isSet: true}
}
// UserInputCustomはNullableStringを利用して入力値の状態を管理する例です。
type UserInputCustom struct {
	Name NullableString
}
func main() {
	// 1. Nameフィールドが未設定の場合
	u1 := &UserInputCustom{}
	fmt.Printf("u1: %+v\n", u1) // Nameは未設定状態
	// 2. Nameフィールドに空文字が設定されている場合
	u2 := &UserInputCustom{Name: NewNullableString("")}
	fmt.Printf("u2: %+v\n", u2) // Nameは空文字が設定された状態
}
u1: &{Name:{value:<nil> isSet:false}}
u2: &{Name:{value:0xc000010230 isSet:true}}

実装例と注意点

サンプルコードの解説

ここまで紹介した実装例では、ポインタ型とカスタムNullable型のそれぞれの特徴がわかるようになっています。

・ポインタ型を利用すると、直接nilと空文字を区別できるためシンプルですが、値にアクセスする際に必ずnilチェックを加える必要があります。

・カスタムNullable型の場合は、内部にisSetフラグを持たせることで、値が設定されたかどうかを明示でき、意図しないnil参照による実行時エラーを防ぐことができます。

どちらの方法も、APIのリクエストパラメータとして未設定状態と意図的な空文字を区別する際に有効ですが、コードの保守性やチームの開発環境に合わせて選択することが望ましいです。

nilチェックで陥りがちな落とし穴

ポインタ型を利用する場合、値にアクセスする前に必ずnilチェックを行わなければなりません。

例えば、直接*u.Nameで値を参照すると、u.Namenilの場合にプログラムがパニックを引き起こします。

以下は、nilチェックを正しく行わなかった例と、その修正方法を示します。

package main
import "fmt"
type UserInput struct {
	Name *string
}
func main() {
	u := &UserInput{}
	// 直接参照すると、Nameがnilの場合にパニックが発生する
	// fmt.Println("Name:", *u.Name) // 以下のようにnilチェックが必要
	if u.Name != nil {
		fmt.Println("Name:", *u.Name)
	} else {
		fmt.Println("Name is nil")
	}
}
Name is nil

また、カスタムNullable型を利用する場合も、値にアクセスする前にIsNull()メソッドで状態を確認することが大切です。

たとえば、if !u.Name.IsNull() { ... }のように記述することで、未設定状態での意図しない動作を回避できます。

このように、どちらの実装方法にしても、nilチェックが抜けると実行時エラーが発生する可能性があるため、十分に注意する必要があります。

まとめ

本記事では、Go言語におけるstring型のゼロ値とnilの違い、nilを扱うためのポインタ型とカスタムNullable型の実装方法について解説しました。

総括すると、API開発での未設定状態の判別に有効なアプローチを理解できる内容となっております。

ぜひ、実際のプロジェクトに取り入れて試してみてください。

関連記事

Back to top button
目次へ