Rubyと筋肉とギターとわたし

筋トレが仕事です

【Golang】超絶簡素なwebサーバーを作る その3

f:id:rdwbocungelt5:20200720165613p:plain

どうもてぃ。

前回の記事がこちら。

smot93516.hatenablog.jp

template.htmlを読み込んでstructを渡す形のコードを前回作成しました。

まずはそのコードから見ていきます。

tempalteにstructを渡す

type Article struct {
    Title string
    Body  template.HTML
}

var tpl *template.Template

func init() {
    tpl = template.Must(template.ParseFiles("template.html"))
}

func escapeHTML(html string) (tpl template.HTML) {
    tpl = template.HTML(html)

    return
}

func helloHandler(rw http.ResponseWriter, req *http.Request) {
    article := Article{
        Title: "golang practice",
        Body:  escapeHTML("<h1>hello golang</h1>"),
    }

    if err := tpl.ExecuteTemplate(rw, "template.html", article); err != nil {
        log.Fatalln(err.Error())
    }
}

該当箇所だけ取り上げました。

Articleのstructを定義し、helloHandlerで初期化してます。

func helloHandler(rw http.ResponseWriter, req *http.Request) {
    // ここね
    article := Article{
        Title: "golang practice",
        Body:  escapeHTML("<h1>hello golang</h1>"),
    }、

    if err := tpl.ExecuteTemplate(rw, "template.html", article); err != nil {
        log.Fatalln(err.Error())
    }
}

そして、単にBodyを文字列としてtemplateにわたした際、htmlタグをエスケープしてくれないのでescapeHTML関数を作成し、template.HTMLに変換する処理を入れてます(わざわざ別関数にしなくても良い気がしてきた…)

article := Article{
    Title: "golang practice",
    Body: template.HTML("<h1>hello golang</h1>"),
}

これでいいかも。

だた、この方法はXSSの対象となるやりかたなので、使用する際は十分注意するようにしてください。

エスケープせずうまく出力する方法は以下。

type Article struct {
    Title string
    Body  string
}

var tpl *template.Template

func safeHTMLTemplate(text string) template.HTML {
    return template.HTML(text)
}

func init() {
    funcMap := template.FuncMap{ "safehtml": safeHTMLTemplate }
    tpl = template.Must(template.New("").Funcs(funcMap).ParseFiles("template.html"))
}

func helloHandler(rw http.ResponseWriter, req *http.Request) {
    article := Article{
        Title: "golang practice",
        Body:  "<h1>hello golang</h1>",
    }

    if err := tpl.ExecuteTemplate(rw, "template.html", article); err != nil {
        log.Fatalln(err.Error())
    }
}

Bodyをstringにしてます。 templateのエスケープしない処理部分をinit()に逃がしました。

template側は

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{.Title}}</title>
</head>
<body>
  {{.Body|safehtml}}
</body>
</html>

.Body|safehtmlとすることでhtmlタグのエスケープを気にせず、うまいこと表示を実現してます。

この方法は以下の記事を参考にしました。感謝。

qiita.com

特にセキュリティ意識しなければどちらでも問題ないとは思います。

最初の方がイメージしやすいので、慣れたらtemplate.FuncMap等使ってみると良いかもです。

jsonを返してみる

バックエンドはGo、フロントエンドはReactやVueを使いたい、そんなときGoはjsonを返すのがメインになると思います。

まずは超かんたんにhandlerを作成してみます。

func jsonResponseHandler(rw http.ResponseWriter, req *http.Request) {
    data := map[string]interface{}{
        "message": "hello golang",
        "status":  http.StatusOK,
    }

    // map to json
    bytes, err := json.Marshal(data)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Fprint(rw, string(bytes))
}

func main() {
    // http.Handle("/hello", http.HandlerFunc(helloHandler))
    http.HandleFunc("/hello", helloHandler)
    http.HandleFunc("/dog", dogHandler)
    http.HandleFunc("/json", jsonResponseHandler)

    http.ListenAndServe(":3000", nil)
}

該当箇所をピックアップしてます。requestは特に気にせず、決まったjsonを返す形です。

手順としては…

  • map作成(valueはinterface型が望ましい)
  • mapをjsonに変換
  • 取得したbytesを文字列に変換して表示

です。json.Marshal()が今回の味噌。

func Marshal(v interface{}) ([]byte, error) {
    e := newEncodeState()

    err := e.marshal(v, encOpts{escapeHTML: true})
    if err != nil {
        return nil, err
    }
    buf := append([]byte(nil), e.Bytes()...)

    encodeStatePool.Put(e)

    return buf, nil
}

godocを見てみると、Marshalに渡すものは何でもokと(interface型はtypescriptで言うanyみたいなもん)。

深く見る必要はないので、返り値を確認するとbyteとerrorが返ってきてます。

何が返ってくるかはgodocを確認するか、エディタの拡張機能で定義元を確認するようにしてください。自分はvim-goを使ってます。

json.Marshal()を使うだけでjsonをbyte型に変換したものが得られるので、文字列に変換してやると見事にjsonを返すエンドポイントの完成ですb

structを使ったjsonを返してみる

次はmapではなくstructを使って返してみます。

DBからデータを抽出してstructにキャッシュし、それをjsonに変換してレスポンスを返す…みたいなことを想定してます。

type Product struct {
    Name     string `json:"name"`
    Price    int    `json:"price"`
    Quantity int    `json:"quantity"`
}

func productResponseHandler(rw http.ResponseWriter, req *http.Request) {
    // product := Product{"商品A", 100, 10}
    product := Product{
        Name:     "商品A",
        Price:    100,
        Quantity: 10,
    }

    // struct to json byte
    // bytes, err := json.Marshal(product)でもおk
    bytes, err := json.Marshal(&product)
    if err != nil {
        http.Error(rw, err.Error(), http.StatusInternalServerError)
        return
    }

    rw.Header().Set("Content-Type", "application/json")
    fmt.Fprint(rw, string(bytes))
}

また、追加部分だけ抜き出してます。

こう見ると、ほとんどmapのときと同じです。json.Marshal()をしてやるだけ。structの場合ポインタを渡すことが多いので、&productでわたしてます。

他にheader情報を付与したりhttp.Error()で明示的にエラーが出るようにしてますが、mapのときと同じ構成でも問題なく動きます。

ポイントはstructの定義の際に、json指定をしてあげること。

type Product struct {
    Name     string `json:"name"`
    Price    int    `json:"price"`
    Quantity int    `json:"quantity"`
}

これを指定しないと何が返ってくるかというと

f:id:rdwbocungelt5:20200915173004p:plain

ちゃんとjsonになってくれません。

structでレスポンスを返す場合は必ずjson指定をしましょう。

おわり

全体を通したコードがこちら

package main

import (
    "encoding/json"
    "fmt"
    "html/template"
    "log"
    "net/http"
)

type Article struct {
    Title string
    Body  string
}

type Product struct {
    Name     string `json:"name"`
    Price    int    `json:"price"`
    Quantity int    `json:"quantity"`
}

var tpl *template.Template

func safeHTMLTemplate(text string) template.HTML {
    return template.HTML(text)
}

func init() {
    funcMap := template.FuncMap{
        "safehtml": safeHTMLTemplate,
    }
    tpl = template.Must(template.New("").Funcs(funcMap).ParseFiles("template.html"))
}

func helloHandler(rw http.ResponseWriter, req *http.Request) {
    article := Article{
        Title: "golang practice",
        Body:  "<h1>hello golang</h1>",
    }

    if err := tpl.ExecuteTemplate(rw, "template.html", article); err != nil {
        log.Fatalln(err.Error())
    }
}

func dogHandler(rw http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(rw, "<h1>dogs</h1><h2>dog dog dog</h2>")
}

func jsonResponseHandler(rw http.ResponseWriter, req *http.Request) {
    data := map[string]interface{}{
        "message": "hello golang",
        "status":  http.StatusOK,
    }

    // map to json
    bytes, err := json.Marshal(data)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Fprint(rw, string(bytes))
}

func productResponseHandler(rw http.ResponseWriter, req *http.Request) {
    // product := Product{"商品A", 100, 10}
    product := Product{
        Name:     "商品A",
        Price:    100,
        Quantity: 10,
    }

    // struct to json byte
    bytes, err := json.Marshal(product)
    if err != nil {
        http.Error(rw, err.Error(), http.StatusInternalServerError)
        return
    }

    rw.Header().Set("Content-Type", "application/json")
    fmt.Fprint(rw, string(bytes))
}

func main() {
    // http.Handle("/hello", http.HandlerFunc返す(helloHandler))
    http.HandleFunc("/hello", helloHandler)
    http.HandleFunc("/dog", dogHandler)
    http.HandleFunc("/json", jsonResponseHandler)
    http.HandleFunc("/product", productResponseHandler)

    http.ListenAndServe(":3000", nil)
}

次はもう少しhttp.Requestに関してまとめて、フレームワークを使った場合どうなるか、というのもまとめていければと思います。

ではでは。