Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

text/template: Inconsistent handling off nil values in if and with #30481

Closed
bep opened this issue Feb 28, 2019 · 6 comments
Closed

text/template: Inconsistent handling off nil values in if and with #30481

bep opened this issue Feb 28, 2019 · 6 comments

Comments

@bep
Copy link
Contributor

@bep bep commented Feb 28, 2019

package main

import (
	"bytes"
	"fmt"
	"log"
	"text/template"
)

type Niller interface {
	Nil() Niller
}

type Nill struct {
	nilv Niller
}

func (n Nill) Nil() Niller {
	return n.nilv
}

func main() {

	tmpl, err := template.New("").Parse(`{{ .Nil }}: {{ with .Nil }}Failed, got {{ . }}{{ else }}OK{{ end }}`)
	if err != nil {
		log.Fatal(err)
	}

	var (
		nil1 Niller = (*Nill)(nil)
		nil2 Niller
		nil3 *Nill
		nil4 Niller = nil
	)

	for i, niller := range []Niller{nil1, nil2, nil3, nil4} {
		var buff bytes.Buffer
		err = tmpl.Execute(&buff, &Nill{nilv: niller})
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(i+1, buff.String())
	}
}

The above prints:

1 <nil>: Failed, got <nil>
2 <nil>: OK
3 <nil>: Failed, got <nil>
4 <nil>: OK

This issue makes more sense if you also glance at these:

  • #30143 panic on nil interface value; it was when implementing workarounds for that issue that I got other curious nullpointers which I had hard time understanding. Unfortunately I found no workarounds that both avoid panics and work sensible with if and where.
  • #28391 about the subjective truth in template.IsTrue

The documentation for both if and with is similar:

"If the value of the pipeline is empty, no output is generated"

I'm not sure what the definition of "empty" in the above, but as all of the above cases outputs <nil>, I assume it is the truthines defined in template.IsTrue that is used.

@bcmills
Copy link
Member

@bcmills bcmills commented Feb 28, 2019

CC @robpike @mvdan for text/template

@bcmills
Copy link
Member

@bcmills bcmills commented Feb 28, 2019

Note that nil2 and nil4 in your example are identical values, and nil1 and nil3 are identical when stored in a []Niller.

@bcmills
Copy link
Member

@bcmills bcmills commented Feb 28, 2019

“empty” in the context of “If the value of the pipeline is empty” means “equal to the zero value of its type”. At the core, this is another instance of https://golang.org/doc/faq#nil_error, and text/template is behaving as documented.

@bcmills bcmills closed this Feb 28, 2019
@bep
Copy link
Contributor Author

@bep bep commented Feb 28, 2019

@bcmills I can sympathize with your urge to be effective and close this issue. But this is a real issue that need some kind of action, even if just a documentation update.

In general, I think every person who uses the Go template API expect this to not panic with a nilpointer, if at all possible:

{{ with .MaybeNil }}{{ .Hello }}{{ end }}

Even after carefully reading the documentation, I think most people would expect the above to not crash. And note that this is less obvious and much harder to debug in a dynamic template context.

All of my 4 examples (even if there are duplicates) would be considered empty by most people.

There is nothing here that talks about "empty" (or even zero values), so that is, I guess, a vague sentence that could be interprited to mean just about anything.

/cc @mvdan @ianlancetaylor

@bep
Copy link
Contributor Author

@bep bep commented Feb 28, 2019

I'm sure I'm talking to myself here, but if you expand the program above:

package main

import (
	"bytes"
	"fmt"
	"log"
	"text/template"
)

type Niller interface {
	Nil() Niller
	Foo() string
}

type Nill struct {
	nilv Niller
}

func (n Nill) Nil() Niller {
	return n.nilv
}

func (n Nill) Foo() string {
	return "asdf"
}

func main() {

	tmpl, err := template.New("").Parse(`{{ with .Nil }}Failed, got {{ . }}{{ else }}OK{{ end }}`)
	if err != nil {
		log.Fatal(err)
	}

	var (
		nil1 Niller = (*Nill)(nil)
		nil2 Niller
		nil3 *Nill
		nil4 Niller = nil
	)

	for i, niller := range []Niller{nil1, nil2, nil3, nil4} {

		var buff bytes.Buffer
		n := &Nill{nilv: niller}
		a, b := template.IsTrue(n.Nil())
		err = tmpl.Execute(&buff, n)
		if err != nil {
			fmt.Println("error:", err)
			continue
		}
		fmt.Println(i+1, a, b, buff.String())
	}
}

You get:

1 false true Failed, got <nil>
2 false true OK
3 false true Failed, got <nil>
4 false true OK

I have looked at the code, and I fail to understand it. template.IsTrue, which is used by with and if, all returns correctly false for all 4 cases, yet with (and if) behaves as if it was truthy.

@mvdan
Copy link
Member

@mvdan mvdan commented Mar 1, 2019

I'm sure I'm talking to myself here

I did follow this thread yesterday, but I had work and a meetup to speak at, so I didn't have a chance to respond properly :)

I see you've opened a new issue, so I'll just focus on that one.

@golang golang locked and limited conversation to collaborators Feb 29, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
4 participants
You can’t perform that action at this time.