在Go条件模板中使用自定义功能

I'm trying to define a custom Go func to use in a template condition. What I want to achieve is : if the given argument is an IPv4 address the template will output IPv4: [argument] else it will output IPv6: [argument].

To do that I have created a template like this:

{{ range .Addresses }}
{{ if IsIPv4 . }}
IPv4: {{ . }}
{{ else }}
IPv6: {{ . }}
{{ end }}
{{ end }}

As you can see I have create a new function called IsIPv4 which take a string argument and given the argument return true or false. Here is the code:

var (
    errNotAnIPAddress = errors.New("The argument is not an IP address")
)

func isIPv4(address string) (bool, error) {
    ip := net.ParseIP(address)

    if ip == nil {
        return false, errNotAnIPAddress
    }

    return (ip.To4() != nil), nil
}

When executing my template I have got no errors but the execution looks like to stop when trying to evaluate {{ if IsIPv4 . }}.

Of course the function is mapped before trying to parse and execute the template.

funcMap := template.FuncMap{
    "IsIPv4": isIPv4,
}

I'm pretty new to Go and I have probably missed something (maybe obvious?).

To debug a little bit I have tried to remove the IsIPv4 call in my template giving a condition like {{ if . }}. And the result was to always output IPv4: [the IP address]. That makes sense to me.

I also took a look at the predefined Go funcs like eq and it looks like I'm trying to reproduce the same idea, without any success.

TL;DR; You must inspect the error value returned by template.Execute() or template.ExecuteTemplate() which will tell you why it doesn't work for you.


Things that could go wrong

First, your execution doesn't panic because it simply returns an error. Would you inspect that, you'll probably know what's going wrong immediately:

if err := t.Execute(os.Stdout, data); err != nil {
    panic(err)
}

Next what could go wrong: you have to register your custom functions prior to parsing the template because the template engine needs to be able to statically analyze the template, and it needs to know prior that IsIPv4 is a function name:

t := template.Must(template.New("").Funcs(template.FuncMap{
    "IsIPv4": isIPv4,
}).Parse(templ))

Another potential error is that isIPv4() expects a value of type string, and you may pass a value of different type.

And custom functions registered for templates should only return a non-nil error if you intend to stop the execution of the template. Because that's what happens if you return an error. template.FuncMap:

[...] if the second (error) return value evaluates to non-nil during execution, execution terminates and Execute returns that error.

Working example

Here's a working example using your template and your isIPv4() function:

t := template.Must(template.New("").Funcs(template.FuncMap{
    "IsIPv4": isIPv4,
}).Parse(templ))

m := map[string]interface{}{
    "Addresses": []string{"127.0.0.1", "0:0:0:0:0:0:0:1"},
}

if err := t.Execute(os.Stdout, m); err != nil {
    panic(err)
}

Output (try it on the Go Playground):

IPv4: 127.0.0.1

IPv6: 0:0:0:0:0:0:0:1

Potential errors

The above program prints the following errors:

Passing an invalid IP:

    "Addresses": []string{"127.0.0.1.9"},

Output:

panic: template: :2:6: executing "" at <IsIPv4 .>: error calling IsIPv4:
    The argument is not an IP address

Passing a non-string value:

    "Addresses": []interface{}{2},

Output:

panic: template: :2:13: executing "" at <.>: wrong type for value; expected string; got int

Attempting to register your custom function after the template has been parsed:

t := template.Must(template.New("").Parse(templ))
t.Funcs(template.FuncMap{
    "IsIPv4": isIPv4,
})

Output:

panic: template: :2: function "IsIPv4" not defined