前置き
GETのクエリ文字列を構築するときやDBの検索条件などで、オプショナルな値によってクエリ文字列やSQLの組み立て方を変える場合がある。
その時に、ポインタを使って、nilかどうかを判断する方法がたまに見られる。やめた方がいいと思っている。
関数内で上書きできるので、呼び出しもとで使っていた値が変わる可能性がある。書き換える必要があるのなら、想定通りの動きなら、問題はないが。
以下のような動きになる危険性がある。
func main() {
now := time.Now()
fmt.Println(now.String()) // original
pointerFunc(nil, &now)
fmt.Println(now.String()) // add 24 hours
}
func pointerFunc(since, until *time.Time) {
query := url.Values{}
if since != nil {
query.Set("since", since.Format(time.RFC3339Nano))
}
if until != nil {
query.Set("until", until.Format(time.RFC3339Nano))
*until = until.Add(24 * time.Hour)
}
}
じゃあどうするのか?
解決案
nullableな値を使おう。
database/sql
には、NullInt64
がある。SpannerやBigQueryにもNullInt64
はある。データベースに、nullableのフィールドがある際に使用される。
とはいえ、これはデータベースとのやりとりで使用されるものであるし、任意のnullable typeではない。
なので、自分でパッケージを作った。任意のtypeに対するnullableが使えるようにした。
「ポインタを使って、nilでないならば〜」が無くなる。
「f(&v)
かf(nil)
で、オプショナルな値を渡して〜」が無くなる。
「var v *type
でf(v)
で、オプショナルな値を渡して〜」が無くなる。
以下、パッケージの説明
(この記事よりもGitHubにあるコードの方がアップデートされやすいので、これは、現時点での情報)
go get github.com/taniko/nullable
使い方(READMEより)
package main
import (
"fmt"
"github.com/taniko/nullable"
)
func main() {
var v nullable.Nullable[string]
fmt.Println(v.IsNull()) // true
v = nullable.New("text")
fmt.Println(v.IsNull()) // false
fmt.Println(v.Value()) // text
}
こんな感じで書くことができる。
最初に書いたオプショナルなsince, untilをnullableで書き直す。
ポインタがなくなった。これで、どこかで値を書き換えられる心配は無くなった。
package main
import (
"net/url"
"time"
"github.com/taniko/nullable"
)
func main() {
var since, until nullable.Nullable[time.Time]
until = nullable.New(time.Now())
f(since, until)
}
func f(since, until nullable.Nullable[time.Time]) {
query := url.Values{}
if !since.IsNull() {
query.Set("since", since.Value().Format(time.RFC3339Nano))
}
if !until.IsNull() {
query.Set("until", until.Value().Format(time.RFC3339Nano))
}
}
「任意の型で〜」と言ったので、当然ながら以下のようにできる
package main
import (
"fmt"
"github.com/taniko/nullable"
)
type User struct {
ID string
Name string
}
func main() {
var user nullable.Nullable[User]
fmt.Println(user.IsNull())
user = nullable.New(User{
ID: "id",
Name: "name",
})
fmt.Println(user.IsNull())
}
ついでに、json.Marshalの際には、nullになる。
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
type Data struct {
User nullable.Nullable[User] `json:"user"`
}
func main() {
var data Data
if err := marshal(data); err != nil {
panic(err)
}
data.User = nullable.New(User{
ID: "123",
Name: "abc",
})
if err := marshal(data); err != nil {
panic(err)
}
}
func marshal(data Data) error {
b, err := json.Marshal(data)
if err != nil {
return err
}
fmt.Println(string(b))
return nil
}
{"user":null}
{"user":{"id":"123","name":"abc"}}
注意事項
現在stableなgomock(v1.6)では、genericsがサポートされていないので、このnullableパッケージは使用できない。
ただし、直接、引数や返り値で指定できないだけで、struct内にあるフィールドとしてnullableが使用されている分には問題ない。
ちなみに、genericsのサポートは、v1.7.0で入る予定(v1.7.0-rc.1)だったが、長らくstableの更新がされず。また、最近、gomockのパッケージが、github.com/golang/mock
からgo.uber.org/mock
へと移行された。
いつgenericsに対応されるかは、以下のリポジトリのページで確認しよう。