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
を空の値を表すために使用できますが、これにはいくつかの重大な欠陥があることは明らかです。使用する前にポインタが空かどうかをチェックする必要があります。
また、このstruct
がsql
のスキャン値に使用される場合、sql.NullString
を使用するか、nullパッケージを使用することができます。
sql.NullString / sql.NullIntなど
データベースに値をマッピングするために定義したstruct
を使用する場合、Scanner
と呼ばれるインターフェースを実装する必要があります。文字列が空の場合、値をデータベースにマッピングすることはできません。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では、基本型をカスタム型として定義し、その型にメソッドを追加することができます。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でデフォルト値とゼロ値を使用する際に発生する可能性のある問題をまとめ、解決策を提案しました。