Golangのデフォルト値とゼロ値

作成者:カランカラン
💡

質問やフィードバックがありましたら、フォームからお願いします

本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください

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のエラーが発生します(動的言語に慣れてしまったせいです)。

フィールドがnil valueを受け取れるようにするには、ポインタを使用することを検討できます。例えば:

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が発生します。panic: runtime error: invalid memory address or nil pointer dereference。初期値がnilであることは、空のポインタを表しており、空のポインタから値を取得しようとするとpanicを引き起こします。したがって、nilを使用して空値を表すことはできますが、明らかにいくつかの重大な欠陥があります。使用する前にポインタがnilでないか確認する必要があります。

さらに、この構造体がSQLの値をスキャンするために使われる場合、sql.NullStringnullパッケージを併用することができます。

sql.NullString / sql.NullInt など

もし私たちが定義した構造体をデータベースにマッピングしたい場合、Scannerというインターフェースを実装する必要があります。空のstringの場合、SQLでは値をマッピングできません。sql.NullStringScannerインターフェースを実装しています。直接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では、元の型をカスタムタイプとして定義し、そのタイプにメソッドを追加することができます。デフォルトでzero valueが出力される場合に、これをnullにするようにMarshalJSONをカスタマイズできます。以下のように:

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. デフォルト値 + タグ

Golangには、高級言語のような構文糖はありません。例えば:

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

また、便利な三項演算子もないため、デフォルト値を判断するのが難しいです。したがって、Golangでデフォルト値を実装するのは少し面倒です。以前、フォーラムで誰かが質問していましたが、議論が白熱しました。その理由は:

なぜこれには特別なサポートが必要なのか?Goには可変引数があり、そこから呼び出しの種類を判断し、欠落している引数のデフォルト値を供給できるからです。あるいは、nil-1、0などの範囲外の値を必須引数に渡してデフォルト値を要求することもできます。これはCの伝統的な手法であり、関数固有の呼び出し規則だけでは不十分だと思います。myfunc(nil, nil, nil, nil, nil)と書く方が、myfunc(, , , ,)と書くよりも「すべてのデフォルト値に従って実行する」と言うことができるので、プログラムを開発しているときに、引数を完全に欠落させたのか、デフォルト値を要求したのかが混同されることがあります。これにより、デフォルト値が全体のプログラムに適さない場合に、静かなエラーが発生する可能性があります。(コードベースが成長するにつれて、他のパッケージで使用するコードのデフォルトを常に制御できるわけではありません。)特別なトークンを「デフォルト値」を表すために追加する必要性も理解できません。これを追加しなくても、より多くのシンボルを言語に追加することなく達成できます。

理由は、Googleはあなたがデフォルト値を機能させるためだけにfunc(nil, nil, nil, nil)のように多くの引数を渡すことを望んでいないからです。また、動的なargs…を使用して、引数の数や型を計算することができるため、デフォルト値を使用する必要はありません。

しかし、本当にデフォルト値が必要な場合はどうすればいいでしょう?初期化時に0や""などではなく、デフォルト値が欲しい場合は、Golangの組み込みタグ機能を試してみることができます。以下のように定義できます:

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

このようにしてデフォルト値を実現できますが、完全な機能を持つタグを実装するためには考慮すべきことが多くあります。後日、もう一度実装してみるつもりです。

結論

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


この翻訳は、日本の読者に響く自然な言語で書かれており、SEOに友好的な内容を保ちながら、元の意味を維持しています。

この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨

Buy me a coffee