This is the 13th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

Author: lomtom

Personal website: lomtom.cn

Personal public account: Bosiao Yuan

Your support is my biggest motivation.

The Go series:

  1. Go (1) Basic introduction
  2. Go (ii) structure
  3. Go (3) Go configuration file
  4. Go (4) Redis operation
  5. Go (5) Go do not know how to use Gorm?
  6. Go (6) come come come, teach you how to call remotely
  7. You say you can’t do concurrency?
  8. Go (8) still don’t know the functional option mode?

The introduction of

Initialize the Option structure because it is private, meaning it can only be accessed inside the package, so you need to write a constructor.

type option struct {
	A string
	B string
	C int
}
Copy the code

The constructor

func newOption(a, b string, c int) *option {
	return &option{
		A: a,
		B: b,
		C: c,
	}
}
Copy the code

When used, simply call this method.

option := newOption("a"."b".10)
Copy the code

Such code appears to be problem-free, and is very convenient to use.

But what if the requirements change and I no longer need the option with three parameters, I only need the option with two or one parameters?

The Go language does not support method overloading. If you need to write multiple initializing constructors for a structure, with the same method name and different parameters, this situation is not allowed, but often this situation is used in many scenarios.

So what do you do? You have to write only one constructor and name it newOption1.

func newOption1(a, b string) *option {
	return &option{
		A: a,
		B: b,
	}
}
Copy the code

At first glance, it is ok and meets our needs, but there is no guarantee that demand will not change in the future.

So here’s the advantage of option mode.

Option mode for initialization

The option pattern uses closures and undetermined arguments to achieve parameter selectibility.

type OptionFunc func(*option)

func WithA(a string) OptionFunc {
	return func(o *option) {
		o.A = a
	}
}

func WithB(b string) OptionFunc {
	return func(o *option) {
		o.B = b
	}
}

func WithC(c int) OptionFunc {
	return func(o *option) {
		o.C = c
	}
}
Copy the code

Start by defining a function type that takes *option and writing the option method for three arguments.

func newOption(opts... OptionFunc)(opt *option) {
	opt = &option{
		A: "A",
		B: "B",
		C: 100,}for _, optFun := range opts {
		optFun(opt)
	}
	return
}
Copy the code

Modify its constructor so that each parameter is initialized as an option, leaving the default if no option for that parameter is passed in.

func TestGo7(t *testing.T) {
	o := newOption()
	fmt.Println(o)

	o1 := newOption(
		WithA("1"),
	)
	fmt.Println(o1)

	o2 := newOption(
		WithA("a"),
		WithB("b"),
		WithC(10),
		)
	fmt.Println(o2)
}
Copy the code

In use, when initializing a structure, you can choose to initialize some of its parameters.

Application of option pattern in Gorm

What else can the option pattern be used for besides struct initialization?

As it happens, the database query operation is often also suitable, in the query, we may need to filter multiple conditions, if each case to write a query method, not only will make the code look messy, and write too much, their logic leaf will become messy.

For example, if you have a dictionary table with many attributes and you need to filter for each attribute, you can have many different situations, whereas with the option mode you only need to write one option for each field to do multi-conditional filtering.

// DataDictionary [...]
type DataDictionary struct {
	ID         string    `gorm:"primaryKey; column:id; type:varchar(255); not null" json:"-"`
	DataType   string    `gorm:"column:data_type; type:varchar(255); not null" json:"dataType"`      // Data type
	DataKey    string    `gorm:"column:data_key; type:varchar(255); not null" json:"dataKey"`        / / the key data
	DataValue  string    `gorm:"column:data_value; type:varchar(255); not null" json:"dataValue"`    / / data value
	SortNumber int       `gorm:"column:sort_number; type:int; not null" json:"sortNumber"`           // Sort number
	Level      string    `gorm:"column:level; type:varchar(255); not null" json:"level"`             / / level
	Deleted    bool      `gorm:"column:deleted; type:tinyint(1); not null; default:0" json:"deleted"` // Whether to delete
	UpdateTime time.Time `gorm:"column:update_time; type:timestamp; not null" json:"updateTime"`     // Update time
	CreateTime time.Time `gorm:"column:create_time; type:timestamp; not null" json:"createTime"`     // Create time
	DomainID   string    `gorm:"column:domain_id; type:varchar(255)" json:"domainId"`               / / the main account
}

// TableName get sql table name. Gets the database table name
func (m *DataDictionary) TableName(a) string {
	return "data_dictionary"
}
Copy the code

Write custom methods and write an Options structure to store option relationships.

type options struct {
	query map[string]interface{}}type Option interface {
	apply(*options)
}

type optionFunc func(*options)

func (f optionFunc) apply(o *options) {
	f(o)
}
Copy the code

Write query option methods on DataType and DataKey fields.

/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * option model * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /

// WithDataType dataType gets the dictionary category
func (obj *DataDictionaryMapper) WithDataType(dataType string) Option {
	return optionFunc(func(o *options) { o.query["data_type"] = dataType })
}

// WithDataKey dataType gets the dictionary category
func (obj *DataDictionaryMapper) WithDataKey(dataKey string) Option {
	return optionFunc(func(o *options) { o.query["data_key"] = dataKey })
}
Copy the code

Write the query method and iterate through the options in the structure you originally defined, Options.

// GetByOption Obtains the function option mode
func (obj *DataDictionaryMapper) GetByOption(opts ... Option) (result []po.DataDictionary, err error) {
	options := options{
		query: make(map[string]interface{}, len(opts)),
	}
	for _, o := range opts {
		o.apply(&options)
	}
	err = obj.DB.Where(options.query).Find(&result).Error
	return
}
Copy the code

Pass the option as a parameter

res, _:= c.GetByOption(c.WithDataType("position"))
Copy the code

You may be wondering why it is ok to pass options.query directly to where.

We define the above options. Query for map [string] interface {}, tx. In our point into the Where method source Statement. BuildCondition (query, args…). As you can see, GORM will type the parameters and convert them to SQL.

switch v := arg.(type) {
		case clause.Expression:
			conds = append(conds, v)
		case *DB:
			if cs, ok := v.Statement.Clauses["WHERE"]; ok {
				if where, ok := cs.Expression.(clause.Where); ok {
					if len(where.Exprs) == 1 {
						if orConds, ok := where.Exprs[0].(clause.OrConditions); ok {
							where.Exprs[0] = clause.AndConditions(orConds)
						}
					}
					conds = append(conds, clause.And(where.Exprs...))
				} else ifcs.Expression ! =nil {
					conds = append(conds, cs.Expression)
				}
			}
		case map[interface{}]interface{} :for i, j := range v {
				conds = append(conds, clause.Eq{Column: i, Value: j})
			}
		case map[string]string:
			var keys = make([]string.0.len(v))
			for i := range v {
				keys = append(keys, i)
			}
			sort.Strings(keys)

			for _, key := range keys {
				conds = append(conds, clause.Eq{Column: key, Value: v[key]})
			}
		case map[string]interface{} :var keys = make([]string.0.len(v))
			for i := range v {
				keys = append(keys, i)
			}
			sort.Strings(keys)

			for _, key := range keys {
				reflectValue := reflect.Indirect(reflect.ValueOf(v[key]))
				switch reflectValue.Kind() {
				case reflect.Slice, reflect.Array:
					if _, ok := v[key].(driver.Valuer); ok {
						conds = append(conds, clause.Eq{Column: key, Value: v[key]})
					} else if _, ok := v[key].(Valuer); ok {
						conds = append(conds, clause.Eq{Column: key, Value: v[key]})
					} else {
						// optimize reflect value length
						valueLen := reflectValue.Len()
						values := make([]interface{}, valueLen)
						for i := 0; i < valueLen; i++ {
							values[i] = reflectValue.Index(i).Interface()
						}

						conds = append(conds, clause.IN{Column: key, Values: values})
					}
				default:
					conds = append(conds, clause.Eq{Column: key, Value: v[key]})
				}
			}
Copy the code

Functional Options Pattern in Go (option)