Closed9

goのgenericsとencoding/jsonとdatabase/sql

podhmopodhmo

https://github.com/guregu/null

のコードの現代的な実装を考えてみる。

podhmopodhmo

なぜこれが欲しくなるかというと、encoding/jsonに対応したdatabase/sql.NullStringなどが欲しくなるから。

  • 嬉しさは、presentationとmodelのstructを統一できること
  • 悪いところは、presentationとmodelのstructを混同できてしまうところ

前者はprototypingではよく後者はsecurity的によろしくない(例えば意図しないfieldが混入してしまうことなど(パスワードなどは気をつける事が多いがメールアドレスあたりが漏れがち))

podhmopodhmo

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などでも良い。

podhmopodhmo

機能をシンプルにできるのでは?

もともとの目的を考えると string に対して string | null が欲しいということだった。ここでencoding/jsonでのomitemptyタグの利用はundefinedに対応する。つまりinputとしてnullが渡せるということ。

そして利用者視点で見ると、zero値がinvalidと考えればNewは値だけを保持すれば良いのではないか?一方でunrequiredをpointerで表す場合もあり、これはFromなどを定義する必要がある?

podhmopodhmo

一番楽なのは、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
}
このスクラップは2023/07/03にクローズされました