From 0af55794f6b5aa1c8a29c3736a167c26dc77eb64 Mon Sep 17 00:00:00 2001 From: John Date: Sun, 17 Mar 2019 22:26:41 +0800 Subject: [PATCH] add more unit test cases for gdb --- TODO.MD | 2 +- g/database/gdb/gdb.go | 4 +- g/database/gdb/gdb_base.go | 54 +++++---- g/database/gdb/gdb_func.go | 19 +++- g/database/gdb/gdb_model.go | 20 ++-- g/database/gdb/gdb_transaction.go | 18 ++- g/database/gdb/gdb_unit_init_test.go | 67 ++++++++++- g/database/gdb/gdb_unit_method_test.go | 152 +++++++++++++++++-------- g/database/gdb/gdb_unit_model_test.go | 106 ++++++++++++----- g/util/gconv/gconv_slice.go | 20 ++++ 10 files changed, 341 insertions(+), 121 deletions(-) diff --git a/TODO.MD b/TODO.MD index 87dd11b67..bf91f63b1 100644 --- a/TODO.MD +++ b/TODO.MD @@ -57,7 +57,7 @@ 1. 更新跨域请求CORS相关功能文档; 1. ghttp的热重启的本地进程端口监听,在不使用该特性时默认关闭掉; 1. gcfg包目前允许添加重复的目录路径,需要在SetPath/AddPath时判断重复性,不能添加重复的路径; - +1. gdb执行数据写入时,如果参数为struct/[]struct,自动映射与表字段对应关系,不再使用gconv标签标识; diff --git a/g/database/gdb/gdb.go b/g/database/gdb/gdb.go index bb67f3966..12ed83f0b 100644 --- a/g/database/gdb/gdb.go +++ b/g/database/gdb/gdb.go @@ -39,8 +39,8 @@ type DB interface { doPrepare(link dbLink, query string) (*sql.Stmt, error) doInsert(link dbLink, table string, data interface{}, option int, batch...int) (result sql.Result, err error) doBatchInsert(link dbLink, table string, list interface{}, option int, batch...int) (result sql.Result, err error) - doUpdate(link dbLink, table string, data interface{}, condition interface{}, args ...interface{}) (result sql.Result, err error) - doDelete(link dbLink, table string, condition interface{}, args ...interface{}) (result sql.Result, err error) + doUpdate(link dbLink, table string, data interface{}, condition string, args ...interface{}) (result sql.Result, err error) + doDelete(link dbLink, table string, condition string, args ...interface{}) (result sql.Result, err error) // 数据库查询 GetAll(query string, args ...interface{}) (Result, error) diff --git a/g/database/gdb/gdb_base.go b/g/database/gdb/gdb_base.go index c7f86ef6c..d16be23d1 100644 --- a/g/database/gdb/gdb_base.go +++ b/g/database/gdb/gdb_base.go @@ -312,7 +312,7 @@ func (bs *dbBase) doInsert(link dbLink, table string, data interface{}, option i return bs.db.doBatchInsert(link, table, data, option, batch...) case reflect.Map: fallthrough case reflect.Struct: - dataMap = Map(gconv.Map(data)) + dataMap = gconv.Map(data) default: return result, errors.New(fmt.Sprint("unsupported data type:", kind)) } @@ -320,7 +320,7 @@ func (bs *dbBase) doInsert(link dbLink, table string, data interface{}, option i for k, v := range dataMap { fields = append(fields, charL + k + charR) values = append(values, "?") - params = append(params, v) + params = append(params, convertParam(v)) } operation := getInsertOperationByOption(option) updateStr := "" @@ -369,6 +369,10 @@ func (bs *dbBase) doBatchInsert(link dbLink, table string, list interface{}, opt var params []interface{} listMap := (List)(nil) switch v := list.(type) { + case Result: + listMap = v.ToList() + case Record: + listMap = List{v.ToMap()} case List: listMap = v case Map: @@ -436,7 +440,7 @@ func (bs *dbBase) doBatchInsert(link dbLink, table string, list interface{}, opt } for i := 0; i < len(listMap); i++ { for _, k := range keys { - params = append(params, listMap[i][k]) + params = append(params, convertParam(listMap[i][k])) } values = append(values, valueHolderStr) if len(values) == batchNum { @@ -479,17 +483,13 @@ func (bs *dbBase) doBatchInsert(link dbLink, table string, list interface{}, opt // CURD操作:数据更新,统一采用sql预处理。 // data参数支持string/map/struct/*struct类型。 func (bs *dbBase) Update(table string, data interface{}, condition interface{}, args ...interface{}) (sql.Result, error) { - link, err := bs.db.Master() - if err != nil { - return nil, err - } - return bs.db.doUpdate(link, table, data, condition, args ...) + newWhere, newArgs := formatCondition(condition, args) + return bs.db.doUpdate(nil, table, data, newWhere, newArgs ...) } // CURD操作:数据更新,统一采用sql预处理。 // data参数支持string/map/struct/*struct类型类型。 -func (bs *dbBase) doUpdate(link dbLink, table string, data interface{}, condition interface{}, args ...interface{}) (result sql.Result, err error) { - params := ([]interface{})(nil) +func (bs *dbBase) doUpdate(link dbLink, table string, data interface{}, condition string, args ...interface{}) (result sql.Result, err error) { updates := "" charL, charR := bs.db.getChars() // 使用反射进行类型判断 @@ -499,43 +499,51 @@ func (bs *dbBase) doUpdate(link dbLink, table string, data interface{}, conditio rv = rv.Elem() kind = rv.Kind() } + params := []interface{}(nil) switch kind { case reflect.Map: fallthrough case reflect.Struct: var fields []string for k, v := range gconv.Map(data) { fields = append(fields, fmt.Sprintf("%s%s%s=?", charL, k, charR)) - params = append(params, gconv.String(v)) + params = append(params, convertParam(v)) } updates = strings.Join(fields, ",") default: updates = gconv.String(data) } - for _, v := range args { - params = append(params, gconv.String(v)) + if len(params) > 0 { + args = append(params, args...) } + // 如果没有传递link,那么使用默认的写库对象 if link == nil { if link, err = bs.db.Master(); err != nil { return nil, err } } - newWhere, newArgs := formatCondition(condition, params) - return bs.db.doExec(link, fmt.Sprintf("UPDATE %s SET %s WHERE %s", table, updates, newWhere), newArgs...) + if len(condition) == 0 { + return bs.db.doExec(link, fmt.Sprintf("UPDATE %s SET %s", table, updates), args...) + } + return bs.db.doExec(link, fmt.Sprintf("UPDATE %s SET %s WHERE %s", table, updates, condition), args...) } // CURD操作:删除数据 func (bs *dbBase) Delete(table string, condition interface{}, args ...interface{}) (result sql.Result, err error) { - link, err := bs.db.Master() - if err != nil { - return nil, err - } - return bs.db.doDelete(link, table, condition, args ...) + newWhere, newArgs := formatCondition(condition, args) + return bs.db.doDelete(nil, table, newWhere, newArgs ...) } // CURD操作:删除数据 -func (bs *dbBase) doDelete(link dbLink, table string, condition interface{}, args ...interface{}) (result sql.Result, err error) { - newWhere, newArgs := formatCondition(condition, args) - return bs.db.doExec(link, fmt.Sprintf("DELETE FROM %s WHERE %s", table, newWhere), newArgs...) +func (bs *dbBase) doDelete(link dbLink, table string, condition string, args ...interface{}) (result sql.Result, err error) { + if link == nil { + if link, err = bs.db.Master(); err != nil { + return nil, err + } + } + if len(condition) == 0 { + return bs.db.doExec(link, fmt.Sprintf("DELETE FROM %s", table), args...) + } + return bs.db.doExec(link, fmt.Sprintf("DELETE FROM %s WHERE %s", table, condition), args...) } // 获得缓存对象 diff --git a/g/database/gdb/gdb_func.go b/g/database/gdb/gdb_func.go index 679ed07d2..a1fd04b1e 100644 --- a/g/database/gdb/gdb_func.go +++ b/g/database/gdb/gdb_func.go @@ -78,8 +78,9 @@ func formatCondition(where interface{}, args []interface{}) (newWhere string, ne default: buffer.WriteString(gconv.String(where)) } + // 没有任何条件查询参数,直接返回 if buffer.Len() == 0 { - buffer.WriteString("1=1") + return "", args } newWhere = buffer.String() tmpArgs = append(tmpArgs, args...) @@ -125,6 +126,22 @@ func formatCondition(where interface{}, args []interface{}) (newWhere string, ne return } +// 将预处理参数转换为底层数据库引擎支持的格式。 +// 主要是判断参数是否为复杂数据类型,如果是,那么转换为基础类型。 +func convertParam(value interface{}) interface{} { + rv := reflect.ValueOf(value) + kind := rv.Kind() + if kind == reflect.Ptr { + rv = rv.Elem() + kind = rv.Kind() + } + switch kind { + case reflect.Struct: + return gconv.String(value) + } + return value +} + // 打印SQL对象(仅在debug=true时有效) func printSql(v *Sql) { s := fmt.Sprintf("%s, %v, %s, %s, %d ms, %s", v.Sql, v.Args, diff --git a/g/database/gdb/gdb_model.go b/g/database/gdb/gdb_model.go index 8339dbe7b..d87a85fd6 100644 --- a/g/database/gdb/gdb_model.go +++ b/g/database/gdb/gdb_model.go @@ -247,13 +247,17 @@ func (md *Model) Data(data ...interface{}) *Model { } model.data = m } else { - switch data[0].(type) { + switch params := data[0].(type) { + case Result: + model.data = params.ToList() + case Record: + model.data = params.ToMap() case List: - model.data = data[0] + model.data = params case Map: - model.data = data[0] + model.data = params default: - rv := reflect.ValueOf(data[0]) + rv := reflect.ValueOf(params) kind := rv.Kind() if kind == reflect.Ptr { rv = rv.Elem() @@ -420,9 +424,9 @@ func (md *Model) Update() (result sql.Result, err error) { } } if md.tx == nil { - return md.db.Update(md.tables, md.data, md.where, md.whereArgs ...) + return md.db.doUpdate(nil, md.tables, md.data, md.where, md.whereArgs ...) } else { - return md.tx.Update(md.tables, md.data, md.where, md.whereArgs ...) + return md.tx.doUpdate(md.tables, md.data, md.where, md.whereArgs ...) } } @@ -434,9 +438,9 @@ func (md *Model) Delete() (result sql.Result, err error) { } }() if md.tx == nil { - return md.db.Delete(md.tables, md.where, md.whereArgs...) + return md.db.doDelete(nil, md.tables, md.where, md.whereArgs...) } else { - return md.tx.Delete(md.tables, md.where, md.whereArgs...) + return md.tx.doDelete(md.tables, md.where, md.whereArgs...) } } diff --git a/g/database/gdb/gdb_transaction.go b/g/database/gdb/gdb_transaction.go index 9f90e458c..b90e664e4 100644 --- a/g/database/gdb/gdb_transaction.go +++ b/g/database/gdb/gdb_transaction.go @@ -162,14 +162,28 @@ func (tx *TX) BatchSave(table string, list interface{}, batch...int) (sql.Result return tx.db.doBatchInsert(tx.tx, table, list, OPTION_SAVE, batch...) } -// CURD操作:数据更新,统一采用sql预处理 -// data参数支持字符串或者关联数组类型,内部会自行做判断处理 +// CURD操作:数据更新,统一采用sql预处理, +// data参数支持字符串或者关联数组类型,内部会自行做判断处理. func (tx *TX) Update(table string, data interface{}, condition interface{}, args ...interface{}) (sql.Result, error) { + newWhere, newArgs := formatCondition(condition, args) + return tx.doUpdate(table, data, newWhere, newArgs ...) +} + +// 与Update方法的区别是不处理条件参数 +func (tx *TX) doUpdate(table string, data interface{}, condition string, args ...interface{}) (sql.Result, error) { return tx.db.doUpdate(tx.tx, table, data, condition, args ...) } // CURD操作:删除数据 func (tx *TX) Delete(table string, condition interface{}, args ...interface{}) (sql.Result, error) { + newWhere, newArgs := formatCondition(condition, args) + return tx.doDelete(table, newWhere, newArgs ...) +} + +// 与Delete方法的区别是不处理条件参数 +func (tx *TX) doDelete(table string, condition string, args ...interface{}) (sql.Result, error) { return tx.db.doDelete(tx.tx, table, condition, args ...) } + + diff --git a/g/database/gdb/gdb_unit_init_test.go b/g/database/gdb/gdb_unit_init_test.go index d460f1fa3..dba6bc9b0 100644 --- a/g/database/gdb/gdb_unit_init_test.go +++ b/g/database/gdb/gdb_unit_init_test.go @@ -1,11 +1,20 @@ package gdb_test import ( + "fmt" + "github.com/gogf/gf/g" + "github.com/gogf/gf/g/container/garray" "github.com/gogf/gf/g/database/gdb" + "github.com/gogf/gf/g/os/gtime" "github.com/gogf/gf/g/test/gtest" "os" ) +const ( + // 初始化表数据量 + INIT_DATA_SIZE = 10 +) + var ( // 数据库对象/接口 db gdb.DB @@ -26,10 +35,12 @@ func init() { Priority: 1, } hostname, _ := os.Hostname() + // 本地测试hack if hostname == "ijohn" { node.Pass = "12345678" } - gdb.AddDefaultConfigNode(node) + gdb.AddConfigNode("test", node) + gdb.AddConfigNode(gdb.DEFAULT_GROUP_NAME, node) if r, err := gdb.New(); err != nil { gtest.Fatal(err) } else { @@ -39,12 +50,25 @@ func init() { if _, err := db.Exec("CREATE DATABASE IF NOT EXISTS `test` CHARACTER SET UTF8"); err != nil { gtest.Fatal(err) } + // 选择操作数据库 db.SetSchema("test") - if _, err := db.Exec("DROP TABLE IF EXISTS `user`"); err != nil { - gtest.Fatal(err) + // 创建默认用户表 + createTable("user") +} + +// 创建指定名称的user测试表,当table为空时,创建随机的表名。 +// 创建的测试表默认没有任何数据。 +// 执行完成后返回该表名。 +// TODO 支持更多数据库 +func createTable(table...string) (name string) { + if len(table) > 0 { + name = table[0] + } else { + name = fmt.Sprintf(`user_%d`, gtime.Nanosecond()) } - if _, err := db.Exec(` - CREATE TABLE user ( + dropTable(name) + if _, err := db.Exec(fmt.Sprintf(` + CREATE TABLE %s ( id int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID', passport varchar(45) NOT NULL COMMENT '账号', password char(32) NOT NULL COMMENT '密码', @@ -52,7 +76,38 @@ func init() { create_time timestamp NOT NULL COMMENT '创建时间/注册时间', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; - `); err != nil { + `, name)); err != nil { + gtest.Fatal(err) + } + return +} + +// 删除指定表. +func dropTable(table string) { + if _, err := db.Exec(fmt.Sprintf("DROP TABLE IF EXISTS `%s`", table)); err != nil { gtest.Fatal(err) } } + +// See createTable. +// 创建测试表,并初始化默认数据。 +func createInitTable(table...string) (name string) { + name = createTable(table...) + array := garray.New(true) + for i := 1; i <= INIT_DATA_SIZE; i++ { + array.Append(g.Map{ + "id" : i, + "passport" : fmt.Sprintf(`t%d`, i), + "password" : fmt.Sprintf(`p%d`, i), + "nickname" : fmt.Sprintf(`T%d`, i), + "create_time" : gtime.Now().String(), + }) + } + result, err := db.Table(name).Data(array.Slice()).Insert() + gtest.Assert(err, nil) + + n, e := result.RowsAffected() + gtest.Assert(e, nil) + gtest.Assert(n, INIT_DATA_SIZE) + return +} diff --git a/g/database/gdb/gdb_unit_method_test.go b/g/database/gdb/gdb_unit_method_test.go index 38b03ede5..b38569509 100644 --- a/g/database/gdb/gdb_unit_method_test.go +++ b/g/database/gdb/gdb_unit_method_test.go @@ -9,6 +9,15 @@ import ( "testing" ) +func TestDbBase_Ping(t *testing.T) { + gtest.Case(t, func() { + err1 := db.PingMaster() + err2 := db.PingSlave() + gtest.Assert(err1, nil) + gtest.Assert(err2, nil) + }) +} + func TestDbBase_Query(t *testing.T) { if _, err := db.Query("SELECT ?", 1); err != nil { gtest.Fatal(err) @@ -144,57 +153,102 @@ func TestDbBase_Insert(t *testing.T) { } func TestDbBase_BatchInsert(t *testing.T) { - if r, err := db.BatchInsert("user", g.List { - { - "id" : 2, - "passport" : "t2", - "password" : "25d55ad283aa400af464c76d713c07ad", - "nickname" : "T2", - "create_time" : gtime.Now().String(), - }, - { - "id" : 3, - "passport" : "t3", - "password" : "25d55ad283aa400af464c76d713c07ad", - "nickname" : "T3", - "create_time" : gtime.Now().String(), - }, - }, 1); err != nil { - gtest.Fatal(err) - } else { - n, _ := r.RowsAffected() - gtest.Assert(n, 2) - } + gtest.Case(t, func() { + if r, err := db.BatchInsert("user", g.List { + { + "id" : 2, + "passport" : "t2", + "password" : "25d55ad283aa400af464c76d713c07ad", + "nickname" : "T2", + "create_time" : gtime.Now().String(), + }, + { + "id" : 3, + "passport" : "t3", + "password" : "25d55ad283aa400af464c76d713c07ad", + "nickname" : "T3", + "create_time" : gtime.Now().String(), + }, + }, 1); err != nil { + gtest.Fatal(err) + } else { + n, _ := r.RowsAffected() + gtest.Assert(n, 2) + } - result, err := db.Delete("user", "id>?", 1) - if err != nil { - gtest.Fatal(err) - } - n, _ := result.RowsAffected() - gtest.Assert(n, 2) - - // []interface{} - if r, err := db.BatchInsert("user", []interface{} { - map[interface{}]interface{} { - "id" : 2, - "passport" : "t2", - "password" : "25d55ad283aa400af464c76d713c07ad", - "nickname" : "T2", - "create_time" : gtime.Now().String(), - }, - map[interface{}]interface{} { - "id" : 3, - "passport" : "t3", - "password" : "25d55ad283aa400af464c76d713c07ad", - "nickname" : "T3", - "create_time" : gtime.Now().String(), - }, - }, 1); err != nil { - gtest.Fatal(err) - } else { - n, _ := r.RowsAffected() + result, err := db.Delete("user", "id>?", 1) + if err != nil { + gtest.Fatal(err) + } + n, _ := result.RowsAffected() gtest.Assert(n, 2) - } + + // []interface{} + if r, err := db.BatchInsert("user", []interface{} { + map[interface{}]interface{} { + "id" : 2, + "passport" : "t2", + "password" : "25d55ad283aa400af464c76d713c07ad", + "nickname" : "T2", + "create_time" : gtime.Now().String(), + }, + map[interface{}]interface{} { + "id" : 3, + "passport" : "t3", + "password" : "25d55ad283aa400af464c76d713c07ad", + "nickname" : "T3", + "create_time" : gtime.Now().String(), + }, + }, 1); err != nil { + gtest.Fatal(err) + } else { + n, _ := r.RowsAffected() + gtest.Assert(n, 2) + } + }) + // batch insert map + gtest.Case(t, func() { + table := createTable() + defer dropTable(table) + result, err := db.BatchInsert(table, g.Map{ + "id" : 1, + "passport" : "t1", + "password" : "p1", + "nickname" : "T1", + "create_time" : gtime.Now().String(), + }) + if err != nil { + gtest.Fatal(err) + } + n, _ := result.RowsAffected() + gtest.Assert(n, 1) + }) + // batch insert struct + gtest.Case(t, func() { + table := createTable() + defer dropTable(table) + + type User struct { + Id int `gconv:"id"` + Passport string `gconv:"passport"` + Password string `gconv:"password"` + NickName string `gconv:"nickname"` + CreateTime *gtime.Time `gconv:"create_time"` + } + user := &User{ + Id : 1, + Passport : "t1", + Password : "p1", + NickName : "T1", + CreateTime : gtime.Now(), + } + result, err := db.BatchInsert(table, user) + if err != nil { + gtest.Fatal(err) + } + n, _ := result.RowsAffected() + gtest.Assert(n, 1) + }) } func TestDbBase_Save(t *testing.T) { diff --git a/g/database/gdb/gdb_unit_model_test.go b/g/database/gdb/gdb_unit_model_test.go index 2ba7ab5ef..1b4864f3d 100644 --- a/g/database/gdb/gdb_unit_model_test.go +++ b/g/database/gdb/gdb_unit_model_test.go @@ -90,29 +90,66 @@ func TestModel_Insert(t *testing.T) { } func TestModel_Batch(t *testing.T) { - result, err := db.Table("user").Filter().Data(g.List{ - { - "id" : 2, - "uid" : 2, - "passport" : "t2", - "password" : "25d55ad283aa400af464c76d713c07ad", - "nickname" : "T2", - "create_time" : gtime.Now().String(), - }, - { - "id" : 3, - "uid" : 3, - "passport" : "t3", - "password" : "25d55ad283aa400af464c76d713c07ad", - "nickname" : "T3", - "create_time" : gtime.Now().String(), - }, - }).Batch(1).Insert() - if err != nil { - gtest.Fatal(err) - } - n, _ := result.RowsAffected() - gtest.Assert(n, 2) + // batch insert + gtest.Case(t, func() { + result, err := db.Table("user").Filter().Data(g.List{ + { + "id" : 2, + "uid" : 2, + "passport" : "t2", + "password" : "25d55ad283aa400af464c76d713c07ad", + "nickname" : "T2", + "create_time" : gtime.Now().String(), + }, + { + "id" : 3, + "uid" : 3, + "passport" : "t3", + "password" : "25d55ad283aa400af464c76d713c07ad", + "nickname" : "T3", + "create_time" : gtime.Now().String(), + }, + }).Batch(1).Insert() + if err != nil { + gtest.Fatal(err) + } + n, _ := result.RowsAffected() + gtest.Assert(n, 2) + }) + + // batch save + gtest.Case(t, func() { + table := createInitTable() + defer dropTable(table) + result, err := db.Table(table).All() + gtest.Assert(err, nil) + gtest.Assert(len(result), INIT_DATA_SIZE) + for _, v := range result { + v["nickname"].Set(v["nickname"].String() + v["id"].String()) + } + r, e := db.Table(table).Data(result).Save() + gtest.Assert(e, nil) + n, e := r.RowsAffected() + gtest.Assert(e, nil) + gtest.Assert(n, INIT_DATA_SIZE*2) + }) + + // batch replace + gtest.Case(t, func() { + table := createInitTable() + defer dropTable(table) + result, err := db.Table(table).All() + gtest.Assert(err, nil) + gtest.Assert(len(result), INIT_DATA_SIZE) + for _, v := range result { + v["nickname"].Set(v["nickname"].String() + v["id"].String()) + } + r, e := db.Table(table).Data(result).Replace() + gtest.Assert(e, nil) + n, e := r.RowsAffected() + gtest.Assert(e, nil) + gtest.Assert(n, INIT_DATA_SIZE*2) + }) } func TestModel_Replace(t *testing.T) { @@ -146,12 +183,23 @@ func TestModel_Save(t *testing.T) { } func TestModel_Update(t *testing.T) { - result, err := db.Table("user").Data("passport", "t22").Where("passport=?", "t2").Update() - if err != nil { - gtest.Fatal(err) - } - n, _ := result.RowsAffected() - gtest.Assert(n, 1) + gtest.Case(t, func() { + result, err := db.Table("user").Data("passport", "t22").Where("passport=?", "t2").Update() + if err != nil { + gtest.Fatal(err) + } + n, _ := result.RowsAffected() + gtest.Assert(n, 1) + }) + + gtest.Case(t, func() { + result, err := db.Table("user").Data("passport", "t2").Where("passport='t22'").Update() + if err != nil { + gtest.Fatal(err) + } + n, _ := result.RowsAffected() + gtest.Assert(n, 1) + }) } func TestModel_Clone(t *testing.T) { diff --git a/g/util/gconv/gconv_slice.go b/g/util/gconv/gconv_slice.go index 14b62e834..456aa7cac 100644 --- a/g/util/gconv/gconv_slice.go +++ b/g/util/gconv/gconv_slice.go @@ -320,4 +320,24 @@ func Interfaces(i interface{}) []interface{} { } return array } +} + +// 将类型转换为[]map[string]interface{}类型. +func Maps(i interface{}) []map[string]interface{} { + if i == nil { + return nil + } + if r, ok := i.([]map[string]interface{}); ok { + return r + } else { + array := Interfaces(i) + if len(array) == 0 { + return nil + } + list := make([]map[string]interface{}, len(array)) + for k, v := range array { + list[k] = Map(v) + } + return list + } } \ No newline at end of file