英文:
Cast nested []interface{} to []map[string]interface{}
问题
我的目标是从一个json生成一个map[string]interface{}
结构,其中所有嵌套的[]interface{}
都被转换为[]map[string]interface{}
。这是因为我们正在使用https://github.com/ivahaev/go-xlsx-templater模块从json填充xlsx。所有的数据都被期望放在一个map[string]interface{}
结构中,其中所有嵌套的[]interface{}
都是[]map[string]interface{}
。
以下是一个输入json的例子:
{
"totalAmount": 4,
"subtotal": 4,
"Vendors": [
{
"MethodOfTenders": [
{
"order": 1,
"fees": 2
},
{
"order": 1,
"fees": 2
}
],
"subtotalFees": 4
},
{
"MethodOfTenders": [
{
"order": 1,
"fees": 2
},
{
"order": 1,
"fees": 1
}
],
"subtotalFees": 3
}
]
}
当解组成一个map[string]interface{}
时,我得到以下结构:
map[string]interface{}:
"totalAmount": 4 interface{}(float64)
"subtotal": 4 interface{}(float64)
"Vendors": interface{}([]interface{})
[0]: interface(map[string]interface{})
"MethodOfTenders": interface{}([]interface{})
[0]: interface(map[string]interface{})
"order": 1 interface{}(float64)
"fees": 2 interface{}(float64)
[1]: interface(map[string]interface{})
"order": 1 interface{}(float64)
"fees": 2 interface{}(float64)
"subtotalFees": 4 interface{}(float64)
[1]: interface(map[string]interface{})
"MethodOfTenders": interface{}([]interface{})
[0]: interface(map[string]interface{})
"order": 1 interface{}(float64)
"fees": 2 interface{}(float64)
[1]: interface(map[string]interface{})
"order": 1 interface{}(float64)
"fees": 2 interface{}(float64)
"subtotalFees": 3 interface{}(float64)
经过一些解析,遍历每个[]interface{}
并创建一个[]map[string]interface{}
来存储每个嵌套的map[string]interface
。
我得到了期望的结果,其中所有的[]interface{}
都是[]map[string]interface{}
。
map[string]interface{}:
"totalAmount": 4 interface{}(float64)
"subtotal": 4 interface{}(float64)
"Vendors": interface{}([]map[string]interface{})
[0]: interface(map[string]interface{})
"MethodOfTenders": interface{}([]map[string]interface{})
[0]: interface(map[string]interface{})
"order": 1 interface{}(float64)
"fees": 2 interface{}(float64)
[1]: interface(map[string]interface{})
"order": 1 interface{}(float64)
"fees": 2 interface{}(float64)
"subtotalFees": 4 interface{}(float64)
[1]: interface(map[string]interface{})
"MethodOfTenders": interface{}([]map[string]interface{})
[0]: interface(map[string]interface{})
"order": 1 interface{}(float64)
"fees": 2 interface{}(float64)
[1]: interface(map[string]interface{})
"order": 1 interface{}(float64)
"fees": 2 interface{}(float64)
"subtotalFees": 3 interface{}(float64)
有没有办法递归地遍历所有的map[string]interface{}
并将[]interface{}
更改为[]map[string]interface{}
?
英文:
My goal is to generate from a json a map[string]interface
structure , where all nested []interface{}
are casted into []map[string]interface{}
. This is because we are using the module https://github.com/ivahaev/go-xlsx-templater to fill xlsx from json . And all the data is expected to be into a map[string]interface{}
struct where all the nested []interface{}
are []map[string]interface
.
Having a json as an input like the following :
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
{
"totalAmount": 4,
"subtotal": 4,
"Vendors": [{
"MethodOfTenders": [{
"order": 1,
"fees": 2
}, {
"order": 1,
"fees": 2
}],
"subtotalFees": 4
},
{
"MethodOfTenders": [{
"order": 1,
"fees": 2
}, {
"order": 1,
"fees": 1
}],
"subtotalFees": 3
}
]
}
<!-- end snippet -->
When unmarshalling into an map[string]interface{}
.I get the following struct :
map[string]interface{}:
"totalAmount" : 4 interface{}(float64)
"subtotal" : 4 interface{}(float64)
"Vendors" : interface{}([]interface{})
[0]: interface(map[string]interface{})
"MethodOfTenders" : interface{}([]interface{})
[0] : interface(map[string]interface{})
"order" : 1 interface{}(float64)
"fees" : 2 interface{}(float64)
[1] : interface(map[string]interface{})
"order" : 1 interface{}(float64),
"fees" : 2 interface{}(float64)
"subtotalFees" : 4 interface{}(float64)
[1]: interface(map[string]interface{})
"MethodOfTenders" : interface{}([]interface{})
[0] : interface(map[string]interface{})
"order" : 1 interface{}(float64)
"fees" : 2 interface{}(float64)
[1] : interface(map[string]interface{})
"order" : 1 interface{}(float64),
"fees" : 2 interface{}(float64)
"subtotalFees" : 3 interface{}(float64)
After doing some parsing , ranging each []interface{} and creating a a []map[string]interface{} to store each one of the nested map[string]interface .
I got the desired result where all []interfaces{} are []map[string]interface{}
map[string]interface{}:
"totalAmount" : 4 interface{}(float64)
"subtotal" : 4 interface{}(float64)
"Vendors" : interface{}([]map[string]interface{})
[0]: interface(map[string]interface{})
"MethodOfTenders" : interface{}([]map[string]interface{})
[0] : interface(map[string]interface{})
"order" : 1 interface{}(float64)
"fees" : 2 interface{}(float64)
[1] : interface(map[string]interface{})
"order" : 1 interface{}(float64),
"fees" : 2 interface{}(float64)
"subtotalFees" : 4 interface{}(float64)
[1]: interface(map[string]interface{})
"MethodOfTenders" : interface{}([]map[string]interface{})
[0] : interface(map[string]interface{})
"order" : 1 interface{}(float64)
"fees" : 2 interface{}(float64)
[1] : interface(map[string]interface{})
"order" : 1 interface{}(float64),
"fees" : 2 interface{}(float64)
"subtotalFees" : 3 interface{}(float64)
Is there any way to walk al the map[string]interface recursively and change the []interfaces{} for []map[string]interface ?
EDIT:
Here the repository with the full template and json , currently working with the reflection approach .
答案1
得分: 1
更新
所以我注意到你添加了一个包含你目前所拥有的内容的存储库链接。模板有些奇怪(其中一些值不是你的示例数据的一部分)。我已经删除了除了实际上在你的示例数据 JSON 中的字段之外的所有字段。事实证明,你正在使用的包在某些方面还比较粗糙(它确实处理了嵌套值中的类型断言,但在顶层没有处理)。我个人建议要么 fork 这个包并修复这个问题,但同时,如果你知道你想要迭代的字段,我用以下代码在大约 5 分钟内解决了这个问题:
var jsonMap map[string]interface{}
data := sampleData()
err := json.Unmarshal(data, &jsonMap)
if err != nil {
panic("Final log")
}
ctx := jsonMap
v := jsonMap["Vendors"]
vs := v.([]interface{})
vendors := make([]map[string]interface{}, 0, len(vs))
for _, v := range vs {
m := v.(map[string]interface{})
vendors = append(vendors, m)
}
jsonMap["Vendors"] = vendors
doc := xlst.New()
err = doc.ReadTemplate("export_support_template.xlsx")
if err != nil {
fmt.Println("ERROR OPENING THE TEMPLATE: ", err)
panic("error opening template")
}
err = doc.Render(ctx)
if err != nil {
fmt.Println("ERROR RENDERING THE TEMPLATE: ", err)
panic("error rendering template")
}
err = doc.Save("report.xlsx")
如你所见,我只是对数据进行了解组合。然后,我专注于 Vendors
键,将其转换为切片(v.([]inteface{})
),创建了一个类型为 []map[string]interface{}
的新变量,并在这个简单的循环中将切片的元素转换为映射并复制到数据中:
for _, v := range vs {
m := v.(map[string]interface{})
vendors = append(vendors, m)
}
然后,我只是重新分配了原始映射中的 Vendors
键(ctx["Vendors"] = vendors
),并将其传递给模板。只需要不到 10 行代码,一切都可以正常工作。不需要反射或任何其他魔法。只需要直接的类型转换。如果不这样做,你正在使用的包会抱怨在 interface{}
类型上使用 range
(而不是首先检查键是否可以成功转换为 []interface{}
)。我从你的主文件中删除了所有其他函数(除了示例数据函数),运行了 go build
并执行了 ./json_marshaller
。它毫不费力地生成了一个正确填充的 xlsx 文件。很简单。
我仍然建议你实际上创建一个 PR 到 templater 包,这样你就不必自己处理这些东西。这应该是一个相当简单的更改,但在此期间:这种方法完全有效。
包含你的模板
查看 go-xlsx-templater 存储库,我真的不明白你为什么需要其他东西:
data := map[string]interface{}
_ = json.Unmarshal(input, &data)
为什么这样就可以了呢?因为 go-xlsx-templater 已经对这个映射中的所有 interface{}
值执行了类型转换。它会遍历映射,检查其中的值是否为 []interface{}
类型,然后迭代这个切片并检查其中是否有 map[string]interface{}
类型的值。这些都在源代码中。根据他们自己的文档,如果你的模板看起来像这样,它应该可以正常工作:
| Total: | {{ totalAmount }} |
| Subtotal: | {{ subtotal }} |
| | Vendor subtotal | fees | order |
| {{ range Vendors }} |
| | | {{ MethodsOfTender.Fees }} | {{ MethodsOfTender.order }} |
| | {{ subtotalFees }} |
| {{ end }} |
你应该得到正确填充的电子表格:
| Total: | 4 | | |
| Subtotal: | 4 | | |
| | Vendor subtotal | fees | order |
| | | 2 | 1 |
| | | 2 | 1 |
| | 4 | | |
| | | 2 | 1 |
| | | 1 | 1 |
| | 3 | | |
基本上,你展示的数据集与该存储库在他们自己的**主 README 文件**中给出的示例没有什么不同。上下文数据本身包含了嵌套的映射,以及一个显示如何使用它的电子表格模板的屏幕截图。坦率地说,我觉得你的问题基本上就是一个 RTFM 的情况...
但是,如果没有你的模板,就无法确定,所以我将完整地保留我的初始答案...
如果我正确理解你的问题,你想要一种使用类型断言/转换从类型为 map[string]interface{}
的映射中提取键,并将类型为 []interface{}
的值用作 []map[string]interface{}
的方法。在这个特定的示例中,你需要将顶级键 Vendors
作为 []map[string]interface{}
访问(而不是 interface{}
或 []interface{}
)。如果这是你要寻找的,你可以很容易地做到这一点:
data := map[string]interface{}{}
if err := json.Unmarshal(jsonBytes, &data); err != nil {
// 处理错误
}
nested := map[string][]map[string]interface{}{} // 需要作为映射切片访问的数据子集
for k, v := range data {
if s, ok := v.([]interface{}); ok {
// 这个键的类型是 []interface{},看看我们是否可以将其用作映射切片
if mapS, err := sliceAnyToMap(s); err == nil {
nested[k] = mapS
} // 如果需要,处理错误
}
}
func sliceAnyToMap(s []any) ([]map[string]interface{}, error) {
ret := make([]map[string]interface{}, 0, len(s))
for _, v := range s {
if m, ok := v.(map[string]interface{}); ok {
ret = append(ret, m)
} else {
return nil, errors.New("slice contains non-map data")
}
}
return ret, nil
}
通过这样做,你将得到一个名为 nested
的变量,其中包含像 Vendors
这样的键,其数据可以作为 []map[string]interface{}
访问。这可以传递给你的模板,以填充电子表格中的供应商数据。
在这里查看演示
现在,在一些 Vendors
键中,还有一些数据,这些数据本身是一个映射,仍然无法直接访问。将这个 sliceAnyToMap
的东西拆分成一个函数将使得更容易转换你需要的所有数据。
现在,说了这么多,我个人认为这实际上是一个非常典型的 X-Y 问题。你在这里展示的数据看起来具有非常明确定义的结构。与其在 map[string]interface{}
等等上纠缠不清,不如使用实际的类型来读取、写入和维护会更容易。根据你在问题中包含的内容,只需要几个类型就可以了:
type Data struct {
Total float64 `json:"totalAmount"`
Subtotal float64 `json:"subtotal"`
Vendors []Vendor `json:"Vendors"`
}
type Vendor struct {
MethodsOfTender []MOT `json:"MethodsOfTender"`
SubtotalFees float64 `json:"subtotalFees"`
}
type MOT struct {
Fees float64 `json:"fees"`
Order float64 `json:"order"`
}
使用这些类型,你可以快速、轻松地将给定的输入解析为一个格式,这个格式在后续的使用中要容易得多:
data := Data{}
if err := json.Unmarshal(input, &data); err != nil {
// 处理错误
}
for _, vendor := range data.Vendors {
fmt.Printf("Vendor subtotal fees: %f\n", vendor.SubtotalFees)
for i, mot := range vendor.MethodsOfTender {
fmt.Printf("MOT %d:\nOrder: %f\nFees: %f\n\n", i+1, mot.Order, mot.Fees)
}
fmt.Println("-----")
}
在这里查看演示
我还想知道是否所有的数值都应该是 float64
。当然,对于金额和/或费用(假设它们是货币值),这是有意义的,但 Order
字段可能包含一个订单号,我不希望它是一个像 0.123 这样的值。
最后,如果你需要将这些数据传递给一个期望一个映射的模板引擎之类的东西,那么使用上述类型做到这一点是非常简单的。有几种方法可以做到这一点。简单的方法是:
func (d Data) ToMap() (map[string]interface{}, error) {
raw, err := json.Marshal(d)
if err != nil {
return nil, err
}
asMap := map[string]interface{}{}
if err := json.Unmarshal(raw, &asMap); err != nil {
return nil, err
}
return asMap, nil
}
JSON 编组很容易实现,但效率稍低,所以如果性能非常重要,你可以花几分钟编写一些类似的方法:
func (d Data) ToMap() map[string]interface{} {
vendors := make([]map[string]interface{}, 0, len(d.Vendors))
for _, v := range d.Vendors {
vendors = append(vendors, v.ToMap())
}
return map[string]interface{}{
"totalAmount": d.Total,
"subtotal": d.Subtotal,
"Vendors": vendors,
}
}
func (v Vendor) ToMap() map[string]interface{} {
mot := make([]map[string]interface{}, 0, len(v.MethodsOfTender))
for _, m := range v.MethodsOfTender {
mot = append(mot, m.ToMap())
}
return map[string]interface{}{
"MethodsOfTender": mot,
"subtotalFees": v.SubtotalFees,
}
}
func (m MOT) ToMap() map[string]interface{} {
return map[string]interface{}{
"fees": m.Fees,
"order": m.Order,
}
}
然后,将你的 Data
对象转换为映射就像这样简单:
dataMap := data.ToMap()
英文:
Update
So I've since noticed you added a link to a repo containing what you have so far. The template is weird (some of the values in there aren't part of your sample data). I've removed all fields except for the ones that were actually in the sample data JSON you have. As it turns out, the package you are using is quite rough around the edges (it does take care of type assertions in nested values, but not at the top level). I'd personally either fork the package and fix that issue, but in the mean while, if you know what fields you want to iterate over, I got it all to work in about 5 minutes with this:
var jsonMap map[string]interface{}
data := sampleData()
err := json.Unmarshal(data, &jsonMap)
if err != nil {
panic("Final log")
}
ctx := jsonMap
v := jsonMap["Vendors"]
vs := v.([]interface{})
vendors := make([]map[string]interface{}, 0, len(vs))
for _, v := range vs {
m := v.(map[string]interface{})
vendors = append(vendors, m)
}
jsonMap["Vendors"] = vendors
doc := xlst.New()
err = doc.ReadTemplate("export_support_template.xlsx")
if err != nil {
fmt.Println("ERROR OPENING THE TEMPLATE: ", err)
panic("error opening template")
}
err = doc.Render(ctx)
if err != nil {
fmt.Println("ERROR RENDERING THE TEMPLATE: ", err)
panic("error rendering template")
}
err = doc.Save("report.xlsx")
As you can see, I just unmarshalled the data. I then focused on the Vendors
key, cast it to a slice (v.([]inteface{}
), created a new variable of type []map[string]interface{}
, and copied over the data casting the elements of the slice to maps in this simple loop:
for _, v := range vs {
m := v.(map[string]interface{})
vendors = append(vendors, m)
}
Then, I just reassigned the Vendors
key in the original map (ctx["Vendors"] = vendors
), and passed that in to the template. Less than 10 lines of code needed, and everything worked like a charm. No need for reflection, or any other magic. Just straightforward type casts. Without this, the package you're using does complain about the use of range
on an interface{}
type (instead of first checking to see if the key can be successfully cast to a []interface{}
first). I removed all other functions from your main file (except for the sample data one), ran go build
and executed ./json_marshaller
. It churned out a correctly populated xlsx file no problem. Easy.
I'd still recommend you actually create a PR to the templater package so you don't have to handle this stuff yourself. It should be a fairly straightforward change, but for the time being: this approach works perfectly well.
Include your template
Looking at the repository of the go-xlsx-templater, I don't really see why you'd need anything other than this:
data := map[string]interface{}
_ = json.Unmarshal(input, &data)
The reason why this is perfectly fine, is because go-xlsx-templater performs the type conversion of all interface{}
values within this map already. It will traverse the map, and check if any of the values are of the type []interface{}
, it'll then iterate over this slice and check for map[string]interface{}
values inside the slice. It's all in the source code. Going by their own documentation, it should work just fine if your template looks somewhat like this:
| Total: | {{ totalAmount }} |
| Subtotal: | {{ subtotal }} |
| | Vendor subtotal | fees | order |
| {{ range Vendors }} |
| | | {{ MethodsOfTender.Fees }} | {{ MethodsOfTender.order }} |
| | {{ subtotalFees }} |
| {{ end }} |
You should end up with the spreadsheet being populated correctly:
| Total: | 4 | | |
| Subtotal: | 4 | | |
| | Vendor subtotal | fees | order |
| | | 2 | 1 |
| | | 2 | 1 |
| | 4 | | |
| | | 2 | 1 |
| | | 1 | 1 |
| | 3 | | |
Essentially, the data-set you're showing is no different to the example the repo gives in their very own main README file. The context data itself contains nested maps, and a screenshot of a spreadsheet template showing how to use it. Putting it bluntly, I feel like your question is pretty much a case of RTFM...
Without your template, however, there's no way to be sure, so I'm leaving my initial answer in full below...
If I understand what you're asking correctly, you're looking for a way to use type assertions/casts to extract keys from a map of type map[string]interface{}
and use the values of type []interface{}
as []map[string]interface{}
. In this particular example, you need the top level key Vendors
to be accessible as []map[string]interface{}
(as opposed to interface{}
or []interface{}
). If that's what you're looking for, you can do this quite easily:
data := map[string]interface{}{}
if err := json.Unmarshal(jsonBytes, &data); err != nil {
// handle error
}
nested := map[string][]map[string]interface{}{} // subset of data that needs to be accessible as slices of maps
for k, v := range data {
if s, ok := v.([]interface); ok {
// this key is of type []interface, see if we can use it as slice of maps
if mapS, err := sliceAnyToMap(s); err == nil {
nested[k] = mapS
} // handle error if needed
}
}
func sliceAnyToMap(s []any) ([]map[string]interface{}, error) {
ret := make([]map[string]interface{}, 0, len(s))
for _, v := range s {
if m, ok := v.(map[string]interface{}); ok {
ret = append(ret, m)
} else {
return nil, errors.New("slice contains non-map data")
}
}
return ret, nil
}
With this, you'll end up with a variable called nested
which will contain keys like Vendors
, with the data accessible as a []map[string]interface{}
. This can be passed on to your templater to populate the spreadsheet with the vendors data.
Demo here
Now, inside some of the Vendors
key, there is some data which is in turn a a map, which still can't be accessed directly. Breaking up this sliceAnyToMap
stuff into a function will make it easier to convert all data you need fairly easily.
Now, having said all this, I do personally think this is very much an X-Y problem. The data you've shown here looks to be of a very well defined structure. Rather than faffing around with map[string]interface{}
and the like, it would be a whole lot easier to read, write, and maintain if you were to use actual types. Based on what you've included in your question, a couple of types like this would be all it takes:
type Data struct {
Total float64 `json:"totalAmount"`
Subtotal float64 `json:"subtotal"`
Vendors []Vendor `json:"Vendors"`
}
type Vendor struct {
MethodOfTenders []MOT `json:"MethodOfTenders"`
SubtotalFees float64 `json:"subtotalFees"`
}
type MOT struct {
Fees float64 `json:"fees"`
Order float64 `json:"order"`
}
With these types, you can quickly, and easily parse the given input into a format that is much, much easier to use further down the line:
data := Data{}
if err := json.Unmarshal(input, &data); err != nil {
// handle error
}
for _, vendor := range data.Vendors {
fmt.Printf("Vendor subtotal fees: %f\n", vendor.SubtotalFees)
for i, mot := range vendor.MethodOfTenders {
fmt.Printf("MOT %d:\nOrder: %f\nFees: %f\n\n", i+1, mot.Order, mot.Fees)
}
fmt.Println("-----")
}
Demo here
Last couple of things I'm wondering is whether or not all numberic values should be float64
. Sure, it makes sense for amounts and/or fees (assuming they're monetary values), but the Order
field probably contains an order number, which I don't expect to be a value like 0.123.
Finally, if you need to pass this data on to a templater of sorts which expects a map, then it's a fairly trivial thing to do using the aforementioned types all the same. There's a couple of ways to do this. The easy way being:
func (d Data) ToMap() (map[string]interface{}, error) {
raw, err := json.Marshal(d)
if err != nil {
return nil, err
}
asMap := map[string]interface{}{}
if err := json.Unmarshal(raw, &asMap); err != nil {
return nil, err
}
return asMap, nil
}
JSON marshalling is quick to implement, but it's a tad inefficient, so you if performance matters a lot, then you could spend a few minutes writing some methods like:
func (d Data) ToMap() map[string]interface{} {
vendors := make([]map[string]interface{}, 0, len(d.Vendors))
for _, v := range d.Vendors {
vendors = append(vendors, v.ToMap())
}
return map[string]interface{}{
"totalAmount": d.Total,
"subtotal": d.Subtotal,
"Vendors": vendors,
}
}
func (v Vendor) ToMap() map[string]interface{} {
mot := make([]map[string]interface{}, 0, len(v.MethodsOfTender))
for _, m := range v.MethodsOfTender {
mot = append(mot, m.ToMap())
}
return map[string]interface{
"MethodsOfTender": mot,
"subtotalFees": v.SubtotalFees,
}
}
func (m MOT) ToMap() map[string]interface{} {
return map[string]interface{}{
"fees": m.Fees,
"order": m.Order,
}
}
Then, turning your Data
object into a map is as simple as:
dataMap := data.ToMap()
答案2
得分: 0
package main
import (
"encoding/json"
"fmt"
)
func main() {
s := `{
"totalAmount": 4,
"subtotal": 4,
"Vendors": [{
"MethodOfTenders": [{
"order": 1,
"fees": 2
}, {
"order": 1,
"fees": 2
}],
"subtotalFees": 4
},
{
"MethodOfTenders": [{
"order": 1,
"fees": 2
}, {
"order": 1,
"fees": 1
}],
"subtotalFees": 3
}
]
}`
var data map[string]interface{}
if err := json.Unmarshal([]byte(s), &data); err != nil {
fmt.Println(err)
}
fmt.Printf("%+v\n", data)
}
当从JSON解组时,它生成了所需的输出,这是go-xlsx-templater
所需要的。
注意: 对于问题中所述的问题,我在这里
创建了一个修复,并提出了PR
。在修复后不需要进行转换。
应用修复后,剩余的代码包括sampleData()
函数,该函数未包含在示例中。
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"reflect"
"strings"
xlst "github.com/ivahaev/go-xlsx-templater"
)
func main() {
wd, err := os.Getwd()
if err != nil {
log.Fatalf(err.Error())
}
path := strings.Join([]string{wd, ""}, "")
doc := xlst.New()
err = doc.ReadTemplate(path + "/export_support_template.xlsx")
if err != nil {
fmt.Println("ERROR OPENING THE TEMPLATE: ", err)
panic("error opening template")
}
var ctx map[string]interface{}
data := sampleData()
err := json.Unmarshal(data, &ctx)
if err != nil {
fmt.Println("Final log")
}
err = doc.Render(ctx)
if err != nil {
fmt.Println("ERROR RENDERING THE TEMPLATE: ", err)
panic("error rendering template")
}
err = doc.Save(path + "/report.xlsx")
if err != nil {
fmt.Println("ERROR SAVING THE TEMPLATE: ", err)
panic("error saving template")
}
}
<details>
<summary>英文:</summary>
```go
package main
import (
"encoding/json"
"fmt"
)
func main() {
s := `{
"totalAmount": 4,
"subtotal": 4,
"Vendors": [{
"MethodOfTenders": [
{
"order": 1,
"fees": 2
}, {
"order": 1,
"fees": 2
}
],
"subtotalFees": 4
},
{
"MethodOfTenders": [
{
"order": 1,
"fees": 2
}, {
"order": 1,
"fees": 1
}
],
"subtotalFees": 3
}
]
}
`
var data map[string]interface{}
if err := json.Unmarshal([]byte(s), &data); err != nil {
fmt.Println(err)
}
fmt.Printf("%+v\n", data)
}
When Unmarshal from json it is generating expected output which is required by go-xlsx-templater
Note: Created a fix for the issue stated in the question here
raised PR
. After the fix no conversion is required.
After applying the fix the remaining code with sampleData()
function which is not included in the example
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"reflect"
"strings"
xlst "github.com/ivahaev/go-xlsx-templater"
)
func main() {
wd, err := os.Getwd()
if err != nil {
log.Fatalf(err.Error())
}
path := strings.Join([]string{wd, ""}, "")
doc := xlst.New()
err = doc.ReadTemplate(path + "/export_support_template.xlsx")
if err != nil {
fmt.Println("ERROR OPENING THE TEMPLATE: ", err)
panic("error opening template")
}
var ctx map[string]interface{}
data := sampleData()
err := json.Unmarshal(data, &ctx)
if err != nil {
fmt.Println("Final log")
}
err = doc.Render(ctx)
if err != nil {
fmt.Println("ERROR RENDERING THE TEMPLATE: ", err)
panic("error rendering template")
}
err = doc.Save(path + "/report.xlsx")
if err != nil {
fmt.Println("ERROR SAVING THE TEMPLATE: ", err)
panic("error saving template")
}
}
答案3
得分: -2
这个技巧是使用反射来实现的。递归地向下遍历并检查值的类型:
// "Casts" map values to the desired type recursively
func castMap(m map[string]interface{}) map[string]interface{} {
for k := range m {
switch reflect.ValueOf(m[k]).Kind() {
case reflect.Map:
mm, ok := m[k].(map[string]interface{})
if !ok {
panic(fmt.Errorf("Expected map[string]interface{}, got %T", m[k]))
}
m[k] = castMap(mm)
case reflect.Slice, reflect.Array:
ma, ok := m[k].([]interface{})
if !ok {
panic(fmt.Errorf("Expected []interface{}, got %T", m[k]))
}
m[k] = castArray(ma)
default:
// fmt.Printf("%s: %T, kind %v\n", k, m[k], reflect.ValueOf(m[k]).Kind())
continue
}
}
return m
}
// "Casts" slice elements to the desired types recursively
func castArray(a []interface{}) []map[string]interface{} {
res := []map[string]interface{}{}
for i := range a {
switch reflect.ValueOf(a[i]).Kind() {
case reflect.Map:
am, ok := a[i].(map[string]interface{})
if !ok {
panic(fmt.Errorf("Expected map[string]interface{}, got %T", a[i]))
}
am = castMap(am)
res = append(res, am)
default:
panic(fmt.Errorf("Expected map[string]interface{}, got %T", a[i]))
}
}
return res
}
完整的示例代码可以在这里找到:https://go.dev/play/p/MEQRe-f3dY1
输出结果如下:
before: map[string]interface {}{"Vendors":[]interface {}{map[string]interface {}{"MethodOfTenders":[]interface {}{map[string]interface {}{"fees":2, "order":1}, map[string]interface {}{"fees":2, "order":1}}, "subtotalFees":4}, map[string]interface {}{"MethodOfTenders":[]interface {}{map[string]interface {}{"fees":2, "order":1}, map[string]interface {}{"fees":1, "order":1}}, "subtotalFees":3}}, "subtotal":4, "totalAmount":4}
after: map[string]interface {}{"Vendors":[]map[string]interface {}{map[string]interface {}{"MethodOfTenders":[]map[string]interface {}{map[string]interface {}{"fees":2, "order":1}, map[string]interface {}{"fees":2, "order":1}}, "subtotalFees":4}, map[string]interface {}{"MethodOfTenders":[]map[string]interface {}{map[string]interface {}{"fees":2, "order":1}, map[string]interface {}{"fees":1, "order":1}}, "subtotalFees":3}}, "subtotal":4, "totalAmount":4}
看到了吗?"Vendors"
原本是[]interface {}
,现在变成了[]map[string]interface {}
。
"Vendors[].MethodOfTenders"
原本是[]interface {}
,现在变成了[]map[string]interface {}
。
如果函数遇到意外情况,它们会引发 panic。如果需要,可以随意修改它们以返回 error
。
更新
这里是使用类型断言的相同递归算法。
// "Casts" map values to the desired type recursively
func castMap(m map[string]interface{}) map[string]interface{} {
for k := range m {
mm, ok := m[k].(map[string]interface{})
if ok {
m[k] = castMap(mm)
continue
}
ma, ok := m[k].([]interface{})
if ok {
m[k] = castArray(ma)
continue
}
}
return m
}
// "Casts" slice elements to the desired types recursively
func castArray(a []interface{}) []map[string]interface{} {
res := []map[string]interface{}{}
for i := range a {
am, ok := a[i].(map[string]interface{})
if ok {
am = castMap(am)
res = append(res, am)
} else {
panic(fmt.Errorf("Expected map[string]interface{}, got %T", a[i]))
}
}
return res
}
完整的代码可以在这里找到:https://go.dev/play/p/IbOhQqpisie
基准测试
其中一位评论者声称“反射是昂贵的”。但在这里并非如此:
goos: windows
goarch: amd64
pkg: example.org/try/test
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkCastReflect-8 382574 2926 ns/op 2664 B/op 29 allocs/op
BenchmarkCastType-8 418257 2934 ns/op 2664 B/op 29 allocs/op
基准测试代码可以在这里找到:https://go.dev/play/p/Ro8PeVQy8kA
Playground 无法执行基准测试。它应该在真实的 CPU 上运行。
英文:
The trick is performed using reflection. Go down recursively and check the kind of values:
// "Casts" map values to the desired type recursively
func castMap(m map[string]any) map[string]any {
for k := range m {
switch reflect.ValueOf(m[k]).Kind() {
case reflect.Map:
mm, ok := m[k].(map[string]any)
if !ok {
panic(fmt.Errorf("Expected map[string]any, got %T", m[k]))
}
m[k] = castMap(mm)
case reflect.Slice, reflect.Array:
ma, ok := m[k].([]any)
if !ok {
panic(fmt.Errorf("Expected []any, got %T", m[k]))
}
m[k] = castArray(ma)
default:
// fmt.Printf("%s: %T, kind %v\n", k, m[k], reflect.ValueOf(m[k]).Kind())
continue
}
}
return m
}
// "Casts" slice elements to the desired types recursively
func castArray(a []any) []map[string]any {
res := []map[string]any{}
for i := range a {
switch reflect.ValueOf(a[i]).Kind() {
case reflect.Map:
am, ok := a[i].(map[string]any)
if !ok {
panic(fmt.Errorf("Expected map[string]any, got %T", a[i]))
}
am = castMap(am)
res = append(res, am)
default:
panic(fmt.Errorf("Expected map[string]any, got %T", a[i]))
}
}
return res
}
Full example with main
is here: https://go.dev/play/p/MEQRe-f3dY1
It's output:
before: map[string]interface {}{"Vendors":[]interface {}{map[string]interface {}{"MethodOfTenders":[]interface {}{map[string]interface {}{"fees":2, "order":1}, map[string]interface {}{"fees":2, "order":1}}, "subtotalFees":4}, map[string]interface {}{"MethodOfTenders":[]interface {}{map[string]interface {}{"fees":2, "order":1}, map[string]interface {}{"fees":1, "order":1}}, "subtotalFees":3}}, "subtotal":4, "totalAmount":4}
after: map[string]interface {}{"Vendors":[]map[string]interface {}{map[string]interface {}{"MethodOfTenders":[]map[string]interface {}{map[string]interface {}{"fees":2, "order":1}, map[string]interface {}{"fees":2, "order":1}}, "subtotalFees":4}, map[string]interface {}{"MethodOfTenders":[]map[string]interface {}{map[string]interface {}{"fees":2, "order":1}, map[string]interface {}{"fees":1, "order":1}}, "subtotalFees":3}}, "subtotal":4, "totalAmount":4}
See? "Vendors"
was []interface {}
, became []map[string]interface {}
"Vendors[].MethodOfTenders"
were []interface {}
, became []map[string]interface {}
The functions panic if they see something unexpected. Feel free to modify them to returning error
if needed.
UPDATE
Here is the same recursive algoritm using type assertions.
// "Casts" map values to the desired type recursively
func castMap(m map[string]any) map[string]any {
for k := range m {
mm, ok := m[k].(map[string]any)
if ok {
m[k] = castMap(mm)
continue
}
ma, ok := m[k].([]any)
if ok {
m[k] = castArray(ma)
continue
}
}
return m
}
// "Casts" slice elements to the desired types recursively
func castArray(a []any) []map[string]any {
res := []map[string]any{}
for i := range a {
am, ok := a[i].(map[string]any)
if ok {
am = castMap(am)
res = append(res, am)
} else {
panic(fmt.Errorf("Expected map[string]any, got %T", a[i]))
}
}
return res
}
Full code https://go.dev/play/p/IbOhQqpisie
BENCHMARK
One of the commenters made a claim that reflection is expensive
. This is not the case here:
goos: windows
goarch: amd64
pkg: example.org/try/test
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkCastReflect-8 382574 2926 ns/op 2664 B/op 29 allocs/op
BenchmarkCastType-8 418257 2934 ns/op 2664 B/op 29 allocs/op
The benchmark is here: https://go.dev/play/p/Ro8PeVQy8kA
Playground doesn't execute benchmarks. It should be run on a real CPU
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论