在模板中出现空指针评估…为什么?有更好的策略吗?

huangapple go评论77阅读模式
英文:

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 &quot;fmt&quot;
import &quot;os&quot;
import &quot;html/template&quot;

func main() {
	// Passing nil directly to Execute doesn&#39;t render anything for missing struct fields
	fmt.Print(&quot;Directly rendering nil\n&quot;)
	tmpl, err := template.New(&quot;master&quot;).Parse(&quot;Foo is: {{.Data.Foo}}&quot;)
	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(&quot;\nRendering Foo\n&quot;)
	render(struct {
		Foo string
	}{
		&quot;foo&quot;,
	})

	// ...but this breaks.
	fmt.Print(&quot;\nRendering nil\n&quot;)
	render(nil)
}

func render(data interface{}) {
	allData := struct {
		Session string
		Data    interface{}
	}{
		&quot;sessionData&quot;,
		data,
	}

    // Hardcoded template for the example - this could be any arbitrary template
	tmpl, err := template.New(&quot;master&quot;).Parse(&quot;Foo is: {{.Data.Foo}}&quot;)
	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 &quot;master&quot; at &lt;.Data.Foo&gt;: 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 &quot;fmt&quot;
import &quot;os&quot;
import &quot;text/template&quot;

func main() {
	tmpl, err := template.New(&quot;master&quot;).Parse(&quot;{{if .Data.Foo}}Foo is: {{.Data.Foo}}{{else}}Foo is empty{{end}}&quot;)
	if err != nil {
		fmt.Printf(err.Error())
		return
	}

	err = tmpl.Execute(os.Stdout, nil)
	if err != nil {
		fmt.Printf(err.Error())
		return
	}

	fmt.Println(&quot;&quot;)

	err = tmpl.Execute(os.Stdout, struct {
		Session string
		Data    map[string]string
	}{
		&quot;sessionData&quot;,
		nil,
	})
	if err != nil {
		fmt.Printf(err.Error())
		return
	}

	fmt.Println(&quot;&quot;)

	err = tmpl.Execute(os.Stdout, struct {
		Session string
		Data    interface{}
	}{
		&quot;sessionData&quot;,
		map[string]string{},
	})
	if err != nil {
		fmt.Printf(err.Error())
		return
	}

	fmt.Println(&quot;&quot;)
}

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.

huangapple
  • 本文由 发表于 2016年3月11日 09:43:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/35930770.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定