Golang 标准库探秘(二):快速搭建 HTTP 服务器

阅读数:24257 2016 年 3 月 6 日

话题:语言 & 开发架构

服务器阐述:

现在市面上有很多高并发服务器,Nginx 就是一个领军人物,也是我们仰望的存在;Nginx+Lua 这种组合也是高并发服务器的一个代表;PHP 语言作为 Nginx+FastCGI 上一个优秀的解释语言也占据大半江山。而如今的 Golang 也作为高并发服务器语言横空出世,因其“语法简单、代码结构简明,维护成本还低,天生高并发”等特性而被广泛应用,尤其是各种云服务,还有 Docker 也是用 Golang 来做实现语言。

接着我们介绍下服务器编程模型,只从线程的角度,不谈并发模型。

从线程的角度,可以分为“单线程”,“多线程”2 种。

单线程:

整个进程只有一个线程,因为只有一个线程的缘故,当请求来的时候只能一个个按照顺序处理,要想实现高性能只能用“non-blocking IO + IO multiplexing”组合 (非阻塞 io + io 复用)。        Nginx 采用的就是多进程 + 单线程 ( 非阻塞 io+io 复用) 模式。

多线程:

进程有多个线程,多个线程就不好控制,还带来一些问题:锁竞争,数据污染、山下文切换带来的开销,但是可以充分利用 CPU。要实现高性能也是“non-blocking IO + IO multiplexing”组合。

所以,其实不管单线程还是多线程都是要用“non-blocking IO + IO multiplexing”组合的。还有一种用户级线程,整个线程的库都是自己维护,“创建,撤销,切换”,内核是不知道用户级线程存在的,缺点是阻塞时会阻塞整个进程。

其实想实现高并发服务器最好用单线程 (不处理逻辑的情况下),节省很多上下文切换开销 (CPU 分配时间片给任务,CPU 加载上下文),但一定要采用 io 上“非阻塞和异步”。因为多线程很难控制,锁,数据依赖,不同场景会让多线程变成串行,控制起来相当繁琐,牺牲很多并发性能 (Golang 采用的抢占式调度),但正常情况下多线程还是挺不错的。下面我们说下 Golang 实现的高并发。

在 Golang 的调度器里用的也是“csp”并发模型,有 3 个重要的概念 P、M、G。

P 是 Processor,G 是 Goroutine,M 是 Machine。

简述:M 是执行 G 的机器线程,跟 P 绑定才可以执行,P 存放 G 的队列。看到这里大家会问到刚刚不是说多线程切换上下文开销很大吗?其实每个 M 都有一个 g0 栈内存,用来执行运行时管理命令。调度时候 M 的 g0 会从每个 G 栈取出栈 (现场),调度之后再保存到 G,这样不同的 M 就可以接着调度了。所有上下文都是自己在切换,省去了内核带来的开销,而且 Golang 会观察,长时间不调度的 G 会被其他 G 抢占 (抢占调度其实就是一个标记)。

采用异步的方式运行 G,这样就实现了并发 (M 可不止一个啊,感兴趣看下Go 并发实战

看到上面估计大家可能稍微了解点 Golang 的优势了吧。不要担心 GC 问题,选择场景问题。

实战

现在我们进入实战部分,手把手教你实现 CGI,FastCGI,HTTP 服务器,主要是用 Golang 的 HTTP 包。TCP 实战就不在这次说了,TCP 其实是块难啃的骨头,简单的几乎话说不清楚,如果是简单写一个“hello world”的例子,让大家似懂非懂的,不如单独开篇讲解一下,从 Tcp 到 Protobuf 再到 RPC,然后写一个稍微复杂点的 tcp 服务器,我们也可以处理下“粘包,丢包”等问题 (Protobuf 解决或者做一个分包算法),如果简单的 demo 可能会导致你丢失兴趣的。

首先了解什么是 CGI?CGI 和 FastCGI 的区别是什么?

CGI:全拼 (Common Gateway Interface) 是能让 web 服务器和 CGI 脚本共同处理客户的请求的协议。Web 服务器把请求转成 CGI 脚本,CGI 脚本执行回复 Web 服务器,Web 服务回复给客户端。

CGI fork 一个新的进程来执行,读取参数,处理数据,然后就结束生命期。

FastCGI 采用 tcp 链接,不用 fork 新的进程,因为程序启动的时候就已经开启了,等待数据的到来,处理数据。

看出来差距在哪里了吧?就是 CGI 每次都要 fork 进程,这个开销很大的。(感兴趣的看下 linux 进程相关知识)。

现在我们来做我们的 CGI 服务器

CGI 服务器

需要用到的包:

复制代码
"net/http/cgi"
"net/http"

简单的 2 个包就可以实现 CGI 服务器了。“高秀敏:准备好了吗?希望别看到老头子他又错了的场景啊”。我们按照“代码 -> 讲解”的流程,先运行在讲解。

复制代码
 package main;
    import (
       "net/http/cgi"
       "fmt"
       "net/http"
    )
    
    funcmain() {
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){
            handler := new(cgi.Handler);
            handler.Path = "/usr/local/go/bin/go";
            script := "/Users/liujinlong/cgi-script" + r.URL.Path;
            fmt.Println(handler.Path);
            handler.Dir = "/Users/liujinlong/cgi-script";
            args := []string{"run", script};
            handler.Args = append(handler.Args, args...);
            fmt.Println(handler.Args);
    
            handler.ServeHTTP(w, r);
       });
    http.ListenAndServe(":8989",nil);
    select {}// 阻塞进程 
    }
    test.go
    package main
    
    import(
        "fmt"
    )
    
    funcinit() {
        fmt.Print("Content-Type: text/plain;charset=utf-8\n\n");
    }
    
    funcmain() {
        fmt.Println("hello!!!!")
    }

看来我们成功了。来看下 net/http/cgi 的包。

先看 host.go,这里有一个重要的结构 Handler。

复制代码
  // Handler runs an executable in a subprocess with a CGI environment.
    type Handler struct{
       Path string // 执行程序 
       Root string // 处理 url 的根,为空的时候“/”
       Dir string         // 目录 
       Env        []string    // 环境变量 
       InheritEnv []string    // 集成环境变量 
       Logger     *log.Logger// 日志 
       Args       []string    // 参数 
       PathLocationHandlerhttp.Handler //http 包的 handler 宿主 
    }

    func(h *Handler) ServeHTTP(rwhttp.ResponseWriter, req *http.Request)

它也实现了 ServeHttp,所有请求都会调用这个,这个后面分析 HTTP 源码的时候回详细讲解它是做什么的。Handler 是在子程序中执行 CGI 脚本的。

复制代码
funcRequest() (*http.Request, error)
funcServe(handler http.Handler) 
    …

先是将前端 CGI 请求转换成 net 包的 HTTP 请求,然后执行 Handler,然后处理 response。

FastCGI 服务器

接下来是 FastCGI 服务器,

用到的包:

复制代码
"net"
    "net/http"
    "net/http/fcgi"

上面已经讲过,它是 TCP 的方式实现的,需要借助其他服务器来做转发,这里我们只提供代码,demo 的截图讲解 TCP 的时候在加上。

需要使用 Nginx,我电脑上没有。各位自己测试一下

复制代码
server {
            listen 80;
 server_name ****;
            ...
            location *... {
                    include         fastcgi.conf;
    fastcgi_pass    127.0.0.1:9001;
            }
            ...
    }//…是省略,自己去写一个 server。(具体谷歌)
    package main
    
    import (
       "net"
       "net/http"
       "net/http/fcgi"
    )
    
    type FastCGIstruct{}
    
    func(s *FastCGI) ServeHTTP(resphttp.ResponseWriter, req *http.Request) {
    resp.Write([]byte("Hello, fastcgi"))
    }
    
    funcmain() {
       listener, _ := net.Listen("tcp", "127.0.0.1:8989")
       srv := new(FastCGI)
       fcgi.Serve(listener, srv)
    select {
    
       }
    }

HTTP 服务器

接下来就是重点了,我们的 HTTP 服务器,这个大家都不陌生,HTTP 是最常用的方式之一,通用性很强,跨团队协作上也比较受到推荐,排查问题也相对来说简单。

我们接下来以 3 种方式来展现 Golang 的 HTTP 服务器的简洁和强大。

  1. 写一个简单的 HTTP 服务器
  2. 写一个稍微复杂带路由的 HTTP 服务器
  3. 分析源码,然后实现一个自定义 Handler 的服务器

然后我们对照 net/http 包来进行源码分析,加强对 http 包的理解。

1、写一个简单的 HTTP 服务器:

复制代码
package main;
    
    import (
        "net/http"
    )
    
    funchello(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Hello"))
    }
    funcsay(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Hello"))
    }
    funcmain() {
        http.HandleFunc("/hello", hello);
        http.Handle("/handle",http.HandlerFunc(say));
        http.ListenAndServe(":8001", nil);
        select{};// 阻塞进程 
    }

是不是很简单,我用 2 种方式演示了这个例子,HandleFunc 和 Handle 方式不同,却都能实现一个路由的监听,其实很简单,但是很多人看到这都会有疑惑,别着急,咱们源码分析的时候你会看到。

2、写一个稍微复杂带路由的 HTTP 服务器:

对着上面的例子想一个问题,我们在开发中会遇到很多问题,比如 handle/res,handle/rsa…等等路由,这两个路由接受的参数都不一样,我们应该怎么写。我先来个图展示下运行结果。

是不是挺惊讶的,404 了,路由没有匹配到。可是我们写 handle 这个路由了。

问题:

  1. 什么原因导致的路由失效
  2. 如何解决这种问题,做一个可以用 Controller 来控制的路由

问题 1:

我们在源码阅读分析的时候会解决。

问题 2:

我们可以设定一个控制器 Handle,它有 2 个 action,我们的执行 handle/res 对应的结果是调用 Handle 的控制器下的 res 方法。这样是不是很酷。

来我们先上代码:

静态目录:

  1. css
  2. js
  3. image

静态目录很好实现,只要一个函数 http.FileServer(),这个函数从文字上看就是文件服务器,他需要传递一个目录,我们常以 http.Dir("Path") 来传递。

其他目录大家自己实现下,我们来实现问题 2,一个简单的路由。

我们来看下代码

复制代码
package main;
    
    import (
       "net/http"
       "strings"
       "reflect"
       "fmt"
    )
    
    funchello(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Hello"));
    }
    
    
    type Handlers struct{
    
    }
    
    func(h *Handlers) ResAction(w http.ResponseWriter, req *http.Request)  {
        fmt.Println("res");
        w.Write([]byte("res"));
    }
    funcsay(w http.ResponseWriter, req *http.Request) {
       pathInfo := strings.Trim(req.URL.Path, "/");
       parts := strings.Split(pathInfo, "/");
       varaction = "";
       fmt.Println(strings.Join(parts,"|"));
       if len(parts) >1 {
          action = strings.Title(parts[1]) + "Action";
       }
       fmt.Println(action);
       handle := &Handlers{};
       controller := reflect.ValueOf(handle);
       method := controller.MethodByName(action);
       r := reflect.ValueOf(req);
       wr := reflect.ValueOf(w);
       method.Call([]reflect.Value{wr, r});
    }
    funcmain() {
        http.HandleFunc("/hello", hello);
        http.Handle("/handle/",http.HandlerFunc(say));
        http.ListenAndServe(":8081", nil);
    select{};// 阻塞进程 
    }

上面代码就可以实现 handle/res,handle/rsa 等路由监听,把前缀相同的路由业务实现放在一个文件里,这样也可以解耦合,是不是清爽多了。其实我们可以在做的更加灵活些。在文章最后我们放出来一个流程图,按照流程图做你们就能写出一个简单的 mvc 路由框架。接下来看运行之后的结果。

如下图: 

(点击放大图像)

3、分析源码,然后实现一个自定义 Handler 的服务器

现在我们利用这个例子来分析下 http 包的源码 (只是服务器相关的,Request 我们此期不讲,简单看看就行。)

其实使用 Golang 做 web 服务器的方式有很多,TCP 也是一种,net 包就可以实现,不过此期我们不讲,因为 HTTP 服务器如果不懂,TCP 会让你更加不明白。

我们从入口开始,首先看 main 方法里的 http.HandleFunc 和 http.Handle 这个绑定路由的方法,上面一直没解释有啥区别。现在我们来看一下。

复制代码
// HandleFunc registers the handler function for the given pattern
    // in the DefaultServeMux.
    // The documentation for ServeMux explains how patterns are matched.
    funcHandleFunc(pattern string, handler func(ResponseWriter, *Request)) 
    funcHandle(pattern string, handler Handler)

Handle 和 HandleFunc 都是注册路由,从上面也能看出来这两个函数都是绑定注册路由函数的。如何绑定的呢?我们来看下。

上面 2 个函数通过 DefaultServeMux.handle,DefaultServeMux.handleFunc 把 pattern 和 HandleFunc 绑定到 ServeMux 的 Handle 上。

为什么 DefaultServeMux 会把路由绑定到 ServeMux 上呢?

复制代码
// DefaultServeMux is the default ServeMux used by Serve.
    varDefaultServeMux = NewServeMux()

因为 DefaultServeMux 就是 ServeMux 的实例对象。导致我们就把路由和执行方法绑注册好了。不过大家请想下 handle/res 的问题?

从上面的分析我们要知道几个重要的概念。

复制代码
HandlerFunc

    // The HandlerFunc type is an adapter to allow the use of
    // ordinary functions as HTTP handlers.  If f is a function
    // with the appropriate signature, HandlerFunc(f) is a
    // Handler object that calls f.
    type HandlerFuncfunc(ResponseWriter, *Request)
    
    // ServeHTTP calls f(w, r).
    func(f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
       f(w, r)
    }

上面的大概意思是,定义了一个函数适配器 (可以理解成函数指针)HandleFunc,通过 HandlerFunc(f) 来进行适配。其实调用的实体是 f 本身。

复制代码
package main
    import "fmt"
    type A func(int, int)
    func(f A)Serve() {
        fmt.Println("serve2")
    }
    funcserve(int,int) {
        fmt.Println("serve1")
    }
    funcmain() {
       a := A(serve)
       a(1,2)// 这行输出的结果是 serve1
       a.Serve()// 这行输出的结果是 serve2
    }

上面结果是 serve1,serve2

Golang 的源码里用了很多 HandleFunc 这个适配器。

接下来我们看第二个,ServeMux 结构,最终我们是绑定它,也是通过它来解析。

复制代码
type ServeMuxstruct{
       mu    sync.RWMutex// 读写锁 
       m     map[string]muxEntry// 路由 map,pattern->HandleFunc
       hosts bool// 是否包含 hosts        
    }
    
    type muxEntrystruct{
       explicit bool// 是否精确匹配,这个在 Golang 实现里是 ture
       h        Handler // 这个路由表达式对应哪个 handler
       pattern  string// 路由 
    }

看到 explicit 的时候是不是就明白为啥 handle/res 不能用 handle 来监听了?原来如此。大致绑定流程大家看明白了吗?如果不理解可以回去再看一遍。

接下来我们来看实现“启动 / 监听 / 触发”服务器的代码。

复制代码
http.ListenAndServe(":8081", nil);

上面这句就是,”:8081”是监听的端口,也是 socket 监听的端口,第二个参数就是我们的 Handler,这里我们写 nil。

复制代码
 funcListenAndServe(addr string, handler Handler) error {
       server := &Server{Addr: addr, Handler: handler}
       return server.ListenAndServe()
    }

从这个代码看出来,Server 这个结构很重要。我们来看看他是什么。

复制代码
type Server struct {
    Addr           string        // 监听的地址和端口 
    Handler        Handler       // 所有请求需要调用的 Handler(
    ReadTimeouttime.Duration // 读的最大 Timeout 时间 
    WriteTimeouttime.Duration // 写的最大 Timeout 时间 
    MaxHeaderBytesint           // 请求头的最大长度 
    TLSConfig      *tls.Config   // 配置 TLS
        ...   // 结构太长我省略些,感兴趣大家自己看下 
    }

Server 提供的方法有:

复制代码
 func(srv *Server) Serve(l net.Listener) error   // 对某个端口进行监听,里面就是调用 for 进行 accept 的处理了 
 func(srv *Server) ListenAndServe() error  // 开启 http server 服务 
 func(srv *Server) ListenAndServeTLS(certFile, keyFile string) error // 开启 https server 服务

Server 的 ListenAndServe 方法通过 TCP 的方式监听端口,然后调用 Serve 里的实现等待 client 来 accept,然后开启一个协程来处理逻辑 (go c.serve)。

它的格式

复制代码
func(srv *Server) ListenAndServe() error 

看到这里我们要了解几个重要的概念。

ResponseWriter:生成 Response 的接口

Handler:处理请求和生成返回的接口

ServeMux:路由,后面会说到 ServeMux 也是一种 Handler

Conn : 网络连接

这几个概念看完之后我们下面要用。

type conn struct

这个结构是一个网络间接。我们暂时忽略。

这个 c.serve 里稍微有点复杂,它有关闭这次请求,读取数据的,刷新缓冲区的等实现。这里我们主要关注一个 c.readRequest(),通过 redRequest 可以得到 Response,就是输出给客户端数据的一个回复者。

它里面包含 request。如果要看懂这里的实现就要搞懂三个接口。

ResponseWriter, Flusher, Hijacker

    // ResponseWriter 的作用是被 Handler 调用来组装返回的 Response 的 
    type ResponseWriter interface {
        // 这个方法返回 Response 返回的 Header 供读写 
        Header() Header
    
        // 这个方法写 Response 的 Body
        Write([]byte) (int, error)
    
        // 这个方法根据 HTTP State Code 来写 Response 的 Header
    
        WriteHeader(int)
    }
    
    // Flusher 的作用是被 Handler 调用来将写缓存中的数据推给客户端 
    type Flusher interface {
        // 刷新缓冲区 
        Flush()
    }
    
    // Hijacker 的作用是被 Handler 调用来关闭连接的 
    type Hijacker interface {
        Hijack() (net.Conn, *bufio.ReadWriter, error)
    
    }

而我们这里的 w 也就是 ResponseWriter 了。而调用了下面这句方法,就可以利用它的 Write 方法输出内容给客户端了。

serverHandler{c.server}.ServeHTTP(w, w.req)

这句就是触发路由绑定的方法了。要看这个触发器我们还要知道几个接口。

具体我们先看下如何实现这三个接口的,因为后面我们要看触发路由执行逻辑片段。实现这三个接口的结构是 response

response
    // response 包含了所有 server 端的 HTTP 返回信息 
    type response struct {
        conn          *conn         // 保存此次 HTTP 连接的信息 
        req           *Request // 对应请求信息 
        chunking      bool     // 是否使用 chunk
        wroteHeaderbool     // header 是否已经执行过写操作 
        wroteContinuebool     // 100 Continue response was written
        header        Header   // 返回的 HTTP 的 Header
        written       int64    // Body 的字节数 
        contentLength int64    // Content 长度 
        status        int      // HTTP 状态 
        needSniffbool  
// 是否需要使用 sniff。(当没有设置 Content-Type 的时候,开启 sniff 能根据 HTTP body 来确定 Content-Type)
        closeAfterReplybool    
// 是否保持长链接。如果客户端发送的请求中 connection 有 keep-alive,这个字段就设置为 false。
        requestBodyLimitHitbool 
// 是否 requestBody 太大了(当 requestBody 太大的时候,response 是会返回 411 状态的,并把连接关闭)
    }

在 response 中是可以看到

func(w *response) Header() Header 
func(w *response) WriteHeader(code int) 
func(w *response) Write(data []byte) (n int, err error) 
func(w *response) WriteString(data string) (n int, err error) 
// either dataB or dataS is non-zero.
func(w *response) write(lenDataint, dataB []byte, dataS string) (n int, err error) 
func(w *response) finishRequest()
func(w *response) Flush() 
func(w *response) Hijack() (rwcnet.Conn, buf *bufio.ReadWriter, err error)

我简单罗列一些,从上面可以看出,response 实现了这 3 个接口。

接下来我们请求真正的触发者也就是 serverHandle 要触发路由 (hijacked finishRequest 暂且不提)。先看一个接口。

Handler
    
    type Handler interface {
    ServeHTTP(ResponseWriter, *Request)  // 具体的逻辑函数 
    }

实现了 handler 接口,就意味着往 server 端添加了处理请求的逻辑函数。

serverHandle 调用 ServeHttp 来选择触发的 HandleFunc。这里面会做一个判断,如果你传递了 Handler,就调用你自己的,如果没传递就用 DefaultServeMux 默认的。到这整体流程就结束了。

过程是:

DefaultServeMux.ServeHttp 执行的简单流程.

  1. h, _ := mux.Handler(r)
  2. h.ServeHTTP(w, r)   // 执行 ServeHttp 函数

查找路由,mux.handler 函数里又调用了另外一个函数 mux.handler(r.Host, r.URL.Path)。

还记得我们的 ServeMux 里的 hosts 标记吗?这个函数里会进行判断。

// Host-specific pattern takes precedence over generic ones
       if mux.hosts {
          h, pattern = mux.match(host + path)
       }
       if h == nil {
         h, pattern = mux.match(path)
       }
       if h == nil {
         h, pattern = NotFoundHandler(), ""
       }

上面就是匹配查找 pattern 和 handler 的流程了

我们来总结一下。

首先调用 Http.HandleFunc

按顺序做了几件事:

  1. 调用了 DefaultServerMux 的 HandleFunc
  2. 调用了 DefaultServerMux 的 Handle
  3. 往 DefaultServeMux 的 map[string]muxEntry 中增加对应的 handler 和路由规则

别忘记 DefaultServerMux 是 ServeMux 的实例。其实都是围绕 ServeMux,muxEntry2 个结构进行操作绑定。

其次调用 http.ListenAndServe(":12345", nil)

按顺序做了几件事情:

  1. 实例化 Server
  2. 调用 Server 的 ListenAndServe()
  3. 调用 net.Listen("tcp", addr) 监听端口,启动 for 循环,等待 accept 请求
  4. 对每个请求实例化一个 Conn,并且开启一个 goroutine 处理请求。
  5. 如:go c.serve()
  6. 读取请求的内容 w, err := c.readRequest(),也就是 response 的取值过程。
  7. 调用 serverHandler 的 ServeHTTP,ServeHTTP 里会判断 Server 的属性里的 header 是否为空,如果没有设置 handler,handler 就设置为 DefaultServeMux,反之用自己的 (我们后面会做一个利用自己的 Handler 写服务器)
  8. 调用 DefaultServeMux 的 ServeHttp( 因为我们没有自己的 Handler,所以走默认的)
  9. 通过 request 选择匹配的 handler:

    A request 匹配 handler 的方式。Hosts+pattern 或 pattern 或 notFound

    B 如果有路由满足,返回这个 handler

    C 如果没有路由满足,返回 NotFoundHandler

  10. 根据返回的 handler 进入到这个 handler 的 ServeHTTP

大概流程就是这个样子,其实在 net.Listen("tcp", addr) 里也做了很多事,我们下期说道 TCP 服务器的时候回顾一下他做了哪些。

通过上面的解释大致明白了我们绑定触发的都是 DefaultServeMux 的 Handler。现在我们来实现一个自己的 Handler,这也是做框架的第一步。我们先来敲代码。

package main;
    
    import (
       "fmt"
       "net/http"
       "time"
    )
    
    type customHandlerstruct{
    
    }
    
    func(cb *customHandler) ServeHTTP( w http.ResponseWriter, r *http.Request ) {
        fmt.Println("customHandler!!");
        w.Write([]byte("customHandler!!"));
    }
    
    funcmain() {
        varserver *http.Server = &http.Server{
          Addr:           ":8080",
          Handler:        &customHandler{},
          ReadTimeout:    10 * time.Second,
          WriteTimeout:   10 * time.Second,
          MaxHeaderBytes: 1 <<20,
       }
    server.ListenAndServe();
    select {
       }
    }

是不是很酷,我们可以利用自己的 handler 做一个智能的路由出来。

不过还是建议使用国内 Golang 语言框架beego,已开源。一款非常不错的框架,谢大维护的很用心,绝对良心框架,而且文档支持,社区也很不错。

最后附上一张最早设计框架时候的一个流程图 (3 年前)。大家可以简单看看,当然也可以尝试的动动手。起码收获很多。

(点击放大图像)

  [1]: http://item.jd.com/11573034.html

  [2]: https://github.com/astaxie/beego

作者简介

刘金龙,艺名:金灶沐 ,go 语言爱好者,2015 年 8 月加入创业团队,负责各种“打杂”工作,之前在 360 电商购物小蜜 java 组担任 java 高级工程师职位,负责购物小蜜服务开发。14 年开始用 go 语言做高并发服务并且尝试阅读 go 语言的源码来学习 go 语言的特性。