質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.49%

なぜServeHTTPを実装しているだけで呼ばれるのかがわからない

受付中

回答 1

投稿

  • 評価
  • クリップ 1
  • VIEW 1,637

score 78

お世話になります。低レベルな質問かもしれませんが、解決したいので質問させてください。
こちらのコードでは、 main()の中でrootHandler(loginHandler)とすることで内部でServeHTTPが呼ばれているようです。
しかし、なぜ勝手に呼ばれるのでしょうか?どこがどうなって呼ばれる仕組みになっているのか教えていただきたいです。
よろしくお願いいたします。

// Use as a wrapper around the handler functions.
type rootHandler func(http.ResponseWriter, *http.Request) error

func loginHandler(w http.ResponseWriter, r *http.Request) error {
    if r.Method != http.MethodPost {
        return NewHTTPError(nil, 405, "Method not allowed.")
    }

    body, err := ioutil.ReadAll(r.Body) // Read request body.
    if err != nil {
        return fmt.Errorf("Request body read error : %v", err)
    }

    // Parse body as json.
    var schema loginSchema
    if err = json.Unmarshal(body, &schema); err != nil {
        return NewHTTPError(err, 400, "Bad request : invalid JSON.")
    }

    ok, err := loginUser(schema.Username, schema.Password)
    if err != nil {
        return fmt.Errorf("loginUser DB error : %v", err)
    }

    if !ok { // Authentication failed.
        return NewHTTPError(nil, 401, "Wrong password or username.")
    }
    w.WriteHeader(200) // Successfully authenticated. Return access token?
    return nil
}

// rootHandler implements http.Handler interface.
func (fn rootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    err := fn(w, r) // Call handler function
    if err == nil {
        return
    }
    // This is where our error handling logic starts.
    log.Printf("An error accured: %v", err) // Log the error.

    clientError, ok := err.(ClientError) // Check if it is a ClientError.
    if !ok {
        // If the error is not ClientError, assume that it is ServerError.
        w.WriteHeader(500) // return 500 Internal Server Error.
        return
    }

    body, err := clientError.ResponseBody() // Try to get response body of ClientError.
    if err != nil {
        log.Printf("An error accured: %v", err)
        w.WriteHeader(500)
        return
    }
    status, headers := clientError.ResponseHeaders() // Get http status code and headers.
    for k, v := range headers {
        w.Header().Set(k, v)
    }
    w.WriteHeader(status)
    w.Write(body)
}

func main() {
    // Notice rootHandler.
    http.Handle("/login/", rootHandler(loginHandler))
    log.Fatal(http.ListenAndServe(":8080", nil))
}
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

+1

Goではあらゆる非インターフェース型の定義に対しメソッドを追加することができます。

rootHandlerと名付けた「func(http.ResponseWriter, *http.Request) error」型(関数型)に
ServeHTTPというメソッドを追加しているのが上記のコードです。

http.Handleの第二引数は「http.Handler」というインターフェース型です。
その定義内容を見ると以下のように書かれています。

type Handler interface{
    ServeHTTP(w ResponseWriter, r *Request)
}

つまり、rootHandlerは「ServeHTTP(w ResponseWriter, r *Request)」メソッドを持つので、
http.Handlerインターフェースと互換があります。
なので「rootHandler(loginHandler)」はhttp.Handleの第二引数に引き渡すことができます。

ざっくり説明すると、Goのhttpサーバー実装はHTTPリクエストのURLにしたがってhttp.Handlerインターフェースと互換のあるインスタンスのServeHTTPメソッドを呼ぶ仕掛けになっています。

http.ListenAndServeの実装は以下のようになっていて、ここから順次掘り下げていけば、
ServeHTTPメソッドを呼ぶ箇所にたどり着けるとは思います。(だいぶ読み進める必要はありますが)

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

また、ListenAndServeの第二引数にnilを渡した場合、http.DefaultServeMuxが代わりに使われます。
http.Handleを呼ぶとhttp.DefaultServeMuxにパスプレフィックスとハンドラインスタンスをセットで記録しています。
http.DefaultServeMuxもまたhttp.Handlerを満たすインスタンスでリクエストのたびにServeHTTPメソッドが呼ばれます。

これらが連携してハンドラのServeHTTPが呼ばれる仕掛けになっています。

いろいろ省略している記述を省略せずに書くと以下のようになります
https://play.golang.org/p/ZgQau4jhyc3

package main

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

type loginFunc func(w http.ResponseWriter, r *http.Request) error

func login(w http.ResponseWriter, r *http.Request) error {
    return nil
}

func (f loginFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := f(w, r); err != nil {
        http.Error(w, "login failed", http.StatusForbidden)
        return
    }
    fmt.Fprintf(w, "hello")
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/", loginFunc(login))
    server := &http.Server{Addr: ":8080", Handler: mux}
    if err := server.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.49%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る