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后端技术,关注公众号