英文:
Nil pointer evaluation in template...why? Is there a better strategy?
问题
我正在尝试包装html/template,以确保我的模板中有特定的数据(例如会话数据),除了我想要渲染的数据之外。然而,我的当前方法有缺陷。下面是一个简化的示例:
package main
import "fmt"
import "os"
import "html/template"
func main() {
// 直接将nil传递给Execute不会渲染缺失的结构字段
fmt.Print("直接渲染nil\n")
tmpl, err := template.New("master").Parse("Foo is: {{.Data.Foo}}")
if err != nil {
fmt.Printf(err.Error())
return
}
err = tmpl.Execute(os.Stdout, nil)
if err != nil {
fmt.Printf(err.Error())
return
}
// 包装模板可以正常工作,只要我提供数据...
fmt.Print("\n渲染Foo\n")
render(struct {
Foo string
}{
"foo",
})
// ...但这样会出错。
fmt.Print("\n渲染nil\n")
render(nil)
}
func render(data interface{}) {
allData := struct {
Session string
Data interface{}
}{
"sessionData",
data,
}
// 示例中的硬编码模板 - 这可以是任意模板
tmpl, err := template.New("master").Parse("Foo is: {{.Data.Foo}}")
if err != nil {
fmt.Printf(err.Error())
return
}
err = tmpl.Execute(os.Stdout, allData)
if err != nil {
fmt.Printf(err.Error())
return
}
}
输出结果如下:
直接渲染nil
Foo is:
渲染Foo
Foo is: foo
渲染nil
Foo is: template: master:1:15: executing "master" at <.Data.Foo>: nil pointer evaluating interface {}.Foo
所以我不太确定首先发生了什么 - 为什么html/template能够处理传递的nil
,但无法处理nil
指针?
其次,有没有更好的方法来解决这个问题?
英文:
I am attempting to wrap html/template so I am guaranteed to have certain data in my templates (session data for example) in addition to the data I want to render. However, my current approach is...flawed. Here's a simplified example below:
package main
import "fmt"
import "os"
import "html/template"
func main() {
// Passing nil directly to Execute doesn't render anything for missing struct fields
fmt.Print("Directly rendering nil\n")
tmpl, err := template.New("master").Parse("Foo is: {{.Data.Foo}}")
if err != nil {
fmt.Printf(err.Error())
return
}
err = tmpl.Execute(os.Stdout, nil)
if err != nil {
fmt.Printf(err.Error())
return
}
// Wrapping templates works as long as I supply data...
fmt.Print("\nRendering Foo\n")
render(struct {
Foo string
}{
"foo",
})
// ...but this breaks.
fmt.Print("\nRendering nil\n")
render(nil)
}
func render(data interface{}) {
allData := struct {
Session string
Data interface{}
}{
"sessionData",
data,
}
// Hardcoded template for the example - this could be any arbitrary template
tmpl, err := template.New("master").Parse("Foo is: {{.Data.Foo}}")
if err != nil {
fmt.Printf(err.Error())
return
}
err = tmpl.Execute(os.Stdout, allData)
if err != nil {
fmt.Printf(err.Error())
return
}
}
I get the following output:
Directly rendering nil
Foo is:
Rendering Foo
Foo is: foo
Rendering nil
Foo is: template: master:1:15: executing "master" at <.Data.Foo>: nil pointer evaluating interface {}.Foo
So I'm not quite sure what's going on in the first place - why is it that html/template is capable of handling being passed nil
, but can't figure out what to do with a nil pointer?
Secondly, is there a better way of approaching this problem?
答案1
得分: 4
你最好的选择是始终将Data定义为map或struct类型,要么将类型定义为map或struct,要么不要使用interface{}
的nil值:
package main
import "fmt"
import "os"
import "text/template"
func main() {
tmpl, err := template.New("master").Parse("{{if .Data.Foo}}Foo is: {{.Data.Foo}}{{else}}Foo is empty{{end}}")
if err != nil {
fmt.Printf(err.Error())
return
}
err = tmpl.Execute(os.Stdout, nil)
if err != nil {
fmt.Printf(err.Error())
return
}
fmt.Println("")
err = tmpl.Execute(os.Stdout, struct {
Session string
Data map[string]string
}{
"sessionData",
nil,
})
if err != nil {
fmt.Printf(err.Error())
return
}
fmt.Println("")
err = tmpl.Execute(os.Stdout, struct {
Session string
Data interface{}
}{
"sessionData",
map[string]string{},
})
if err != nil {
fmt.Printf(err.Error())
return
}
fmt.Println("")
}
关于为什么会这样工作,这有点复杂,你需要查看代码:https://golang.org/src/text/template/exec.go?s=4647:4717#L521
当使用nil调用execute时,reflect.ValueOf(nil)
返回一个无效的Value,因此evalField返回零值,最终得到一个空字符串。
然而,当使用有效的struct调用execute时,第一个reflect.ValueOf返回一个有效的值。.Data
命令在你传递给Execute的整个struct上调用evalField,并且evalField调用FieldByIndex/FieldByName来获取"Data"字段。这不会返回无效的Value。
接下来,当评估.Foo
时,如果Data是一个接口或指针,indirect函数会一直跟随它,直到找到它为nil,然后会失败并报错。
当Data是一个map时,indirect函数不会执行任何操作,也不会失败。
这可能是text/template包中的一个bug。
英文:
Your best bet is to always make Data a map or struct, either by making the type a map or struct, or by not using a nil with interface{}
:
package main
import "fmt"
import "os"
import "text/template"
func main() {
tmpl, err := template.New("master").Parse("{{if .Data.Foo}}Foo is: {{.Data.Foo}}{{else}}Foo is empty{{end}}")
if err != nil {
fmt.Printf(err.Error())
return
}
err = tmpl.Execute(os.Stdout, nil)
if err != nil {
fmt.Printf(err.Error())
return
}
fmt.Println("")
err = tmpl.Execute(os.Stdout, struct {
Session string
Data map[string]string
}{
"sessionData",
nil,
})
if err != nil {
fmt.Printf(err.Error())
return
}
fmt.Println("")
err = tmpl.Execute(os.Stdout, struct {
Session string
Data interface{}
}{
"sessionData",
map[string]string{},
})
if err != nil {
fmt.Printf(err.Error())
return
}
fmt.Println("")
}
Play: http://play.golang.org/p/9GkAp6ysvD
As for why it works like this, it's a bit complicated, you have to look at the code: https://golang.org/src/text/template/exec.go?s=4647:4717#L521
When execute is called with nil, reflect.ValueOf(nil)
returns an invalid Value, so evalField returns the zero value, and you end up with an empty string.
However, when execute is called with a valid struct, that first reflect.ValueOf returns a valid value. The .Data
command calls evalField on the whole struct you passed to Execute, and evalField calls FieldByIndex/FieldByName to get the "Data" field. This doesn't return an invalid Value.
Next, when .Foo
is evaluated, if Data is an interface or a pointer, the indirect function follows it to the end, and if it finds that it's nil, it fails with this error.
When Data is a map, the indirect function doesn't do anything, and it doesn't fail.
This might be a bug in the text/template package.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论