簡易復刻出的 Golang HTTP HandleFunc

身為一個 web 狗,用新語言寫個 router 也是應該的,Golang 本身在寫 HTTP 服務就有極大的優勢,官方自帶的 library 就很好用了,以至於到目前為止的統計大部分的人還是直接使用原生的 library 而非使用框架,但是 router 這部份就統計看來已經有了大方向, Mux 是目前大多數人使用的 router 框架,這邊我們玩一下 Golang 原生的 handler 讓它可以和原生的 HandleFunc 有一樣的感覺

第一步:寫一個簡單的 HTTP server

相信大家都不會。。。當然就是要 google

關鍵字 : golang http

第一篇就會看到官方的 library 的連結囉!

開一個新專案

cd $GOPATH/src/
mkdir router
touch main.go

依照官方網站的提示寫出一個簡單的 HTTP 服務

package main

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

func main() {
	http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
	})

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

瀏覽看看

go run main.go
firefox 127.0.0.1:8080/bar

所以

我們可以知道 Golang 本身其實就可以做簡單的 router 讓對應的 URL 可以去執行你的 function,但是如果你想自己搞呢?

資料

Router 最重要的資訊其實就是 Domain 後面的 URI 或稱作 Path,所以我就用這兩個關鍵字直接打在 接收 *http.Request 的 function 裡面,發現 Path 沒有反應,但是 URI 讓 VScode 給了提示,那另一個 Path 就如同官方的教學,可以用 r.URL.Path 取得。

func main() {
	http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
        r.RequestURI
	})

Handler

官方的程式碼中有一段我沒有貼上來,原因就是貼上去會壞掉,那我們要怎麼自己來寫這所謂的 handler 呢?

fooHandler := "" // 至少要宣告吧
http.Handle("/foo", fooHandler)

看看錯誤碼吧

cannot use foohandler (type string) as type http.Handler in argument to http.Handle:
	string does not implement http.Handler (missing ServeHTTP method)

看起來是少了 ServeHTTP 這個方法,所以我們需要讓 fooHandler 有這個 Method 才能跑,但是我們也不知道他要有啥才好,所以回到官方看看找到 ServeHTTP,我們就照著做一個空的 Method 試試看能不能跑。

type myhandler struct {
}

func (handle *myhandler) ServeHTTP(w http.ResponseWriter, r *http.Request){

}

跑一下

go run main.go
firefox 127.0.0.1:8080/foo

可以看到是空的,但是不會壞掉,那就試著在這個 function 裡面加點東西吧

fmt.Fprint(w, "r: "+r.Method+r.URL.Path)

重跑一次看看

go run main.go
firefox 127.0.0.1:8080/foo

可以看到 firefox 裡面有文字了

r: GET/foo

來個對應表吧

對應表

為了要簡易的復刻 HandleFunc 我們就用個簡單的 map 來儲存對應的 URL Path 到對應的 function

type myhandler struct {
	route map[string]func(http.ResponseWriter, *http.Request)
}

以上我們定義了一個 struct 來當作我們的 handler 讓它有一個 route 的屬性,之後我們就可以讓使用者透過它來讓進來的 Request 跑去我們要的 function 裡面

註冊

有了表,我們要讓別人可以填表,所以我們在 myhandler 下面實做一個註冊用的 function

func (handle *myhandler) Register(uri string, f func(http.ResponseWriter, *http.Request)) {
    // 如果還沒初始化,幫它初始化
	if handle.route == nil {
		handle.route = make(map[string]func(http.ResponseWriter, *http.Request))

	}
	handle.route[uri] = f
}

ServeHTTP

最後就是要處理進來的 Request 啦!我們只要確定近來的 URL Path 有在我們的表內,我們就可以去呼叫存在 route 內所對應的 function

func (handle *myhandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Println("r: ", r.Method, r.RequestURI) // 在 terminal 可以看到 log
	if _, ok := handle.route[r.RequestURI]; ok {
		handle.route[r.RequestURI](w, r)
	}
}

main

最後就來玩玩我們寫好的 handler 吧!

func main() {
	handler := new(myhandler)
	handler.Register("/ping", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "pong")
	})
	s := &http.Server{
		Addr:    ":8080",
		Handler: handler,
	}
	log.Fatal(s.ListenAndServe())
}

Try it!

go run main.go
firefox 127.0.0.1:8080/ping

結論

程式就是這樣有了資料就可以很多事情,但是一切一定都不是我們想像的那麼簡單,看看 Mux 上的功能,想想看我們要怎麼做才能完成這麼多功能呢?

如果要實做出 Middle ware 讓別人可以加,你覺得怎麼改比較好呢?

最後得程式碼

package main

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

type myhandler struct {
	route map[string]func(http.ResponseWriter, *http.Request)
}

func (handle *myhandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Println("r: ", r.Method, r.RequestURI)
	if _, ok := handle.route[r.RequestURI]; ok {
		handle.route[r.RequestURI](w, r)
	}
}

func (handle *myhandler) Register(uri string, f func(http.ResponseWriter, *http.Request)) {
	if handle.route == nil {
		handle.route = make(map[string]func(http.ResponseWriter, *http.Request))

	}
	handle.route[uri] = f
}

func main() {
	handler := new(myhandler)
	handler.Register("/ping", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "pong")
	})
	s := &http.Server{
		Addr:    ":8080",
		Handler: handler,
	}
	log.Fatal(s.ListenAndServe())
}