Search code examples
templatesstructgointerfaceembedding

Template object field enforcement


Go provides great build in HTML templating functionality, however for me it is important that I can ensure certain fields will always be available to templates in my application. A good example of this is a title field, which needs to be shown on every HTML page.

Given the following pages:
Home
Register
Contact

I would likely create the following objects for the templating system:
HomePage struct
RegisterPage struct
ContactPage struct

Is there a recommended way to ensure that the structs for each page have certain fields available to them?

Ideally I would implement this through Polymorphism, but that isn't officially supported in Go. The alternative, embedding doesn't appear to have any enforcement, i.e all of the child structs can embed a parent struct but don't have to.

Let me know if I haven't expressed my question clearly enough.


Solution

  • Executing a template does not enforce anything to the parameters, Template.Execute() accepts a value of type interface{}.

    You are the one creating the HomePage, RegisterPage and ContactPage structs. What stops you from embedding a BasePage struct with the required fields? Are you worried you will forget about it? You will notice it at the first testing, I wouldn't worry about that:

    type BasePage struct {
        Title string
        Other string
        // other required fields...
    }
    
    type HomePage struct {
        BasePage
        // other home page fields...
    }
    
    type RegisterPage struct {
        BasePage
        // other register page fields...
    }
    

    If you want from code to check if page structs embed the BasePage, I recommend another way: interfaces.

    type HasBasePage interface {
        GetBasePage() BasePage
    }
    

    Example HomePage that implements it:

    type HomePage struct {
        BasePage
        // other home page fields...
    }
    
    func (h *HomePage) GetBasePage() BasePage {
        return h.BasePage
    }
    

    Now obviously only pages that have a GetBasePage() method can be passed as a value of HasBasePage:

    var page HasBasePage = &HomePage{} // Valid, HomePage implements HasBasePage
    

    If you don't want to use interfaces, you can use the reflect package to check if a value is a struct value and if it embeds another interface. Embedded structs appear and can be accessed like ordinary fields e.g. with Value.FieldByName(), with the type name being the field name.

    Example code using reflect to check if a value embeds BasePage:

    page := &HomePage{BasePage: BasePage{Title: "Home page"}}
    
    v := reflect.ValueOf(page)
    if v.Kind() == reflect.Ptr {
        v = v.Elem()
    }
    if v.Kind() != reflect.Struct {
        fmt.Println("Error: not struct!")
        return
    }
    bptype := reflect.TypeOf(BasePage{})
    bp := v.FieldByName(bptype.Name()) // "BasePage"
    if !bp.IsValid() || bp.Type() != bptype {
        fmt.Println("Error: struct does not embed BasePage!")
        return
    }
    
    fmt.Printf("%+v", bp)
    

    Output (try it on the Go Playground):

    {Title:Home page Other:}