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 を使用する際に遭遇する可能性のある問題についてまとめ、その解決策を提示してみた。
関連記事
- 測定が目標になるとき:窓税からPull Request数まで かつて僕は小さなツールを自作し、四半期で自分がどれだけPRに貢献したか、レビューコメントをどれだけ残したか、チケットをどれだけ消化したかを集計して、上司にアウトプットを証明しようとしたことがある。上司は淡々と、評価はアウトプットだけで見るものではないと言った。数年後、僕はようやく理解した――測定が目標になるとき、それはもはや良い測定ではなくなるのだ。英国の窓税、ハノイのネズミ駆除の報奨金から、現代のPR数による開発者評価に至るまで、そのメカニズムはまったく同じだ。
- Cloudflare Images を画像ストレージ・変換ソリューションとして使う ウェブページに画像を1枚置くのはフロントエンドにとって最も簡単なことだが、リサイズや各種フォーマットの生成、さらにはトラフィックの負荷に耐えることまで完璧にやろうとすると、実際には一つの包括的なソリューションが必要になる。僕はその後、すべて Cloudflare Images に任せるようになり、オリジナル画像1枚だけを渡すようにしている。
- もう AWS Access Key を使うのはやめよう Access Key は AWS において見落とされがちなセキュリティリスクだ。OIDC と IAM Role を組み合わせることで、GitHub Actions にシークレットを一切保持させることなく、安全に AWS リソースを操作できるようにする。
- データベース主キー:AUTO_INCREMENT、UUID、そしてUUIDv7 バックエンド開発で度々直面する主キーの決定。auto incrementを使うべきか、それともUUIDか?衝突への懸念は?UUIDv7とcreated_at + インデックスの性能差はどれほどか?実際に2,000万件のデータで検証したベンチマークと設計上の意思決定を解説する。