GO語言在WEB開發領域中的使用愈來愈普遍,Hired 發佈的《2019 軟件工程師狀態》報告中指出,具備 Go 經驗的候選人是迄今爲止最具吸引力的。平均每位求職者會收到9 份面試邀請。
php
想學習go,最基礎的就要理解go是怎麼作到高併發的。
那麼什麼是高併發?golang
高併發(High Concurrency)是互聯網分佈式系統架構設計中必須考慮的因素之一,它一般是指,經過設計保證系統可以同時並行處理不少請求。web
嚴格意義上說,單核的CPU是無法作到並行的,只有多核的CPU才能作到嚴格意義上的並行,由於一個CPU同時只能作一件事。那爲何是單核的CPU也能作到高併發。這就是操做系統進程線程調度切換執行,感受上是並行處理了。因此只要進程線程足夠多,就能處理C1K C10K的請求,可是進程線程的數量又受到操做系統內存等資源的限制。每一個線程必須分配8M大小的棧內存,無論是否使用。每一個php-fpm須要佔用大約20M的內存。因此目前有線程的Java就比只有進程的PHP的併發處理能力高。固然了,軟件的處理能力不單單跟內存有關,還有是否阻塞,是否異步處理,CPU等等。Nginx做爲單線程的模型卻能夠承擔幾萬甚至幾十萬的併發請求,Nginx的話題提及來也就更多了。
咱們繼續聊咱們的Go,那麼是否是能夠有一種語言使用更小的處理單元,佔用內存比線程更小,那麼它的併發處理能力就能夠更高。因此Google就作了這件事,就有了golang語言,golang從語言層面就支持了高併發。面試
goroutine是Go並行設計的核心。goroutine說到底其實就是協程,可是它比線程更小,幾十個goroutine可能體如今底層就是五六個線程,Go語言內部幫你實現了這些goroutine之間的內存共享。執行goroutine只需極少的棧內存(大概是4~5KB),固然會根據相應的數據伸縮。也正由於如此,可同時運行成千上萬個併發任務。goroutine比thread更易用、更高效、更輕便。後端
一些高併發的處理方案基本都是使用協程,openresty也是利用lua語言的協程作到了高併發的處理能力,PHP的高性能框架Swoole目前也在使用PHP的協程。
協程更輕量,佔用內存更小,這是它能作到高併發的前提。架構
學習go的HTTP代碼。先建立一個簡單的web服務。併發
package main import ( "fmt" "log" "net/http" ) func response(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello world!") //這個寫入到w的是輸出到客戶端的 } func main() { http.HandleFunc("/", response) err := http.ListenAndServe(":9000", nil) if err != nil { log.Fatal("ListenAndServe: ", err) } }
而後編譯框架
go build -o test_web.gobin ./test_web.gobin
而後訪問curl
curl 127.0.0.1:9000 Hello world!
這樣簡單的一個WEB服務就搭建起來。接下來咱們一步一步理解這個Web服務是怎麼運行的,怎麼作到高併發的。
咱們順着http.HandleFunc("/", response)方法順着代碼一直往上看。異步
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) } var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux type ServeMux struct { mu sync.RWMutex//讀寫鎖。併發處理須要的鎖 m map[string]muxEntry//路由規則map。一個規則一個muxEntry hosts bool //規則中是否帶有host信息 } 一個路由規則字符串,對應一個handler處理方法。 type muxEntry struct { h Handler pattern string }
上面是DefaultServeMux的定義和說明。咱們看到ServeMux結構體,裏面有個讀寫鎖,處理併發使用。muxEntry結構體,裏面有handler處理方法和路由字符串。
接下來咱們看下,http.HandleFunc函數,也就是DefaultServeMux.HandleFunc作了什麼事。咱們先看mux.Handle第二個參數HandlerFunc(handler)
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { mux.Handle(pattern, HandlerFunc(handler)) } type Handler interface { ServeHTTP(ResponseWriter, *Request) // 路由實現器 } type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
咱們看到,咱們傳遞的自定義的response方法被強制轉化成了HandlerFunc類型,因此咱們傳遞的response方法就默認實現了ServeHTTP方法的。
咱們接着看mux.Handle第一個參數。
func (mux *ServeMux) Handle(pattern string, handler Handler) { mux.mu.Lock() defer mux.mu.Unlock() if pattern == "" { panic("http: invalid pattern") } if handler == nil { panic("http: nil handler") } if _, exist := mux.m[pattern]; exist { panic("http: multiple registrations for " + pattern) } if mux.m == nil { mux.m = make(map[string]muxEntry) } mux.m[pattern] = muxEntry{h: handler, pattern: pattern} if pattern[0] != '/' { mux.hosts = true } }
將路由字符串和處理的handler函數存儲到ServeMux.m 的map表裏面,map裏面的muxEntry結構體,上面介紹了,一個路由對應一個handler處理方法。
接下來咱們看看,http.ListenAndServe(":9000", nil)作了什麼
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } func (srv *Server) ListenAndServe() error { addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) }
net.Listen("tcp", addr),就是使用端口addr用TCP協議搭建了一個服務。tcpKeepAliveListener就是監控addr這個端口。
接下來就是關鍵代碼,HTTP的處理過程
func (srv *Server) Serve(l net.Listener) error { defer l.Close() if fn := testHookServerServe; fn != nil { fn(srv, l) } var tempDelay time.Duration // how long to sleep on accept failure if err := srv.setupHTTP2_Serve(); err != nil { return err } srv.trackListener(l, true) defer srv.trackListener(l, false) baseCtx := context.Background() // base is always background, per Issue 16220 ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { rw, e := l.Accept() if e != nil { select { case <-srv.getDoneChan(): return ErrServerClosed default: } if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 c := srv.newConn(rw) c.setState(c.rwc, StateNew) // before Serve can return go c.serve(ctx) } }
for裏面l.Accept()接受TCP的鏈接請求,c := srv.newConn(rw)建立一個Conn,Conn裏面保存了該次請求的信息(srv,rw)。啓動goroutine,把請求的參數傳遞給c.serve,讓goroutine去執行。
這個就是GO高併發最關鍵的點。每個請求都是一個單獨的goroutine去執行。
那麼前面設置的路由是在哪裏匹配的?是在c.serverde的c.readRequest(ctx)裏面分析出URI METHOD等,執行serverHandler{c.server}.ServeHTTP(w, w.req)作的。看下代碼
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爲空,就咱們剛開始項目中的ListenAndServe第二個參數。咱們是nil,因此就走DefaultServeMux,咱們知道開始路由咱們就設置的是DefaultServeMux,因此在DefaultServeMux裏面我必定能夠找到請求的路由對應的handler,而後執行ServeHTTP。前邊已經介紹過,咱們的reponse方法爲何具備ServeHTTP的功能。流程大概就是這樣的。
咱們看下流程圖
咱們基本已經學習忘了GO 的HTTP的整個工做原理,瞭解到了它爲何在WEB開發中能夠作到高併發,這些也只是GO的冰山一角,還有Redis MySQL的鏈接池。要熟悉這門語言仍是多寫多看,才能掌握好它。靈活熟練的使用。
------------------------------------end
一塊兒關注高性能WEB後端技術,關注公衆號