カランのブログ

ソフトウェアエンジニア / 台湾人 / 福岡生活

今のモード ライト

golangでは、初期化時に値が与えられない場合、ゼロ値が使用されます。

しかし、しばらく使ってみると、常にゼロ値を使用すると、ユーザーが値を入力しなかったためのゼロ値なのか、ユーザーが元々ゼロ値を入力したのかを区別することができません。

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ではなく""ではなくnullを使用することです。どのように修正すればよいでしょうか?

1. ポインタを使用する

私たちは、プリミティブ型の初期値がnilではないことを知っていますし、if !str {...}のような操作を使用してもmismatched types string and nil(動的言語に慣れてしまったため)というエラーが発生します。

したがって、フィールドが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であるため、それは空のポインタを示し、空のポインタを取得しようとするとパニックが発生します。したがって、nilを空の値を表すために使用できますが、これにはいくつかの重大な欠陥があることは明らかです。使用する前にポインタが空かどうかをチェックする必要があります。

また、このstructsqlのスキャン値に使用される場合、sql.NullStringを使用するか、nullパッケージを使用することができます。

sql.NullString / sql.NullIntなど

データベースに値をマッピングするために定義したstructを使用する場合、Scannerと呼ばれるインターフェースを実装する必要があります。文字列が空の場合、値をデータベースにマッピングすることはできません。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では、基本型をカスタム型として定義し、その型にメソッドを追加することができます。MarshalJSONをカスタマイズして、デフォルトの出力がゼロ値ではなく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. デフォルト値 + タグ

golangには、他の高級言語のシンタックスシュガーやデフォルト値をサポートするテナリ演算子のようなものはありません。そのため、golangでデフォルト値を実装するには、少し手間がかかります。以前、フォーラムで質問がありましたが、回答は取得できませんでした。その理由は次のようなものです:

なぜこれに対して言語に追加の特別なサポートが必要なのですか?Goには...変数の数を変えることができる引数のサポートがあり、これにより、どのタイプの呼び出しを意味するかを判断し、欠落した引数のデフォルト値を提供することができます。または、デフォルト値を要求された引数に対して範囲外またはゼロ値(nil、-1、0など)のような値を渡すこともできます。これはCに長い伝統があるため、なぜ関数固有の呼び出し規則だけでは十分ではないのですか?私は「デフォルト値」を表すための特別なトークンを追加する必要性も理解できません。なぜなら、言語にさらにシンボルを追加することなく、これを実現できるからです。

おそらく、Googleはデフォルト値が機能するために、一堆のfunc(nil, nil, nil, nil)を渡すことを望んでいなかったり、動的なargs…を使用してパラメータの数と型を計算することができるため、デフォルト値を使用する必要がないと考えているのかもしれません。

しかし、本当にデフォルト値が必要な場合はどうすればよいでしょうか?私は初期化時に0、""などではなく、何も設定したくないのですが、どうすればよいでしょうか?この場合、golangの組み込みのタグ機能を試してみることができます。例えば:

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

といった方法で実現することができますが、機能が完全に実装されるためには考慮すべきことも多いです。改めて試してみる機会があれば、実装してみましょう。

結論

この記事では、golangでデフォルト値とゼロ値を使用する際に発生する可能性のある問題をまとめ、解決策を提案しました。

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Kalan です。台湾出身で、2019年に日本へ転職し、福岡に住んでいます。フロントエンド開発に精通しているだけでなく、IoT、アプリ開発、バックエンド、電子工作などの分野にも挑戦しています。 最近、エレキギターを始めました。ブログを通じて、より多くの人と交流できればと思っています。気軽に絡んでください