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

筋トレが仕事です

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

f:id:rdwbocungelt5:20200720165613p:plain

どうもてぃ。

ある程度キャッチアップを終えたので少しずつアウトプットしていきます。

go初心者なのでお手柔らかにお願いします。

環境

ホットリロード環境を作る

ホットリロードとしてairを使用します。

導入は簡単。GitHubに書いてあるとおり。

github.com

一応丁寧に書いておくと

$ go get -u github.com/cosmtrek/air

で、airを使えるようにしたあと、今回作成するファイル(main.go)と同じディレクトリ内に.air.tomlを以下の形で配置するだけ。

# Config file for [Air](https://github.com/cosmtrek/air) in TOML format

# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"

[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main ."
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # ms

[log]
# Show log time
time = false

[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
# Delete tmp directory on exit
clean_on_exit = true

↑ 手順にあるexampleをそのまま使用すればおkです。

とりあえずairの導入は完了。

localhost:8080にアクセスできるようにする

まずは、net/httpパッケージを用いて8080ポートにアクセスできるようにしてみます。

http.ListenAndServeを使うだけです。

package main

import "net/http"

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

これでおk。

airで立ち上げて、localhost:8080へアクセスすると… f:id:rdwbocungelt5:20200828114604p:plain

f:id:rdwbocungelt5:20200828114624p:plain

なんにも設定してないので、デフォルトで404が返ってきます。

ListenAndServeを確認してみる

初心者の方は飛ばしても大丈夫です。

http.ListenAndServeの定義を詳しく見ればわかるんですが、本当なら第二引数にはhandlerを渡します(厳密には違います)

// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

こんな感じ。

さらにhandlerが使われてるServerを見てみると…

// A Server defines parameters for running an HTTP server.
// The zero value for Server is a valid configuration.
type Server struct {
    // Addr optionally specifies the TCP address for the server to listen on,
    // in the form "host:port". If empty, ":http" (port 80) is used.
    // The service names are defined in RFC 6335 and assigned by IANA.
    // See net.Dial for details of the address format.
    Addr string

    Handler Handler // handler to invoke, http.DefaultServeMux if nil

    // TLSConfig optionally provides a TLS configuration for use
    // by ServeTLS and ListenAndServeTLS. Note that this value is
    // cloned by ServeTLS and ListenAndServeTLS, so it's not
    // possible to modify the configuration with methods like
    // tls.Config.SetSessionTicketKeys. To use
    // SetSessionTicketKeys, use Server.Serve with a TLS Listener
    // instead.
    TLSConfig *tls.Config
.
.
.

http.DefaultServeMux if nilらしいので辿っていくと

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    handler.ServeHTTP(rw, req)
}

handlerがnilだったらDefaultServeMuxが入ってますね。

また、handlerに関して更にたどると

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    mux.mu.RLock()
    defer mux.mu.RUnlock()

    // Host-specific pattern takes precedence over generic ones
    if mux.hosts {
        h, pattern = mux.match(host + path)
    }
    if h == nil {
        h, pattern = mux.match(path)
    }
    if h == nil {
        h, pattern = NotFoundHandler(), ""
    }
    return
}

handlerがnilだったらNotFoundHanlder()が実行されてるっぽい。おそらくmux.matchj(path)のところでパスが存在しない場合もNotFoundが動いてそうな処理が書かれてる。

見てみると…


// NotFound replies to the request with an HTTP 404 not found error.
func NotFound(w ResponseWriter, r *Request) { Error(w, "404 page not found", StatusNotFound) }

// NotFoundHandler returns a simple request handler
// that replies to each request with a ``404 page not found'' reply.
func NotFoundHandler() Handler { return HandlerFunc(NotFound) }

NotFoundが返されてる。

辿っていくと、handlerがnilの際、実際にどういう処理が行われてるか、なんとなくわかるはずです。

ある程度プログラミング経験のある人はgodocを確認しながらやっていくと理解が深まるんじゃないでしょうか。

↓godoc

godoc.org

http.HandlerFuncを使う方法

これが一番簡単。

package main

import (
  "net/http"
  "fmt"
)

type handlerType int

func (t handlerType) ServeHTTP(rw http.ResposeWriter, req *http.Request) {
  fmt.Fprintf(rw, "hello world")
}

func main() {
  var t handlerType
  http.ListenAndServe(":8080", t)
}

main.goとして保存。

localhost:8080にアクセスするとhello worldが表示されると思います。

パスに関しては設定していないので、どのパスにアクセスしても今の状態ではhello worldが表示されてしまいます。

↓こんな感じ

f:id:rdwbocungelt5:20200831160310p:plain

なので、それを改良していきます。

パスに応じて表示するものを変更する

ServeHTTPreq *http.Requestを使用します。

package main

import (
    "fmt"
    "net/http"
)

type handlerType int

func (t handlerType) 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, "default")
    }
}

func main() {
    var t handlerType
    http.ListenAndServe(":8080", t)
}

かなり簡単ですが、req.URL.Pathでパスを取得して、switch表示するものを変更しているだけです。

localhost:8080/doglocalhost:8080/catlocalhost:8080にアクセスして確認してみるとswitchで評価されたパスによって表示されるものが変わるーといった感じです。

終わり

簡単すぎたので、次はhttp.HandleFuncを使って良い感じにやっていければと思います。

今週中にその2書く予定ですのでお楽しみに。