Go Web學習(1)——標準庫http實現server

PedroGao發表於2018-01-22

最近放假在家好好學習了一下Go語言,Go作為Google官推的Server語言,因為天生的併發性和完備的標準庫讓Go語言在服務端如魚得水。筆者在簡單的學習了之後,真的是驚訝連連,好了進入正題。

首先,我們必須實現一個Go Web版的Hello World。

package main

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

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "%s\n", "Hello World")
	})
	err := http.ListenAndServe(":8000", nil)
	if err != nil {
		log.Fatal(err)
	}
}
複製程式碼

我們可以看到Go語言實現一個Web HelloWorld的簡潔程度甚至直接媲美Node.js,不需要任何容器便可以實現一個高併發的簡單伺服器。下面我們來分析一下這個程式碼:

首先,我們匯入了fmt,http包,log包其實對於HelloWorld來說並沒有匯入的必要,但是日誌輸出這個良好習慣還是得遵從。在main()函式的第一行,我們通過http.HandleFunc定義了路由為"/"的響應函式,這個響應函式,接受傳來的Request,並對Response做一定的處理即寫入HelloWorld然後直接返回給瀏覽器。然後便可以直接呼叫http.ListenAndServe來監聽本地的8000埠,便可以直接在瀏覽器上看到HelloWorld。

好,上面的流程其實很簡單,有一定Web程式設計的人便都能明白,接下來我們便從Go的原始碼中看一看,這段程式碼究竟是如何實現的。

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}
複製程式碼

上面這段便是Go原始碼中對HandleFunc函式的實現,我們可以看到這個函式直接將所有引數全部傳遞給了DefaultServeMux.HandleFunc來呼叫。

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux
複製程式碼

DefaultServeMux是http包中的全域性變數,它的原型是ServeMux這個結構體,我們再往上翻看這個結構體的HandleFunc方法。

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	mux.Handle(pattern, HandlerFunc(handler))
}
複製程式碼

我們可以看到,似乎沒完沒了,HandleFunc也是直接呼叫這個結構體的另一個方法Handle,另外HandlerFunc(handler)中的HandlerFunc也只是一個type的定義。

type HandlerFunc func(ResponseWriter, *Request)
複製程式碼

這個函式本身並沒有實現什麼,需要我們自己去實現它的內容。也就是我們上面所提到的響應函式。

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler)
複製程式碼

終於我們找到了源頭,當然這個方法的原始碼還比較長,這裡就不貼出全部,Handle這個方法接受兩個引數,pattern這個string型別的參數列示路由,第二個引數handle它其實是Handler介面。

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
複製程式碼

可以看到Handler這個介面中只定義了ServeHTTP這一個方法,換句話說,我們也可以直接實現ServeHTTP這個方法來實現Handler這個介面,然後我們便可以傳給ServeMux來自定義我們的HelloWorld.

package main

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

type CustomHandler struct{}

func (*CustomHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "%s\n", "Hello World")
}

func main() {
	mux := http.NewServeMux()
	mux.Handle("/", &CustomHandler{})
	err := http.ListenAndServe(":8000", mux)
	if err != nil {
		log.Fatal(err)
	}
}
複製程式碼

上面的程式碼可以看到,我們定義了一個CustomHandler,然後實現了ServeHTTP這個方法從而實現了Handler這個介面,在main方法中,我們通過NewServeMux建立了一個自己的mux而不去使用http內的預設ServerMux。然後呼叫ListenAndServe方法,並將自己的mux傳入,程式便會實現自定義的HelloWorld了。 接下來我們來看一下ListenAndServe這個方法:

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

原始碼中可以看到該方法會將傳入進來的addr引數和handler送給Server這個結構體,從而新建一個server然後呼叫這個server的ListenAndServe方法,對於Server這個結構它已經是Go語言對於這個方面非常底層的實現了,它非常強大,而且實現了很多的方法,這裡不過多闡述,主要是實力不夠(笑)。 好,回到正題,既然如此,我們便可以自己建立Server這個例項,來自定義我們的HelloWorld的第二版本。

package main

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

type CustomHandler struct{}

var mux = make(map[string]func(http.ResponseWriter, *http.Request))

func Hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "%s\n", "Hello World")
}

func (*CustomHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if handler, ok := mux[r.URL.String()]; ok {
		handler(w, r)
	}
}

func main() {
	server := http.Server{
		Addr:":8000",
		Handler:&CustomHandler{},
		ReadHeaderTimeout:5 * time.Second,
	}
	mux["/"] = Hello
	err := server.ListenAndServe()
	if err != nil {
		log.Fatal(err)
	}
}
複製程式碼

上面這段程式碼便是自創server的實現了,這裡挑選幾條新的程式碼說明一下,我們定義了一個mux的全域性變數,它來裝配我們的路由與相應函式的對映,相當於上面的mux.Handle("/", .....),這裡比較簡陋的直接用Map來實現,接下來我們定義了Hello這個響應函式,我們也重寫了ServeHTTP這個方法,它會判斷request的url路徑與我們mux裡面的路徑是否匹配,如果匹配在從mux中取出相應的響應函式並將w http.ResponseWriter, r *http.Request這兩個引數傳遞給這個相應函式。

在main函式裡,我們建立了自己的server,通過埠號,Handler及timeout時間來定義它,然後呼叫它的ListenAndServe方法,便可以實現與前面兩個相同的HelloWorld功能。好了,今天寫到這裡,太晚了(笑)。

相關文章