嵌套结构体导出excel的实现方式

245 阅读2分钟

前言

之前写过 go 导出excel并返回前端下载, 有个朋友用我封装方法的时候出现了问题, 结构体的部分字段没有写入, 这个问题是怎么个操作呢

问题

他的结构体是嵌套了另一个匿名结构体, 即:

type A struct {
		Name string
	}
	param := struct {
		Id int
		A
	}{1, A{"test"}}

然后导致 WriteStruct 方法解析不出来, WriteStruct:

// Writes a struct to row r. Accepts a pointer to struct type 'e',
// and the number of columns to write, `cols`. If 'cols' is < 0,
// the entire struct will be written if possible. Returns -1 if the 'e'
// doesn't point to a struct, otherwise the number of columns written
func (r *Row) WriteStruct(e interface{}, cols int) int {
	if cols == 0 {
		return cols
	}

	v := reflect.ValueOf(e).Elem()
	if v.Kind() != reflect.Struct {
		return -1 // bail if it's not a struct
	}

	n := v.NumField() // number of fields in struct
	if cols < n && cols > 0 {
		n = cols
	}

	var k int
	for i := 0; i < n; i, k = i+1, k+1 {
		f := v.Field(i)

		switch t := f.Interface().(type) {
		case time.Time:
			cell := r.AddCell()
			cell.SetValue(t)
		case fmt.Stringer: // check Stringer first
			cell := r.AddCell()
			cell.SetString(t.String())
		case sql.NullString: // check null sql types nulls = ''
			cell := r.AddCell()
			if cell.SetString(``); t.Valid {
				cell.SetValue(t.String)
			}
		case sql.NullBool:
			cell := r.AddCell()
			if cell.SetString(``); t.Valid {
				cell.SetBool(t.Bool)
			}
		case sql.NullInt64:
			cell := r.AddCell()
			if cell.SetString(``); t.Valid {
				cell.SetValue(t.Int64)
			}
		case sql.NullFloat64:
			cell := r.AddCell()
			if cell.SetString(``); t.Valid {
				cell.SetValue(t.Float64)
			}
		default:
			switch f.Kind() {
			case reflect.String, reflect.Int, reflect.Int8,
				reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float64, reflect.Float32:
				cell := r.AddCell()
				cell.SetValue(f.Interface())
			case reflect.Bool:
				cell := r.AddCell()
				cell.SetBool(t.(bool))
			default:
				k-- // nothing set so reset to previous
			}
		}
	}

	return k
}

可以看到里面只反射了第一层, 如果里面有嵌套结构的话就不会继续遍历, 所以导致嵌套结构体的字段值没有写入到excel中

解决

通过查看 WriteStruct 方法的源码文件我们可以看到里面还有一个可用方法 WriteSlice:

// Writes an array to row r. Accepts a pointer to array type 'e',
// and writes the number of columns to write, 'cols'. If 'cols' is < 0,
// the entire array will be written if possible. Returns -1 if the 'e'
// doesn't point to an array, otherwise the number of columns written.
func (r *Row) WriteSlice(e interface{}, cols int) int {
	if cols == 0 {
		return cols
	}

	// make sure 'e' is a Ptr to Slice
	v := reflect.ValueOf(e)
	if v.Kind() != reflect.Ptr {
		return -1
	}

	v = v.Elem()
	if v.Kind() != reflect.Slice {
		return -1
	}

	// it's a slice, so open up its values
	n := v.Len()
	if cols < n && cols > 0 {
		n = cols
	}

	var setCell func(reflect.Value)
	setCell = func(val reflect.Value) {
		switch t := val.Interface().(type) {
		case time.Time:
			cell := r.AddCell()
			cell.SetValue(t)
		case fmt.Stringer: // check Stringer first
			cell := r.AddCell()
			cell.SetString(t.String())
		case sql.NullString:  // check null sql types nulls = ''
			cell := r.AddCell()
			if cell.SetString(``); t.Valid {
				cell.SetValue(t.String)
			}
		case sql.NullBool:
			cell := r.AddCell()
			if cell.SetString(``); t.Valid {
				cell.SetBool(t.Bool)
			}
		case sql.NullInt64:
			cell := r.AddCell()
			if cell.SetString(``); t.Valid {
				cell.SetValue(t.Int64)
			}
		case sql.NullFloat64:
			cell := r.AddCell()
			if cell.SetString(``); t.Valid {
				cell.SetValue(t.Float64)
			}
		default:
			switch val.Kind() { // underlying type of slice
			case reflect.String, reflect.Int, reflect.Int8,
				reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float64, reflect.Float32:
				cell := r.AddCell()
				cell.SetValue(val.Interface())
			case reflect.Bool:
				cell := r.AddCell()
				cell.SetBool(t.(bool))
			case reflect.Interface:
				setCell(reflect.ValueOf(t))
			}
		}
	}

	var i int
	for i = 0; i < n; i++ {
		setCell(v.Index(i))
	}
	return i
}

这样的话我们自己写一套反射规则把结构体的值映射出来就好了

// StructValueToSlice 结构体值转入slice
func StructValueToSlice(val interface{}) (data []interface{}) {
	// 判断是否为指针
	//var t reflect.Type
	var v reflect.Value
	if reflect.ValueOf(val).Type().Kind() == reflect.Struct {
		v = reflect.ValueOf(val)
		//t = reflect.TypeOf(val)
	} else {
		v = reflect.Indirect(reflect.ValueOf(val))
		//t = reflect.TypeOf(val).Elem()
	}

	for i := 0; i < v.NumField(); i++ {
		//判断是否是嵌套结构
		if v.Field(i).Type().Kind() == reflect.Struct {
			structField := v.Field(i).Type()
			for j := 0; j < structField.NumField(); j++ {
				//fmt.Println(fmt.Sprintf("%s %s = %v ", structField.Field(j).Name, structField.Field(j).Type, v.Field(i).Field(j).Interface()))
				data = append(data, v.Field(i).Field(j).Interface())
			}
		} else {
			//fmt.Println(fmt.Sprintf("%s %s = %v ", t.Field(i).Name, t.Field(i).Type, v.Field(i).Interface()))
			data = append(data, v.Field(i).Interface())
		}
	}
	return
}

// RecursionStructValueToSlice 递归多层嵌套结构体的值转入slice
func RecursionStructValueToSlice(val interface{}, v reflect.Value, data *[]interface{}) {
	// 判断是否为指针
	if val == nil {
	} else if reflect.ValueOf(val).Type().Kind() == reflect.Struct {
		v = reflect.ValueOf(val)
	} else {
		v = reflect.Indirect(reflect.ValueOf(val))
	}
	for i := 0; i < v.NumField(); i++ {
		//判断是否是嵌套结构
		if v.Field(i).Type().Kind() == reflect.Struct {
			RecursionStructValueToSlice(nil, v.Field(i), data)
		} else {
			*data = append(*data, v.Field(i).Interface())
		}
	}
	return
}

下面是单元测试

func TestStructValueToSlice(t *testing.T) {
	type A struct {
		Name string
	}
	param := struct {
		Id int
		A
	}{1, A{"test"}}
	t.Log(StructValueToSlice(param))
}

func TestRecursionStructValueToSlice(t *testing.T) {
	type A struct {
		Age int
	}
	type B struct {
		Name string
		A
	}
	param := struct {
		Id int
		B
	}{1, B{"test", A{11}}}
	data := make([]interface{}, 0)
	RecursionStructValueToSlice(param, reflect.Value{}, &data)
	t.Log(data)
}

这样的话就可以了, 插入之前先走下映射获取到值的结构体切片, 然后再用 WriteSlice 方法写入就可以了

优化

看 WriteSlice 方法可以看出来它里面也是反射获取slice的值, 然后 r.AddCell().cell.SetValue(t) 方法插进去了, 所以想提升效率的话可以直接在反射的方法里直接将值写入行就行了, 省了一步反射的过程