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

筋トレが仕事です

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

f:id:rdwbocungelt5:20200720165613p:plain

どうもてぃです。

前回の記事がこちら。

smot93516.hatenablog.jp

今回はhandlerを作成して、html要素を返してみようと思います。

環境

前回の振り返り

前回と少し違いますが、http.HandlerFuncを用いた方法です。

package main

import (
    "fmt"
    "net/http"
)

func ServeHTTP(rw http.ResponseWriter, req *http.Request) {
    switch req.URL.Path {
    case "/dog":
        fmt.Fprintf(rw, "dog dog dog")
    case "/cat":
        fmt.Fprintf(rw, "cat cat cat")
    default:
        fmt.Fprintf(rw, "hello world")
    }
}

func main() {
    http.ListenAndServe(":8080", http.HandlerFunc(ServeHTTP))
}

http.ListenAndServeの第二引数にHandlerFuncを渡し、リクエストパスに応じて表示するものを変更していました。

http.HandleFuncを使う

今回はhttp.HandleFuncを使います。前回はhttp.HandlerFuncです。見間違えないように気をつけてね。

まず/helloにHandleFuncを設定してみます。

package main

import (
    "net/http"
)

// 使わないので一旦コメントアウト
// func ServeHTTP(rw http.ResponseWriter, req *http.Request) {
//     switch req.URL.Path {
//     case "/dog":
//         fmt.Fprintf(rw, "dog dog dog")
//     case "/cat":
//         fmt.Fprintf(rw, "cat cat cat")
//     default:
//         fmt.Fprintf(rw, "hello world")
//     }
// }

func helloHandler(rw http.ResponseWriter, req *http.Request) {
}

func main() {
    http.HandleFunc("/hello", helloHandler)
    http.ListenAndServe(":8080", nil)
}

http.HandleFuncのgodocを見てみるとわかりますが、第一引数はパス、第二引数はhandlerをわたします。

一応参考に引っ張ってくると

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

http.ResponseWriter*http.Requestを引数にもった関数のhandlerを渡せば良いので、helloHandlerのような形で今後も関数を作成していきます。

この状態で前回設定したairを起動し、localhost:8080/helloアクセスすると何も表示されていないページが出てくると思います。

f:id:rdwbocungelt5:20200901164746p:plain

それ以外のパス(例えばアプリケーションルート)にアクセスすると、404 page not foundが返ってきます。

これは前回の記事にも書いた、http.ListenAndServeの第二引数にnilを渡しておくと、設定してないパスにアクセスした際、内部的にNotFoundの関数が返るようになっているためです。

実際にFprintfで表示する

helloHandlerで実際に文字を表示してみます。

前回の記事をやってればだいたいわかります。

package main

import (
    "fmt"
    "net/http"
)

func helloHandler(rw http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(rw, "<h1>hello golang</h1>")
}

func main() {
    http.HandleFunc("/hello", helloHandler)

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

f:id:rdwbocungelt5:20200901165639p:plain

はい、簡単。

ちなみに、http.HandleFunc("/hello/", helloHandler)としておくと、localhost:8080/hello/asdfasd/qwerqw/asdfasとかにアクセスしても<h1>hello golang</h1>が表示されます。

こんな感じ。

f:id:rdwbocungelt5:20200901165938p:plain

html要素を複数渡す

まずはhelloHandlerに直接書いてみます。

func helloHandler(rw http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(rw, `
      <!DOCTYPE html>
      <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>golang practice</title>
          </head>
          <body>
              <h1>hello golang</h1>
          </body>
      </html>
  `)
}

文字列が複数行に渡る場合は、バッククォートでかけばおkです。

他にもhandlerを追加してみます。

package main

import (
    "fmt"
    "net/http"
)

func helloHandler(rw http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(rw, `
      <!DOCTYPE html>
      <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>golang practice</title>
          </head>
          <body>
              <h1>hello golang</h1>
          </body>
      </html>
  `)
}

func dogHandler(rw http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(rw, `
      <!DOCTYPE html>
      <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>golang practice</title>
          </head>
          <body>
              <h1>dogs</h1>
              <h2>dog dog dog</h2>
          </body>
      </html>
  `)
}

func main() {
    http.HandleFunc("/hello", helloHandler)
    http.HandleFunc("/dog", dogHandler)

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

こんな感じで要素を直接渡すことで、bodyを書き換えたり、titleを書き換えたり出来ます。

html要素を関数化

上記2つのhandlerの場合、bodyしか変更がなく、html要素を毎回書くのはさすがに冗長すぎるので関数化します。

func htmlTemplate(title, body string) string {
    template := fmt.Sprintf(`
      <!DOCTYPE html>
      <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>%s</title>
          </head>
          <body>
              %s
          </body>
      </html>`, title, body)

    return template
}

関数をもうちょい良い感じに使うと

func htmlTemplate(title, body string) (template string) {
    template = fmt.Sprintf(`
      <!DOCTYPE html>
      <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>%s</title>
          </head>
          <body>
              %s
          </body>
      </html>`, title, body)
    return
}

こんな感じにかけます。

作成した関数を適応

実際に適応して整理したものがこちら。

package main

import (
    "fmt"
    "net/http"
)

func helloHandler(rw http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(rw, htmlTemplate("golang practice", "<h1>hello golang</h1>"))
}

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

func htmlTemplate(title, body string) (template string) {
    template = fmt.Sprintf(`
      <!DOCTYPE html>
      <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>%s</title>
          </head>
          <body>
              %s
          </body>
      </html>`, title, body)
    return
}

func main() {
    http.HandleFunc("/hello", helloHandler)
    http.HandleFunc("/dog", dogHandler)

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

templateファイルを読み込む形へ変更

html要素を関数化しましたが、実際はテンプレートを読み込んで値をわたしてhandlerごとに表示するものを変更するほうがいいですよね。

てなわけで、htmlTemplate部分を別ファイルにして読み込むにします。

まずはmain.goと同じディレクトリにtemplate.htmlを作成して、以下の形で準備します。

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

これをgo側で読み込みます。

とりあえず、読み込み部分だけ書くと

var tpl *template.Template

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

func init()func main()の前に動く初期化処理。

読み込んでキャッシュしたtplに対して操作をしていきます。

template.htmlに書いてある.Body.Titleはstructをtemplate.htmlに渡しているので、structを作成する前提で進めます(structについても今度まとめる予定)。

package main

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

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) (tmpl template.HTML) {
    tmpl = 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())
    }
}

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

func main() {
    http.HandleFunc("/hello", helloHandler)
    http.HandleFunc("/dog", dogHandler)

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

とりあえずコードだけ。

ちょっと記事が長くなってしまったので、helloHandlerだけ適応してます。

解説はまた次回その3をお楽しみに。

合わせてjsonの返し方も解説出来たらと思います。

ではでは。

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