· 6分で読了

Golang におけるデフォルト値と zero value

この記事は中国語から自動翻訳されたものです。翻訳によりニュアンスが失われている場合があります。

Golang では、初期化時に値を代入しない場合、zero value が使用される。

しかし、しばらく使っていると、毎回 zero value で代用していると、ユーザーが値を入力しなかったために zero value になったのか、それともユーザーが元々 zero value を入力したのかが区別できなくなることに気づく。

type User struct {
    ID    string
    Email string
    Name  string
}

func main() {
    user := &User{
        ID: "1",
    }
}

この場合、Email と Name には何も入力されていないため、デフォルトではどちらも "" になる。ここまでは特に問題なさそうに見えるが、JSON にシリアライズしたいとしたらどうだろうか?

package main
import (
  "fmt"
  "encoding/json"
)
type User struct {
    ID    string `json:"id"`
    Email string `json:"email"`
    Name  string `json:"name"`
}

func main() {
    user := &User{
        ID: "1",
    }
    d, _ := json.Marshal(user)
    fmt.Println(string(d))
}

この時、以下のように出力される:

{
    "id":"1",
    "email":"",
    "name":""
}

僕たちの要件としてよくあるのは、email や name に値がない場合は "" ではなく null で表したいというケースだ。どのように修正するのが良いだろうか?

1. ポインタを使うように変更する

プリミティブ型(primitive type)の初期値は nil になり得ず、if !str {...} のような操作で判定することもできない(mismatched types string and nil というエラーが出る。動的型付け言語に慣れすぎたツケだQQ)。

そのため、フィールドで nil 値を受け取れるようにしたい場合は、ポインタの使用を検討できる。例えば:

package main

import (
  "fmt"
  "encoding/json"
)

type User struct {
    ID    *string `json:"id"`
    Email *string `json:"email"`
    Name  *string `json:"name"`
}

func ToStringPtr(str string) *string {
    return &str
}

func main() {
    user := &User{
        ID: ToStringPtr("1"),
		Email: ToStringPtr("kalan@gmail.com"),
    }
    d, _ := json.Marshal(user)
    fmt.Println(string(d))
}

この時、出力は次のようになる:

{
    "id":"1",
    "email":"kalan@gmail.com",
    "name":null
}

これでかなり良くなったように見えるが、果たしてこれは本当に良い方法なのだろうか?続けて見ていこう。

フィールドがすべてポインタになったため、user.Email で直接値を取得しようとすると:

package main

import (
  "fmt"
)

type User struct {
    ID    *string `json:"id"`
    Email *string `json:"email"`
    Name  *string `json:"name"`
}

func ToStringPtr(str string) *string {
    return &str
}

func main() {
    user := &User{
        ID: ToStringPtr("1"),
    		Email: ToStringPtr("xxx@gmail.com"),
    }
    fmt.Println(user.Email) // 0x1040c130
  	val, _ := json.Marshal(user)
}

直接値を取得しようとするとポインタのアドレスが返ってくるため、値を取得するには *user.Email のように書き換える必要がある。しかし、*user.Email を見て少しヒヤッとしないだろうか?例えば、直接 *user.Name の値を取得しようとすると:

func main() {
    user := &User{
        ID: ToStringPtr("1"),
    }
    fmt.Println(*user.Name)
}

この時、パニックが発生する。panic: runtime error: invalid memory address or nil pointer dereference。初期値が nil であり、それが null ポインタであることを意味しているため、null ポインタを参照しようとするとパニックを引き起こす。したがって、nil を使って空の値を表現することはできるものの、明らかにいくつかの重大な欠点がある。ポインタを使用する前に、それが空の値(nil)かどうかをチェックする必要があるのだ。

また、この struct を sql で値をスキャンするために使う予定なら、sql.NullString や null パッケージを組み合わせることもできる。

sql.NullString / sql.NullInt など

定義した struct をデータベースからマッピングさせたい場合、Scanner というインターフェースを実装する必要がある。string が空値の場合、sql では値をうまくマッピングできない。sql.NullString は Scanner インターフェースを実装している。これを直接 json に変換すると、次のようになる:

{
  "email": {
    "Valid": true,
    "String": "xxx@gmail.com"
  }
}

これを正しい json 形式に変換するには、さらに MarshalJSON を使用する必要がある(まあ、このままでもいいっちゃいいのだが…)。

null

null パッケージは、よく使われるデータ型をあらかじめ定義してくれており、sql や JSON と組み合わせて使用するのに便利だ。

type User struct {
    ID    null.String `json:"id"`
    Email null.String `json:"email"`
    Name  null.String `json:"name"`
}

2. MarshalJSON を再実装する

Golang では、プリミティブ型を独自の型(type)として定義し、その型にメソッドを追加することができる。MarshalJSON を自分で実装することで、デフォルトでは zero value になるはずの出力を null にすることができる。例えばこんな感じだ:

type nullString string

func (str nullString) MarshalJSON() ([]byte, error) {
	raw := string(str)
	if string(str) == "" {
		return json.Marshal(nil)
	}
	val := raw
	return json.Marshal(val)
}

func main() {
	str := nullString("")
	val, _ := json.Marshal(str)
	fmt.Println(string(val))
}

3. default value + tag

Golang には、より高水準な言語にあるようなシンタックスシュガーはない。例えば:

const getPerson = (id = 1, name = '') => (
)
def get_person(id=1, name='', is_admin=False):
    ...

また、デフォルト値の判定に役立つ便利な三項演算子(ternary operator)もない。そのため、Golang でデフォルト値を実装するのは少々面倒だ。かつてフォーラムで質問した人もいたが、却下されていた。その理由は以下の通りだ:

Why does this need additional, special support in the language? Go has … to support a variable number of arguments, from which you could figure out which type of call it means, and supply defaults for missing arguments. Or you could pass some out-of-range or zero value like nil or -1 or even 0 to required arguments to ask for a default value. This has a long tradition in C, so why are function-specific calling conventions not enough? I think I’d rather see myfunc(nil, nil, nil, nil, nil) than myfunc(, , , ,) to say “do what you do with all default values”, since while I’m developing a program it conflates whether I missed a parameter completely or meant to ask for a default value. This can cause silent errors in cases where the default value is no acceptable to the overall program. (As the code base grows we won’t always be able to control the defaults for code we use in others’ packages.) I also don’t see why it’s necessary to add a special token to represent “default value”, since we can accomplish this without adding more symbols to the language.

理由としては大体、Google 側としてはデフォルト値を効かせるためだけに func(nil, nil, nil, nil) のように大量の引数を渡すようなことを望んでおらず、可変長引数 args… を使って引数の数や型を直接計算すれば、デフォルト値を使う必要はないと考えているからだ。

しかし、本当にデフォルト値が必要な場合はどうすればいいのだろう?初期化時に 0 や "" にしたくない時はどうすべきか?その場合は、Golang 組み込みの tag 機能を試してみると良い。例えば以下のように定義する:

type People struct {
  Name string `default:""`
  Age int64 `default:10`
}

このような方法で実現できる。ただし、十分に機能する tag を実装しようとすると、考慮すべきことも少なくない。また日を改めて、実際に実装を試してみようと思う。

結論

本記事では、Golang においてデフォルト値や zero value を使用する際に遭遇する可能性のある問題についてまとめ、その解決策を提示してみた。

関連記事

他のトピックを探索