586 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			586 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2012 Gary Burd
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License"): you may
 | |
| // not use this file except in compliance with the License. You may obtain
 | |
| // a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | |
| // License for the specific language governing permissions and limitations
 | |
| // under the License.
 | |
| 
 | |
| package redis
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"reflect"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| )
 | |
| 
 | |
| func ensureLen(d reflect.Value, n int) {
 | |
| 	if n > d.Cap() {
 | |
| 		d.Set(reflect.MakeSlice(d.Type(), n, n))
 | |
| 	} else {
 | |
| 		d.SetLen(n)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func cannotConvert(d reflect.Value, s interface{}) error {
 | |
| 	var sname string
 | |
| 	switch s.(type) {
 | |
| 	case string:
 | |
| 		sname = "Redis simple string"
 | |
| 	case Error:
 | |
| 		sname = "Redis error"
 | |
| 	case int64:
 | |
| 		sname = "Redis integer"
 | |
| 	case []byte:
 | |
| 		sname = "Redis bulk string"
 | |
| 	case []interface{}:
 | |
| 		sname = "Redis array"
 | |
| 	default:
 | |
| 		sname = reflect.TypeOf(s).String()
 | |
| 	}
 | |
| 	return fmt.Errorf("cannot convert from %s to %s", sname, d.Type())
 | |
| }
 | |
| 
 | |
| func convertAssignBulkString(d reflect.Value, s []byte) (err error) {
 | |
| 	switch d.Type().Kind() {
 | |
| 	case reflect.Float32, reflect.Float64:
 | |
| 		var x float64
 | |
| 		x, err = strconv.ParseFloat(string(s), d.Type().Bits())
 | |
| 		d.SetFloat(x)
 | |
| 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 | |
| 		var x int64
 | |
| 		x, err = strconv.ParseInt(string(s), 10, d.Type().Bits())
 | |
| 		d.SetInt(x)
 | |
| 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 | |
| 		var x uint64
 | |
| 		x, err = strconv.ParseUint(string(s), 10, d.Type().Bits())
 | |
| 		d.SetUint(x)
 | |
| 	case reflect.Bool:
 | |
| 		var x bool
 | |
| 		x, err = strconv.ParseBool(string(s))
 | |
| 		d.SetBool(x)
 | |
| 	case reflect.String:
 | |
| 		d.SetString(string(s))
 | |
| 	case reflect.Slice:
 | |
| 		if d.Type().Elem().Kind() != reflect.Uint8 {
 | |
| 			err = cannotConvert(d, s)
 | |
| 		} else {
 | |
| 			d.SetBytes(s)
 | |
| 		}
 | |
| 	default:
 | |
| 		err = cannotConvert(d, s)
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func convertAssignInt(d reflect.Value, s int64) (err error) {
 | |
| 	switch d.Type().Kind() {
 | |
| 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 | |
| 		d.SetInt(s)
 | |
| 		if d.Int() != s {
 | |
| 			err = strconv.ErrRange
 | |
| 			d.SetInt(0)
 | |
| 		}
 | |
| 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 | |
| 		if s < 0 {
 | |
| 			err = strconv.ErrRange
 | |
| 		} else {
 | |
| 			x := uint64(s)
 | |
| 			d.SetUint(x)
 | |
| 			if d.Uint() != x {
 | |
| 				err = strconv.ErrRange
 | |
| 				d.SetUint(0)
 | |
| 			}
 | |
| 		}
 | |
| 	case reflect.Bool:
 | |
| 		d.SetBool(s != 0)
 | |
| 	default:
 | |
| 		err = cannotConvert(d, s)
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func convertAssignValue(d reflect.Value, s interface{}) (err error) {
 | |
| 	if d.Kind() != reflect.Ptr {
 | |
| 		if d.CanAddr() {
 | |
| 			d2 := d.Addr()
 | |
| 			if d2.CanInterface() {
 | |
| 				if scanner, ok := d2.Interface().(Scanner); ok {
 | |
| 					return scanner.RedisScan(s)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	} else if d.CanInterface() {
 | |
| 		// Already a reflect.Ptr
 | |
| 		if d.IsNil() {
 | |
| 			d.Set(reflect.New(d.Type().Elem()))
 | |
| 		}
 | |
| 		if scanner, ok := d.Interface().(Scanner); ok {
 | |
| 			return scanner.RedisScan(s)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	switch s := s.(type) {
 | |
| 	case []byte:
 | |
| 		err = convertAssignBulkString(d, s)
 | |
| 	case int64:
 | |
| 		err = convertAssignInt(d, s)
 | |
| 	default:
 | |
| 		err = cannotConvert(d, s)
 | |
| 	}
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func convertAssignArray(d reflect.Value, s []interface{}) error {
 | |
| 	if d.Type().Kind() != reflect.Slice {
 | |
| 		return cannotConvert(d, s)
 | |
| 	}
 | |
| 	ensureLen(d, len(s))
 | |
| 	for i := 0; i < len(s); i++ {
 | |
| 		if err := convertAssignValue(d.Index(i), s[i]); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func convertAssign(d interface{}, s interface{}) (err error) {
 | |
| 	if scanner, ok := d.(Scanner); ok {
 | |
| 		return scanner.RedisScan(s)
 | |
| 	}
 | |
| 
 | |
| 	// Handle the most common destination types using type switches and
 | |
| 	// fall back to reflection for all other types.
 | |
| 	switch s := s.(type) {
 | |
| 	case nil:
 | |
| 		// ignore
 | |
| 	case []byte:
 | |
| 		switch d := d.(type) {
 | |
| 		case *string:
 | |
| 			*d = string(s)
 | |
| 		case *int:
 | |
| 			*d, err = strconv.Atoi(string(s))
 | |
| 		case *bool:
 | |
| 			*d, err = strconv.ParseBool(string(s))
 | |
| 		case *[]byte:
 | |
| 			*d = s
 | |
| 		case *interface{}:
 | |
| 			*d = s
 | |
| 		case nil:
 | |
| 			// skip value
 | |
| 		default:
 | |
| 			if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
 | |
| 				err = cannotConvert(d, s)
 | |
| 			} else {
 | |
| 				err = convertAssignBulkString(d.Elem(), s)
 | |
| 			}
 | |
| 		}
 | |
| 	case int64:
 | |
| 		switch d := d.(type) {
 | |
| 		case *int:
 | |
| 			x := int(s)
 | |
| 			if int64(x) != s {
 | |
| 				err = strconv.ErrRange
 | |
| 				x = 0
 | |
| 			}
 | |
| 			*d = x
 | |
| 		case *bool:
 | |
| 			*d = s != 0
 | |
| 		case *interface{}:
 | |
| 			*d = s
 | |
| 		case nil:
 | |
| 			// skip value
 | |
| 		default:
 | |
| 			if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
 | |
| 				err = cannotConvert(d, s)
 | |
| 			} else {
 | |
| 				err = convertAssignInt(d.Elem(), s)
 | |
| 			}
 | |
| 		}
 | |
| 	case string:
 | |
| 		switch d := d.(type) {
 | |
| 		case *string:
 | |
| 			*d = s
 | |
| 		case *interface{}:
 | |
| 			*d = s
 | |
| 		case nil:
 | |
| 			// skip value
 | |
| 		default:
 | |
| 			err = cannotConvert(reflect.ValueOf(d), s)
 | |
| 		}
 | |
| 	case []interface{}:
 | |
| 		switch d := d.(type) {
 | |
| 		case *[]interface{}:
 | |
| 			*d = s
 | |
| 		case *interface{}:
 | |
| 			*d = s
 | |
| 		case nil:
 | |
| 			// skip value
 | |
| 		default:
 | |
| 			if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
 | |
| 				err = cannotConvert(d, s)
 | |
| 			} else {
 | |
| 				err = convertAssignArray(d.Elem(), s)
 | |
| 			}
 | |
| 		}
 | |
| 	case Error:
 | |
| 		err = s
 | |
| 	default:
 | |
| 		err = cannotConvert(reflect.ValueOf(d), s)
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // Scan copies from src to the values pointed at by dest.
 | |
| //
 | |
| // Scan uses RedisScan if available otherwise:
 | |
| //
 | |
| // The values pointed at by dest must be an integer, float, boolean, string,
 | |
| // []byte, interface{} or slices of these types. Scan uses the standard strconv
 | |
| // package to convert bulk strings to numeric and boolean types.
 | |
| //
 | |
| // If a dest value is nil, then the corresponding src value is skipped.
 | |
| //
 | |
| // If a src element is nil, then the corresponding dest value is not modified.
 | |
| //
 | |
| // To enable easy use of Scan in a loop, Scan returns the slice of src
 | |
| // following the copied values.
 | |
| func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) {
 | |
| 	if len(src) < len(dest) {
 | |
| 		return nil, errors.New("redigo.Scan: array short")
 | |
| 	}
 | |
| 	var err error
 | |
| 	for i, d := range dest {
 | |
| 		err = convertAssign(d, src[i])
 | |
| 		if err != nil {
 | |
| 			err = fmt.Errorf("redigo.Scan: cannot assign to dest %d: %v", i, err)
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 	return src[len(dest):], err
 | |
| }
 | |
| 
 | |
| type fieldSpec struct {
 | |
| 	name      string
 | |
| 	index     []int
 | |
| 	omitEmpty bool
 | |
| }
 | |
| 
 | |
| type structSpec struct {
 | |
| 	m map[string]*fieldSpec
 | |
| 	l []*fieldSpec
 | |
| }
 | |
| 
 | |
| func (ss *structSpec) fieldSpec(name []byte) *fieldSpec {
 | |
| 	return ss.m[string(name)]
 | |
| }
 | |
| 
 | |
| func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {
 | |
| 	for i := 0; i < t.NumField(); i++ {
 | |
| 		f := t.Field(i)
 | |
| 		switch {
 | |
| 		case f.PkgPath != "" && !f.Anonymous:
 | |
| 			// Ignore unexported fields.
 | |
| 		case f.Anonymous:
 | |
| 			// TODO: Handle pointers. Requires change to decoder and
 | |
| 			// protection against infinite recursion.
 | |
| 			if f.Type.Kind() == reflect.Struct {
 | |
| 				compileStructSpec(f.Type, depth, append(index, i), ss)
 | |
| 			}
 | |
| 		default:
 | |
| 			fs := &fieldSpec{name: f.Name}
 | |
| 			tag := f.Tag.Get("redis")
 | |
| 			p := strings.Split(tag, ",")
 | |
| 			if len(p) > 0 {
 | |
| 				if p[0] == "-" {
 | |
| 					continue
 | |
| 				}
 | |
| 				if len(p[0]) > 0 {
 | |
| 					fs.name = p[0]
 | |
| 				}
 | |
| 				for _, s := range p[1:] {
 | |
| 					switch s {
 | |
| 					case "omitempty":
 | |
| 						fs.omitEmpty = true
 | |
| 					default:
 | |
| 						panic(fmt.Errorf("redigo: unknown field tag %s for type %s", s, t.Name()))
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 			d, found := depth[fs.name]
 | |
| 			if !found {
 | |
| 				d = 1 << 30
 | |
| 			}
 | |
| 			switch {
 | |
| 			case len(index) == d:
 | |
| 				// At same depth, remove from result.
 | |
| 				delete(ss.m, fs.name)
 | |
| 				j := 0
 | |
| 				for i := 0; i < len(ss.l); i++ {
 | |
| 					if fs.name != ss.l[i].name {
 | |
| 						ss.l[j] = ss.l[i]
 | |
| 						j += 1
 | |
| 					}
 | |
| 				}
 | |
| 				ss.l = ss.l[:j]
 | |
| 			case len(index) < d:
 | |
| 				fs.index = make([]int, len(index)+1)
 | |
| 				copy(fs.index, index)
 | |
| 				fs.index[len(index)] = i
 | |
| 				depth[fs.name] = len(index)
 | |
| 				ss.m[fs.name] = fs
 | |
| 				ss.l = append(ss.l, fs)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	structSpecMutex  sync.RWMutex
 | |
| 	structSpecCache  = make(map[reflect.Type]*structSpec)
 | |
| 	defaultFieldSpec = &fieldSpec{}
 | |
| )
 | |
| 
 | |
| func structSpecForType(t reflect.Type) *structSpec {
 | |
| 
 | |
| 	structSpecMutex.RLock()
 | |
| 	ss, found := structSpecCache[t]
 | |
| 	structSpecMutex.RUnlock()
 | |
| 	if found {
 | |
| 		return ss
 | |
| 	}
 | |
| 
 | |
| 	structSpecMutex.Lock()
 | |
| 	defer structSpecMutex.Unlock()
 | |
| 	ss, found = structSpecCache[t]
 | |
| 	if found {
 | |
| 		return ss
 | |
| 	}
 | |
| 
 | |
| 	ss = &structSpec{m: make(map[string]*fieldSpec)}
 | |
| 	compileStructSpec(t, make(map[string]int), nil, ss)
 | |
| 	structSpecCache[t] = ss
 | |
| 	return ss
 | |
| }
 | |
| 
 | |
| var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil pointer to a struct")
 | |
| 
 | |
| // ScanStruct scans alternating names and values from src to a struct. The
 | |
| // HGETALL and CONFIG GET commands return replies in this format.
 | |
| //
 | |
| // ScanStruct uses exported field names to match values in the response. Use
 | |
| // 'redis' field tag to override the name:
 | |
| //
 | |
| //      Field int `redis:"myName"`
 | |
| //
 | |
| // Fields with the tag redis:"-" are ignored.
 | |
| //
 | |
| // Each field uses RedisScan if available otherwise:
 | |
| // Integer, float, boolean, string and []byte fields are supported. Scan uses the
 | |
| // standard strconv package to convert bulk string values to numeric and
 | |
| // boolean types.
 | |
| //
 | |
| // If a src element is nil, then the corresponding field is not modified.
 | |
| func ScanStruct(src []interface{}, dest interface{}) error {
 | |
| 	d := reflect.ValueOf(dest)
 | |
| 	if d.Kind() != reflect.Ptr || d.IsNil() {
 | |
| 		return errScanStructValue
 | |
| 	}
 | |
| 	d = d.Elem()
 | |
| 	if d.Kind() != reflect.Struct {
 | |
| 		return errScanStructValue
 | |
| 	}
 | |
| 	ss := structSpecForType(d.Type())
 | |
| 
 | |
| 	if len(src)%2 != 0 {
 | |
| 		return errors.New("redigo.ScanStruct: number of values not a multiple of 2")
 | |
| 	}
 | |
| 
 | |
| 	for i := 0; i < len(src); i += 2 {
 | |
| 		s := src[i+1]
 | |
| 		if s == nil {
 | |
| 			continue
 | |
| 		}
 | |
| 		name, ok := src[i].([]byte)
 | |
| 		if !ok {
 | |
| 			return fmt.Errorf("redigo.ScanStruct: key %d not a bulk string value", i)
 | |
| 		}
 | |
| 		fs := ss.fieldSpec(name)
 | |
| 		if fs == nil {
 | |
| 			continue
 | |
| 		}
 | |
| 		if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
 | |
| 			return fmt.Errorf("redigo.ScanStruct: cannot assign field %s: %v", fs.name, err)
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	errScanSliceValue = errors.New("redigo.ScanSlice: dest must be non-nil pointer to a struct")
 | |
| )
 | |
| 
 | |
| // ScanSlice scans src to the slice pointed to by dest. The elements the dest
 | |
| // slice must be integer, float, boolean, string, struct or pointer to struct
 | |
| // values.
 | |
| //
 | |
| // Struct fields must be integer, float, boolean or string values. All struct
 | |
| // fields are used unless a subset is specified using fieldNames.
 | |
| func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error {
 | |
| 	d := reflect.ValueOf(dest)
 | |
| 	if d.Kind() != reflect.Ptr || d.IsNil() {
 | |
| 		return errScanSliceValue
 | |
| 	}
 | |
| 	d = d.Elem()
 | |
| 	if d.Kind() != reflect.Slice {
 | |
| 		return errScanSliceValue
 | |
| 	}
 | |
| 
 | |
| 	isPtr := false
 | |
| 	t := d.Type().Elem()
 | |
| 	if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
 | |
| 		isPtr = true
 | |
| 		t = t.Elem()
 | |
| 	}
 | |
| 
 | |
| 	if t.Kind() != reflect.Struct {
 | |
| 		ensureLen(d, len(src))
 | |
| 		for i, s := range src {
 | |
| 			if s == nil {
 | |
| 				continue
 | |
| 			}
 | |
| 			if err := convertAssignValue(d.Index(i), s); err != nil {
 | |
| 				return fmt.Errorf("redigo.ScanSlice: cannot assign element %d: %v", i, err)
 | |
| 			}
 | |
| 		}
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	ss := structSpecForType(t)
 | |
| 	fss := ss.l
 | |
| 	if len(fieldNames) > 0 {
 | |
| 		fss = make([]*fieldSpec, len(fieldNames))
 | |
| 		for i, name := range fieldNames {
 | |
| 			fss[i] = ss.m[name]
 | |
| 			if fss[i] == nil {
 | |
| 				return fmt.Errorf("redigo.ScanSlice: ScanSlice bad field name %s", name)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if len(fss) == 0 {
 | |
| 		return errors.New("redigo.ScanSlice: no struct fields")
 | |
| 	}
 | |
| 
 | |
| 	n := len(src) / len(fss)
 | |
| 	if n*len(fss) != len(src) {
 | |
| 		return errors.New("redigo.ScanSlice: length not a multiple of struct field count")
 | |
| 	}
 | |
| 
 | |
| 	ensureLen(d, n)
 | |
| 	for i := 0; i < n; i++ {
 | |
| 		d := d.Index(i)
 | |
| 		if isPtr {
 | |
| 			if d.IsNil() {
 | |
| 				d.Set(reflect.New(t))
 | |
| 			}
 | |
| 			d = d.Elem()
 | |
| 		}
 | |
| 		for j, fs := range fss {
 | |
| 			s := src[i*len(fss)+j]
 | |
| 			if s == nil {
 | |
| 				continue
 | |
| 			}
 | |
| 			if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
 | |
| 				return fmt.Errorf("redigo.ScanSlice: cannot assign element %d to field %s: %v", i*len(fss)+j, fs.name, err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Args is a helper for constructing command arguments from structured values.
 | |
| type Args []interface{}
 | |
| 
 | |
| // Add returns the result of appending value to args.
 | |
| func (args Args) Add(value ...interface{}) Args {
 | |
| 	return append(args, value...)
 | |
| }
 | |
| 
 | |
| // AddFlat returns the result of appending the flattened value of v to args.
 | |
| //
 | |
| // Maps are flattened by appending the alternating keys and map values to args.
 | |
| //
 | |
| // Slices are flattened by appending the slice elements to args.
 | |
| //
 | |
| // Structs are flattened by appending the alternating names and values of
 | |
| // exported fields to args. If v is a nil struct pointer, then nothing is
 | |
| // appended. The 'redis' field tag overrides struct field names. See ScanStruct
 | |
| // for more information on the use of the 'redis' field tag.
 | |
| //
 | |
| // Other types are appended to args as is.
 | |
| func (args Args) AddFlat(v interface{}) Args {
 | |
| 	rv := reflect.ValueOf(v)
 | |
| 	switch rv.Kind() {
 | |
| 	case reflect.Struct:
 | |
| 		args = flattenStruct(args, rv)
 | |
| 	case reflect.Slice:
 | |
| 		for i := 0; i < rv.Len(); i++ {
 | |
| 			args = append(args, rv.Index(i).Interface())
 | |
| 		}
 | |
| 	case reflect.Map:
 | |
| 		for _, k := range rv.MapKeys() {
 | |
| 			args = append(args, k.Interface(), rv.MapIndex(k).Interface())
 | |
| 		}
 | |
| 	case reflect.Ptr:
 | |
| 		if rv.Type().Elem().Kind() == reflect.Struct {
 | |
| 			if !rv.IsNil() {
 | |
| 				args = flattenStruct(args, rv.Elem())
 | |
| 			}
 | |
| 		} else {
 | |
| 			args = append(args, v)
 | |
| 		}
 | |
| 	default:
 | |
| 		args = append(args, v)
 | |
| 	}
 | |
| 	return args
 | |
| }
 | |
| 
 | |
| func flattenStruct(args Args, v reflect.Value) Args {
 | |
| 	ss := structSpecForType(v.Type())
 | |
| 	for _, fs := range ss.l {
 | |
| 		fv := v.FieldByIndex(fs.index)
 | |
| 		if fs.omitEmpty {
 | |
| 			var empty = false
 | |
| 			switch fv.Kind() {
 | |
| 			case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
 | |
| 				empty = fv.Len() == 0
 | |
| 			case reflect.Bool:
 | |
| 				empty = !fv.Bool()
 | |
| 			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 | |
| 				empty = fv.Int() == 0
 | |
| 			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
 | |
| 				empty = fv.Uint() == 0
 | |
| 			case reflect.Float32, reflect.Float64:
 | |
| 				empty = fv.Float() == 0
 | |
| 			case reflect.Interface, reflect.Ptr:
 | |
| 				empty = fv.IsNil()
 | |
| 			}
 | |
| 			if empty {
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 		args = append(args, fs.name, fv.Interface())
 | |
| 	}
 | |
| 	return args
 | |
| }
 |