概要
過去に簡単な Web アプリを作りましたが他にもいろいろな使い方ができます
フレームワークでも net/http がベースになっているものが多く覚えていて損はないので勉強がてらいろいろな使い方を試してみました
参考サイトを元に書いているので基本的には写経ですが写経でも実際に手を動かしながらやると勉強になります
環境
- macOS 10.14.4
- golang 1.11.5
追記: 20210114
ボディを読み込む場合に r.Body.Read ではなく ioutil.ReadAll(buffer) を使ったほうが良いというご指摘があったので修正しました
修正版は以下になります
package main
import (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"time"
)
type MyHandler struct {
}
func (mh MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
if r.Header.Get("Content-Type") == "application/json" {
buffer, err := ioutil.ReadAll(r.Body)
if err != nil && err != io.EOF {
w.WriteHeader(http.StatusConflict)
return
}
defer r.Body.Close()
b := map[string]string{}
err = json.Unmarshal(buffer, &b)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
w.Write(buffer)
return
} else {
w.WriteHeader(http.StatusUnsupportedMediaType)
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
}
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: MyHandler{},
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
}
srv.ListenAndServe()
}
クロージャ関数を使う
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello")
})
http.ListenAndServe(":8080", nil)
}
http.HandleFunc でリクエストを受けるのが基本です
http.HandleFunc は第二引数に関数 (handler func(ResponseWriter, *Request)) を受け取り対象のルーティングにアクセスがあった場合に関数が実行されます
golang ではクロージャが使えるので関数を外部に宣言しないで直接引数に関数を渡すことができます
簡単な処理の場合はこれでいいと思います
http.Handle を使う
http.HandleFunc ではなく http.Handle を使ってルーティングを定義することもできます
package main
import (
"fmt"
"net/http"
)
type MyStruct struct {
name string
}
func (my MyStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, my.name)
}
func main() {
http.Handle("/", MyStruct{"hawksnowlog"})
http.ListenAndServe(":8080", nil)
}
構造体を用意して、その構造体に ServeHTTP(w http.ResponseWriter, r *http.Request) を実装すれば OK です
あとは http.Handle の第二引数に構造体を指定すれば OK です
この場合は構造体のメンバが ServeHTTP 関数内で参照できるのが便利なのとルーティングごとに構造体を分けて実装することができるのでリファクタリングなどにも使えると思います
POST で application/json だけを許可する
ボディに JSON が設定されて POST で受け取るケースはよくあると思います
コードは少し長くなります
package main
import (
"encoding/json"
"io"
"net/http"
"strconv"
)
func postHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
if r.Header.Get("Content-Type") == "application/json" {
length, err := strconv.Atoi(r.Header.Get("Content-Length"))
if err != nil {
w.WriteHeader(http.StatusLengthRequired)
return
}
buffer := make([]byte, length)
_, err = r.Body.Read(buffer)
if err != nil && err != io.EOF {
w.WriteHeader(http.StatusConflict)
return
}
b := map[string]string{}
err = json.Unmarshal(buffer, &b)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
w.Write(buffer)
return
} else {
w.WriteHeader(http.StatusUnsupportedMediaType)
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
}
func main() {
http.HandleFunc("/", postHandler)
http.ListenAndServe(":8080", nil)
}
r.Header.Get("Content-Type") で Content-Type をチェックします
application/json 以外だった場合は http.StatusUnsupportedMediaType を返します
次に r.Header.Get("Content-Length") をチェックします
これはボディに JSON が設定されていれば (大抵の場合) 同時にヘッダにその JSON の長さを設定してくれます
ボディ を取得するには r.Body.Read(buffer) を使います
buffer はバイト配列で Content-Length 分用意されています
今回の場合、ボディは JSON 形式なので json.Unmarshal(buffer, &b) で変換します
とりあえず map[string]string{} に変換していますが型などが決まっている場合は struct を使っても良いと思います
ちゃんと JSON に変換することができれば成功でそのまま指定された JSON を返却しています
curl -XPOST -H 'content-type: application/json' -d '{"key":"value"}' localhost:8080
こんな感じでリクエストする成功します
r.Body.Read でタイムアウトしない
上記のコードの場合、実は以下のようなリクエストを送信すると r.Body.Read の部分でタイムアウトしません
curl -XPOST -H 'content-type: application/json' -H 'content-length: 10' localhost:8080
なので http.Server と http.Handler を使ってタイムアウト (ReadTimeout/WriteTimeout) を設定したほうが良いです
package main
import (
"encoding/json"
"io"
"net/http"
"strconv"
"time"
)
type MyHandler struct {
}
func (mh MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
if r.Header.Get("Content-Type") == "application/json" {
length, err := strconv.Atoi(r.Header.Get("Content-Length"))
if err != nil {
w.WriteHeader(http.StatusLengthRequired)
return
}
buffer := make([]byte, length)
_, err = r.Body.Read(buffer)
if err != nil && err != io.EOF {
w.WriteHeader(http.StatusConflict)
return
}
defer r.Body.Close()
b := map[string]string{}
err = json.Unmarshal(buffer, &b)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
w.Write(buffer)
return
} else {
w.WriteHeader(http.StatusUnsupportedMediaType)
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
}
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: MyHandler{},
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
}
srv.ListenAndServe()
}
テンプレートを使う
例えば HTML なんか配信したい場合にテンプレートが使えます
標準のライブラリで html/template というライブラリがあるのでそれを使ってみます
vim index.tpl
<html>
<head>
<title>template test</title>
</head>
<body>
<ul>
{{ range $key, $val := . }}
<li>{{ $key }} => {{ $val }}</li>
{{ end }}
</ul>
</body>
</html>
go 側から map を渡してその中身を range で取り出して表示する HTML です
package main
import (
"html/template"
"net/http"
)
func templateHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("./index.tpl"))
p := map[string]string{
"key1": "value1",
"key2": "value2",
"key3": "value3",
}
err := t.Execute(w, p)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func main() {
http.HandleFunc("/", templateHandler)
http.ListenAndServe(":8080", nil)
}
まず template.Must で使用するテンプレートを初期化します
template.ParseFiles を使えば複数のテンプレートファイルを読み込むことができます
[]string を使えば複数のテンプレートファイルを指定可能です
テンプレートをコールする場合は t.Execute(w, p) を使います
template.ExecuteTemplate を使えば複数テンプレートを読み込んでいる場合にテンプレートを指定して実行することができます
実行するとわかりますが、出力される HTML のテンプレートが改行されているわかると思います
もし改行したくない場合は trim モードを使うと改行されなくなります
改行させたくない場合はテンプレートの最後にハイフンを入れると改行されなくなります
{{ range $key, $val := . -}}
<li>{{ $key }} => {{ $val -}}</li>
{{ end -}}
また今回は map をテンプレート側に渡しましたが構造体を渡すとメンバを {{ .Name }} のように参照することが可能です
静的ファイルを配信する
mkdir publiccp icon.png public/
// 一部省力
func main() {
http.HandleFunc("/", templateHandler)
http.Handle("/img/", http.StripPrefix("/img/", http.FileServer(http.Dir("./public"))))
http.ListenAndServe(":8080", nil)
}
http.FileServer を使います
public ディレクトリ配下に配置したファイルを /img/ というパスで配信します
なので上記の場合は localhost:8080/img/icon.png で画像ファイルを参照することができます
ロギングを考える
http.ListenAndServe の第二引数を使うのが一番簡単かなと思います
所謂ミドルウェア的な使い方ができます
package main
import (
"html/template"
"log"
"net/http"
)
func templateHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("./index.tpl"))
p := map[string]string{
"key1": "value1",
"key2": "value2",
"key3": "value3",
}
err := t.Execute(w, p)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func logRequest(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL)
handler.ServeHTTP(w, r)
})
}
func main() {
http.HandleFunc("/", templateHandler)
http.Handle("/img/", http.StripPrefix("/img/", http.FileServer(http.Dir("./public"))))
http.ListenAndServe(":8080", logRequest(http.DefaultServeMux))
}
こうすることで HandleFund などで登録したハンドラが実行される前に logRequest がコールされるようになります
logRequest 内で handler.ServeHTTP(w, r) することで次のハンドラが自動的にコールされるイメージです
DefaultServeMux は ServeMux 構造体になります
また r.Body に関しては実際にボディを扱うハンドラ内で参照しましょう
なぜなら r.Body を複数回参照すると 2 回目以降は空になってしまうためです (参考)
最後に
net/http パッケージをいろいろと試してみました
デフォルトのパッケージだけでも結構いろいろできるなと思いました
もちろんサードパーティのフレームワークもたくさんあるのでそれらを使ったほうが ORM があったりロギングがあったりエラーハンドリングが用意されていたりするので便利ではあると思います
http.Handle と http.HandleFunc、あとは http.Server あたりを理解して使いこなせるようになると良いかなと思います
r.Body.Read(buffer)だと最後まで読み込まれない可能性があり、ioutil.ReadAll(buffer)を使用する必要があるようです。
返信削除https://qiita.com/nyamage/items/e07de57d486238567ba7#comment-2fe78bb08ff9187aac6b
ご指摘ありがとうございます、ioutil.ReadAll を使ったバージョンを追記しました
返信削除