【Golang】自前でマルチプレクサを設定している場合のWebSocketの実装

前提

  • 標準ライブラリのみでの実装です。
  • WebSocketのパッケージはgorilla/websocketではなく、準標準ライブラリのgolang.org/x/net/WebSocketを使用しています。

一般的なWebSocketの実装方法

Goの準標準ライブラリでWebSocketを実装したサーバーを立てる際、http.Handle(/hoge, Handle)を用いて、

// https://pkg.go.dev/golang.org/x/net/websocketでの実装例
func EchoServer(ws *websocket.Conn) {
    io.Copy(ws, ws)
}

func main() {
    http.Handle("/echo", websocket.Handler(EchoServer))
    err := http.ListenAndServe(":12345", nil)
    if err != nil {
        panic("ListenAndServe: " + err.Error())
    }
}

DefaultServeMuxにWebSocket用のハンドラを登録するのが一般的かと思われます。

ここで問題…

しかし、私はいくつもhttp.Handleやらhttp.HandleFuncを書くのがなんとなく好きでなかったため、ListenAndServeで第二引数に'nil'を渡さず、下記のような自家製マルチプレクサを渡していました。
この場合、上記のhttp.Handle("/echo", websocket.Handler(EchoServer))の実装では、websocket.Handlerが自前のマルチプレクサに登録されずDefaultServeMuxに登録されてしまうため、/echoのルーティング時にWebSocketが機能しません。

// WebSocketが機能しない例(http.Handleで登録)
type Mux struct{}

func (mux Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.URL.Path {
    case "/hoge":
     
    case "/hage":
     
    }
    http.NotFound(w, r)
}

func EchoServer(ws *websocket.Conn) {
    io.Copy(ws, ws)
}

func main() {
    http.Handle("/echo", websocket.Handler(EchoServer)) //DefaultServeMuxに登録される
    err := http.ListenAndServe(":12345", mux)
    if err != nil {
        panic("ListenAndServe: " + err.Error())
    }
}

また、下記のようにマルチプレクサ内に処理を記述しても動きませんでした。

// WebSocketが機能しない例(マルチプレクサ内にハンドル記述)
type Mux struct{}

func (mux Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.URL.Path {
    case "/hoge":
     
    case "/echo":
               //error: websocket.Handler(EchoServer) evaluated but not used
        websocket.Handler(EchoServer)  //ただハンドラをコピペするだけではダメ
    }
    http.NotFound(w, r)
}

func EchoServer(ws *websocket.Conn) {
    io.Copy(ws, ws)
}

func main() {
    err := http.ListenAndServe(":12345", mux)
    if err != nil {
        panic("ListenAndServe: " + err.Error())
    }
}

解決策

Type Handlerに実装されているServeHTTPメソッドを付け加えると、ハンドリングがうまくいきWebSocketが機能します。イマイチはっきりした理屈はわからないのですが、http.HandleではハンドラのServeHTTPメソッドを勝手に呼び出してくれるところ、今回のように関数内で使用する場合はしっかりServeHTTP(w, r)を明示しないといけないようです。

// Websocketが起動する例
type Mux struct{}

func (mux Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.URL.Path {
    case "/hoge":
     
    case "/echo":
        websocket.Handler(EchoServer)).ServeHTTP(w, r)
    }
    http.NotFound(w, r)
}

func EchoServer(ws *websocket.Conn) {
    io.Copy(ws, ws)
}

func main() {
    err := http.ListenAndServe(":12345", mux)
    if err != nil {
        panic("ListenAndServe: " + err.Error())
    }
}