【Go言語】int型やstring型にnilを含める方法
Go言語では、変数の初期化時に値を設定しない場合、ゼロ値と呼ばれるデフォルト値が自動的に設定される。(int型:0、string型:空文字 etc)
ただ、場合によっては未設定(nil) なのか0もしくは空文字を設定したのか明確に区別したいときがある。
ユースケースを1つ挙げると、ユーザからのリクエストを受け付けた値を保持する際、実際に0をリクエストしていたのか、そもそもリクエストされてないのか、というケースがあった。
例えば、支払いに関するリクエスト内容を保持する構造体に「支払い金額」「使用ポイント」がある場合、「使用ポイント」は0がリクエストされる場合はOKとし、リクエストされない場合はエラーとしたい。
package main
import "fmt"
type PaymentInput struct {
PaymentTotal int
Point int
}
func main() {
i1 := &PaymentInput{PaymentTotal: 1000}
fmt.Printf("i1 : %+v\n", i1) // i1 : &{PaymentTotal:1000 Point:0}
i2 := &PaymentInput{PaymentTotal: 1000, Point: 0}
fmt.Printf("i2 : %+v\n", i2) // i2 : &{PaymentTotal:1000 Point:0}
i3 := &PaymentInput{PaymentTotal: 1000, Point: nil} // cannot use nil as int value in struct literal
fmt.Printf("i3 : %+v\n", i3)
}
その場合、上記で示したコードのi1
とi2
は違う状態であってほしいが同じになってしまい、nilを持たせることもできない。
そういったケースでnilを扱う方法を2つ紹介する。
その1 : ポインタ型にする
1つ目の方法はポイント型に変更する。
package main
import "fmt"
type PaymentInput struct {
PaymentTotal int
Point *int // ポイント型に変更
}
func main() {
i1 := &PaymentInput{PaymentTotal: 1000}
fmt.Printf("i1 : %+v\n", i1) // i1 : &{PaymentTotal:1000 Point:<nil>}
p2 := 0
i2 := &PaymentInput{PaymentTotal: 1000, Point: &p2}
fmt.Printf("i2 : %+v\n", i2) // i2 : &{PaymentTotal:1000 Point:0x14000096028}
i3 := &PaymentInput{PaymentTotal: 1000, Point: nil}
fmt.Printf("i3 : %+v\n", i3) // i3 : &{PaymentTotal:1000 Point:<nil>}
}
この方法のメリットは言語仕様そのままで、特別な仕組みを追加してない点が1つ挙げられる。
ただ、「ポイント型の場合は未設定とゼロ値を分けたい項目である」という暗黙的なルールを含み、コードからその意図が読み取りづらい。
また、値を参照するにnilチェックが必要となり、書き忘れると実行時にエラーとなる可能性を残してしまう。
i := &PaymentInput{PaymentTotal: 1000}
if i.PaymentTotal < *i.Point { // panic: runtime error: invalid memory address or nil pointer dereference
fmt.Println("使用ポイントが支払い金額を超過しているエラー")
}
その2 : 型を拡張し設定の有無を示すbool型を持たせる
2つ目の方法が型を拡張し設定の有無を示すbool型を持たせる。
database/sql
パッケージでNullXxx
という構造体が用意されており、DBから取得した値がnullである場合に使用する型である。
type NullInt32 struct {
Int32 int32
Valid bool // Valid is true if Int32 is not NULL
}
// https://pkg.go.dev/database/sql#NullInt32
これと同じ仕組みを導入する。
package main
import "fmt"
type PaymentInput struct {
PaymentTotal int
Point NullableInt
}
type NullableInt struct {
value *int
isSet bool
}
func (v *NullableInt) Value() int {
if !v.isSet {
return 0
}
return *v.value
}
func (v *NullableInt) Set(val int) {
v.value = &val
v.isSet = true
}
func (v *NullableInt) Unset() {
v.value = nil
v.isSet = false
}
func (v *NullableInt) IsNull() bool {
return !v.isSet
}
func NewNullableInt(val int) NullableInt {
return NullableInt{value: &val, isSet: true}
}
func main() {
i1 := &PaymentInput{PaymentTotal: 1000}
fmt.Printf("i1 : %+v\n", i1) // i1 : &{PaymentTotal:1000 Point:{value:<nil> isSet:false}}
i2 := &PaymentInput{PaymentTotal: 1000, Point: NewNullableInt(0)}
fmt.Printf("i2 : %+v\n", i2) // i2 : &{PaymentTotal:1000 Point:{value:0x1400012c038 isSet:true}}
}
NullableInt
という型を新たに作り、nilの有無をisSet
に持たせるようにしている。
基本的にdatabase/sql
パッケージのNullXxx
と同じだが、nilチェックを行うIsNull
メソッド、再設定用のSet
、Unset
メソッドなど必要な機能を持たせている。
これにより、コードの見通しが良くなり、安全にポインタを扱うことができる。
if i.Point.IsNull() {
fmt.Println("入力エラー")
}
if i.PaymentTotal < i.Point.Value() {
fmt.Println("使用ポイントが支払い金額を超過しているエラー")
}
どちらを採用するか
簡単なツールなど比較的コード量が少ない場合であれば、その1のパターンで事足りると思うが、複数人で開発する規模であれば、その2のような仕組みを導入したほうが良いだろう。
Discussion