goのgenericsとencoding/jsonとdatabase/sql
のコードの現代的な実装を考えてみる。
なぜこれが欲しくなるかというと、encoding/jsonに対応したdatabase/sql.NullStringなどが欲しくなるから。
- 嬉しさは、presentationとmodelのstructを統一できること
- 悪いところは、presentationとmodelのstructを混同できてしまうところ
前者はprototypingではよく後者はsecurity的によろしくない(例えば意図しないfieldが混入してしまうことなど(パスワードなどは気をつける事が多いがメールアドレスあたりが漏れがち))
interface
database/sql
database/sqlで必要とされるものについて
package sql // import "database/sql"
type NullInt32 struct {
Int32 int32
Valid bool // Valid is true if Int32 is not NULL
}
NullInt32 represents an int32 that may be null. NullInt32 implements the
Scanner interface so it can be used as a scan destination, similar to
NullString.
func (n *NullInt32) Scan(value any) error
func (n NullInt32) Value() (driver.Value, error)
これは、database/sql/driver.Valuer と database/sql.Scanner を実装している。
encoding/json
一方encoding/jsonでは以下のようなMarshallerとUnmarshallerが必要とされている。
package json // import "encoding/json"
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
Marshaler is the interface implemented by types that can marshal themselves
into valid JSON.
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
Unmarshaler is the interface implemented by types that can unmarshal a JSON
description of themselves. The input can be assumed to be a valid encoding
of a JSON value. UnmarshalJSON must copy the JSON data if it wishes to
retain the data after returning.
By convention, to approximate the behavior of Unmarshal itself, Unmarshalers
implement UnmarshalJSON([]byte("null")) as a no-op.
これは encoding.TextMarshalerなどでも良い。
具体的な実装を観てみると、database/sqlの各種Typeを埋め込みを使って表現している。
そして
- NewInt, IntFrom, IntFromPtrという関数
- ValueOrZeroというメソッドを用意してる
encoding/jsonのためにUnmarshalJSON,MarshalJSONなどを定義している。
機能をシンプルにできるのでは?
もともとの目的を考えると string
に対して string | null
が欲しいということだった。ここでencoding/jsonでのomitemptyタグの利用はundefinedに対応する。つまりinputとしてnull
が渡せるということ。
そして利用者視点で見ると、zero値がinvalidと考えればNewは値だけを保持すれば良いのではないか?一方でunrequiredをpointerで表す場合もあり、これはFromなどを定義する必要がある?
package nullable
import "database/sql/driver"
type Nullable[T driver.Valuer] struct {
T
}
// [compiler] embedded field type cannot be a (pointer to a) type parameter
埋め込みをgenericsのtype parameterとしてstructに渡す機能は1.18には含まれていないらしい?
Go generics: is it possible to embed generic structs? - Stack Overflow
一番楽なのは、database/sqlのunexported functionをgo:linknameでimportしてきて使うことでは?
convertAssign()とか。
// NullInt32 represents an int32 that may be null.
// NullInt32 implements the Scanner interface so
// it can be used as a scan destination, similar to NullString.
type NullInt32 struct {
Int32 int32
Valid bool // Valid is true if Int32 is not NULL
}
// Scan implements the Scanner interface.
func (n *NullInt32) Scan(value any) error {
if value == nil {
n.Int32, n.Valid = 0, false
return nil
}
n.Valid = true
return convertAssign(&n.Int32, value)
}
// Value implements the driver Valuer interface.
func (n NullInt32) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return int64(n.Int32), nil
}
現状ではこんな感じが限界か。。
リポジトリにした