Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

많은 언어들이 문서를 다른 형태로 변환하는 기능을 가지고 있다. Go는 컨텐츠에 포함된 특정 문자열을 변환하거나 삽입하는 템플릿 매커니즘을 제공한다. 템플릿은 특히 처리 결과를 HTML 문서로 표시하기 위해서 널리 사용한다.

Contents

소개

많은 컨텐츠들은 고정된 부분과 동적인 부분으로 구성이 된다.

이름, 전화번호, E-mail, 주소 같은 것들은 고정된 영역이다. 이들을 양식에 맞게 배치해서 템플릿을 구성해 놓으면, 이후 동적인 값들(밑줄 부분)을 채워 넣기만 하면, 개인 명함이 완성된다. 만약 명함의 디자인이 변경된다면, 텟플릿만 재 구성해서 다시 찍어내면 된다. 양식과 정보를 분리해서 다양한 표현을 가능하도록 만든게 템플릿이다.

소프트웨어 영역도 마찬가지로, 같은 정보를 다양한 양식으로 표현해야 하는 경우에 "템플릿"개념을 유용하게 사용할 수 있다. 실제 모든 서버사이드 언어들은 "고정된 페이지"안에 유저 정보등과 같은 동적인 데이터를 입력하기 위한 메커니즘을 제공한다. PHP의 경우 <?php ...?>, JSP는 <%=...=?>등으로 문서에 데이터를 삽입할 수 있다. go 언어는 {{...}}를 이용한다.

Inserting object value

Go 객체의 각 필드 이름을 템플릿에 삽입하는 방식으로, 객체를 템플릿에 적용할 수 있다. 템플릿에서는 {{{.FieldName}}형식으로 삽입할 객체의 필드명을 적어주면 된다. 템플릿 패키지는 fmt 패키지를 이용해서 템플릿의 .FieldName을 값으로 변환한다.

예를 들어 아래와 같은 타입을 가지는 객체가 있다고 가정해보자.
type Person struct {
    Name      string
    Age       int
    Emails     []string
    Jobs       []*Jobs
}

템플릿에서 Name과 Age를 사용하고 싶다면
The name is {{.Name}}.
The age is {{.Age}}.
Emails와 Jobs는 배열인데, 이 경우 range를 이용해서 배열을 순환할 수 있다.
{{range .Emails}}
  ...
{{end}}

Job을 아래와 같이 정의했다고 가정해 보자.
type Job struct {
    Employer string
    Role string
}
템플릿에서 Person 의 Jobs의 필드는 {{range .Jobs}}로 접근할 수 있다. 하지만 지금 템플릿에 넘어간 객체는 Person으로 아직 Jobs 객체를 접근할 수 없다. 이럴 때는 {{with ...}} .. {{end}}를 이용해서, 다른 객체의 접근이 가능하다.
{{with .Jobs}}
    {{range .}}
      An employer is {{.Employer}}
      and the role is {{.Role}}
    {{end}}
{{end}}

우리가 템플릿을 만들게 되면, 객체를 템플릿에 밀어 넣는 것으로 객체가 가지는 각 필드의 값을 템플릿에 채워 넣을 수 있다. 이 과정은 1. 템플릿 문서를 파싱하고 2. 파싱한 템플릿 문서에 객체를 밀어 넣는 두 개의 과정으로 이루어진다. 작업 결과는 Writer에 의해서 출력 된다.
t := template.New("Person template")
t, err := t.Parse(templ)
if err == nil {
    buff := bytes.NewBufferString("")
    t.Execute(buff, person)
}

아래는 템플릿에 객체를 적용한 다음 표준출력하는 예제 프로그램이다.
package main

import (
    "fmt"
    "html/template"
    "os"
)

type Person struct {
    Name   string
    Age    int
    Emails []string
    Jobs   []*Job
}

type Job struct {
    Employer string
    Role     string
}

const templ = `The name is {{.Name}}.
The age is {{.Age}}.
{{range .Emails}}
An email is {{.}}
{{end}}

{{with .Jobs}}
{{range .}}
An employer is {{.Employer}}
and the role is {{.Role}}
{{end}}
{{end}}
`

func main() {
    job1 := Job{Employer: "Monash", Role: "Honorary"}
    job2 := Job{Employer: "Box Hill", Role: "Head of HE"}

    person := Person{
        Name:   "jam",
        Age:    50,
        Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
        Jobs:   []*Job{&job1, &job2},
    }

    t := template.New("Person template")
    t, err := t.Parse(templ)
    checkError(err)

    err = t.Execute(os.Stdout, person)
    checkError(err)
}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error", err.Error())
        os.Exit(1)
    }
}
실행 결과
The name is jan.
The age is 50.

        An email is jan@newmarch.name

        An email is jan.newmarch@gmail.com



    
        An employer is Monash
        and the role is Honorary
    
        An employer is Box Hill
        and the role is Head of HE
빈 줄이 너무 많아서 결과가 썩 좋아보이지 않는데, 템플릿의 각 줄이 빈 줄로 변환되기 때문이다. 원하는대로 출력을 하고 싶다면, 템플릿안에 있는 개행문자를 고려해서 템플릿을 개발해야 한다.
{{range .Emails}}   An email is {{.}}
{{end}}

이번 예제에서는 string을 이용해서 템플릿을 만들었는데, template.ParseFiles() 메서드를 이용해서 파일로 부터 템플릿을 읽어드리는 방법도 있다.

Pipelines

go의 template 패키지의 위치가 "html/template"인 것에 주목하자. 우리는 템플릿 이름에서, 이 템플릿의 용도가 HTML문서를 만들기 위한게 목적임을 알 수 있다. 이런 목적에 따라서 템플릿은 텍스트 문자를 HTML 형식에 맞게 변환해서 출력을 한다. 예를 들어 <는 "<"로 &는 "&"로 변환을 한다. Job 객체의 필드 값을 아래와 같이 변경한 다음 테스트를 해보자.
        Emails: []string{"<frank jan> jan@newmarch.name", "<frank jan> jan.newmarch@gmail.com"},
아래와 같은 결과를 확인할 수 있을 거다.
        An email is &lt;frank jan&gt; jan@newmarch.name
        An email is &lt;frank jan&gt; jan.newmarch@gmail.com

Go의 템플릿은 문자값을 다른 함수로 보낼 수 있다. 위 코드를 예로 들어 설명하자면, 실제는 <Monash>값을 html 함수로 보내서, html 특수문자들을 변환 한 다음에 최종 출력한다. 이 함수는 유닉스의 파이프라인과 비슷한 방식(표준출력 값을 파이프를 이용해서 다른 프로그램의 표준 입력으로 보내는)으로 작동한다. 사용 방법은 "| 함수명"이다.

Go의 템플릿은 파이프라인 함수로 사용할 수 있는 몇 개의 함수를 제공하는데, html함수도 그 중 하나다. 만약 html 함수를 템플릿에 적용하길 원한다면 {{. | html}} 하면 된다.

Go의 "text/template" 패키지를 이용해서 테스트 해보자.
package main

import (
    "fmt"
    "os"
    "text/template"
)

type Person struct {
    Name   string
    Age    int
    Emails []string
    Jobs   []*Job
}

type Job struct {
    Employer string
    Role     string
}

const templ = `The name is {{.Name}}.
The age is {{.Age}}.
{{range .Emails}}   An email is {{.}}
{{end}}

{{with .Jobs}}
    {{range .}}
    An employer is {{.Employer|html}}
    and the role is {{.Role}}
{{end}}
{{end}}
`

func main() {
    job1 := Job{Employer: "Monash", Role: "Honorary and test"}
    job2 := Job{Employer: "Box Hill", Role: "Head of HE"}

    person := Person{
        Name:   "jam",
        Age:    50,
        Emails: []string{"<frank jan> jan@newmarch.name", "<frank jan> jan.newmarch@gmail.com"},
        Jobs:   []*Job{&job1, &job2},
    }

    t := template.New("Person template")
    t, err := t.Parse(templ)
    checkError(err)

    err = t.Execute(os.Stdout, person)
    checkError(err)
}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error", err.Error())
        os.Exit(1)
    }
}

실행 결과
The name is jam.
The age is 50.
        An email is <frank jan> jan@newmarch.name 
        An email is <frank jan> jan.newmarch@gmail.com


        An employer is Monash
        and the role is Honorary and test

        An employer is Box Hill
        and the role is Head of HE

Person.Emails에 대해서 "html" 함수를 사용하도록 템플릿을 수정했다.
const templ = `The name is {{.Name}}.
The age is {{.Age}}.
{{range .Emails}}   An email is {{.|html}}
{{end}}
....

실행 결과
The name is jam.
The age is 50.
        An email is &lt;frank jan&gt; jan@newmarch.name
        An email is &lt;frank jan&gt; jan.newmarch@gmail.com
"<", ">"이 HTML 문자 처리된 걸 확인할 수 있다.

Defining functions

템플릿은 fmt 패키지를 이용해서 객체로부터 삽입되는 값을 문자로 표현한다. 때때로 fmt 대신 다른 방식으로 값을 표현하고 싶을 때가 있다. 예를들어 email 주소가 스패머의 공격대상이 되는 걸 막기 위해서 "@"를 "at"으로 변경해야 하는 경우다. 이 경우 "@"를 "at"으로 변경하는 사용자 정의 함수를 만들어서 fmt 대신에 사용할 수 있다.

template의 FuncMap메서드를 이용해서, 함수를 등록할 수 있다. 등록한 함수는 위에서 다룬 파이프라인을 이용해서 사용하면 된다.
type FuncMap map[string]interface{}

Email 주소의 "@"를 "at"으로 치환하기 위한 함수 EmailExpander를 만들었다면, 아래와 같이 등록할 수 있다.
t = t.Funcs(template.FuncMap{"emailExpand": EmailExpander})

EmailExpander 함수는 아래와 같이 만들면 된다.
func EmailExpander(args ...interface{}) string

우리가 만들 함수는 단지 하나의 string 형의 매개변수만을 가질 것이다. 따라서 "func EmailExpander(s string) string" 과 같이 간단하게 만들 수 있을 것이다. Go 템플릿 라이브러리에 있는 코드들이 잘못된 입력등을 처리하기 위한 코드를 가지고 있어서, 그와 비슷하게 만들려다 보니 복잡해 보이는 코드를 만들게 됐다.
package main

import (
    "fmt"
    "os"
    "strings"
    "text/template"
)
    
type Person struct {
    Name   string
    Age    int
    Emails []string
    Jobs   []*Job
}   
    
type Job struct {
    Employer string
    Role     string
}

const templ = `The name is {{.Name}}.
The age is {{.Age}}.
{{range .Emails}}   An email is {{.|emailExpand}}
{{end}}

{{with .Jobs}}
    {{range .}}
    An employer is {{.Employer|html}}
    and the role is {{.Role}}
{{end}}
{{end}}
`

func EmailExpander(args ...interface{}) string {
    ok := false
    var s string
    if len(args) == 1 {
        s, ok = args[0].(string)
    }
    if !ok {
        s = fmt.Sprint(args...)
    }   
    
    // find the @ symbol
    substrs := strings.Split(s, "@")
    if len(substrs) != 2 {
        return s
    }
    // replace the @ by " at "
    return (substrs[0] + " at " + substrs[1])
}       
    
func main() {
    job1 := Job{Employer: "Monash", Role: "Honorary and test"}
    job2 := Job{Employer: "Box Hill", Role: "Head of HE"}

    person := Person{
        Name:   "jam",
        Age:    50,
        Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
        Jobs:   []*Job{&job1, &job2},
    }

    t := template.New("Person template")

    t = t.Funcs(template.FuncMap{"emailExpand": EmailExpander})
    t, err := t.Parse(templ)
    checkError(err)

    err = t.Execute(os.Stdout, person)
    checkError(err)
}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error", err.Error())
        os.Exit(1)
    }
}

실행 결과
The name is jan.
    An email is "jan at newmarch.name"
    An email is "jan.newmarch at gmail.com"

변수

템플릿 안에서 유저가 정의한 변수를 사용 할 수 있다. 지금까지의 예제 프로그램을 약간 수정해서, 템플릿에서의 변수 활용 방안을 찾아보도록 하자. 일단 Person 구조체는 그대로 사용하기로 했다.
type Person struct {
    Name    string
	Emails  []string
}
Emails는 배열이기 때문에 "range"로 접근을 해야 한다.
{{range .Emails}}
  {{.}}
{{end}}
Emails의 값을 가져오는 건 문제 없는데, 배열이 아닌 Name은 문제가 된다. 이 문제는 Name 필드의 값을 변수에 저장해서 사용하는 것으로 간단히 해결할 수 있다. 변수는 "$변수명"형식으로 사용할 수 있다.
{{$name := .Name}}
{{range .Emails}}
  Name is {{$name}}, emails is {{.}}
{{end}}

완전한 예제프로그램
/**
 * PrintNameEmails
 */

package main

import (
    "fmt"
    "html/template"
    "os"
)

type Person struct {
    Name   string
    Emails []string
}

const templ = `{{$name := .Name}}
{{range .Emails}}
Name is {{$name}}, email is {{.}}
{{end}}
`

func main() {
    person := Person{
        Name:   "jan",
        Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
    }

    t := template.New("Person template")
    t, err := t.Parse(templ)
    checkError(err)

    err = t.Execute(os.Stdout, person)
    checkError(err)
}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}

실행 결과
Name is jan, email is jan@newmarch.name

Name is jan, email is jan.newmarch@gmail.com

Conditional statements

다시 Person 예제를 보도록 하자. 지금까지는 range를 이용해서 email의 목록을 출력했는데, 하나의 줄에 출력하고 싶어졌다. 그래서 아래와 같이 코드를 수정했다.
Name is {{.Name}}
Emails are {{.Emails}}
출력 결과는 아래와 같다.
Emails are [jan@newmarch.name jan.newmarch@gmail.com]
이렇게 출력되는 이유는 "fmt" 패키지를 이용하기 때문이다.

이런식의 출력은 애플리케이션 개발에 문제가 될 수 있다. 예를 들어 값을 JSON 형태로 출력해야 한다고 가정해보자. 이 경우 우리가 원하는 결과는 아래 같아야 한다.
{
    "Name":"jan"
    "Emails": ["jan@newmarch.name", "jan.newmarch@gmail.com"]
}

Person의 값을 JSON으로 출력하려고 아래와 같이 템플릿을 만들었다고 가정해 보자.
{
    "Name": "{{.Name}}"
    "Emails": "{{.Emails}}"
}
출력 결과는 아래와 같아서
{
    "Name": "jan"
    "Emails": "[jan@newmarch.name jan.newmarch@gmail.com]"
}
아래와 같은 이유로 제대로 처리할 수 없게 됐다.
  1. 배열을 "로 감싸는 것은 JSON 문법에 어긋난다. 게다가.
  2. 배열의 값들은 "로 깜싸야 한다.
  3. 배열의 값과 값은 콤마(,)로 구분해야 한다.
Range로 이 문제를 해결할 수 있을지 확인해 보자.
{
    "Name": {{.Name}},
    "Emails": [ {{range .Emails}} "{{.}}", {{end}} ]
}

실행 결과는 다음과 같다.
{
    "Name": jan,
    "Emails": [  "jan@newmarch.name",  "jan.newmarch@gmail.com",  ]
}
거의 해결된 것 같은데, 쓸데없는 컴마(,)가 문제다. JSON 표준은 값이 없는 컴마를 허용하지 않는다.

이 문제는 마지막의 ","를 제거하는 것으로 해결할 수 있는데, 비교적 쉽게 코드를 만들 수 있다. 아래는 마지막의 컴마를 제거하는 일반적인 코드다.
package main

import "fmt"
import "strconv"

func main() {
    s := "["
    for i := 0; i < 5; i++ {
        if i != 0 {
            s = s + ","
        }
        s = s + strconv.Itoa(i)
    }
    s = s + "]"
    fmt.Println(s)
}
인덱스 값을 검사해서 컴마를 없제거하면 된다. 조건문이 필요하다는 이야기다.

Go 템플릿은 템플릿안에서 조건문(Conditional statements)의 사용을 허용한다. 위의 코드를 템플릿안에 구현하면 된다.
/**
 * PrintNameEmails
 */

package main

import (
    "fmt"
    "html/template"
    "os"
)

type Person struct {
    Name   string
    Emails []string
}

const templ = `
{"Name": "{{.Name}}",
"Emails": [
{{range $index, $elmt := .Emails}}
    {{if $index}}
        , "{{$elmt}}"
    {{else}}
        "{{$elmt}}"
    {{end}}
{{end}}
]
}
`

func main() {
    person := Person{
        Name:   "jan",
        Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
    }

    t := template.New("Person template")
    t, err := t.Parse(templ)
    checkError(err)

    err = t.Execute(os.Stdout, person)
    checkError(err)
}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}
이제 제대로된 JSON문을 출력하는 걸 확인할 수 있을 거다.