0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

変数の有無をnilで判断しない

Posted at

前置き

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 *typef(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に対応されるかは、以下のリポジトリのページで確認しよう。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?