Life of xhu

gin 源码阅读笔记

Dec 21, 2017  |  Go

今天来看一个 Go 项目的源码: gin: Live reload utility for Go web servers.

这个项目的简介是实现 Go web server 的实时重载, 现在这个博客的 dev 模式就是使用这个项目启动的, 启动脚本如下:

gin --excludeDir archives --excludeDir node_modules --excludeDir app/assets --all --port 8283 --appPort 13109

忽略命令中的一串参数, 这行脚本的作用是, 整个项目对外暴露 8283, 请求会被重定向到 13109 端口上, 然后 main.go 是 go server 入口并且实现热重载, 这样分析之后我们我们可以把这个问题分成两个部分:

  1. 怎么在内部启动 go server 并做 http 数据包的转发
  2. 怎么一个检测文件改动并重启内部服务器

带着这两个问题, 我们直接开始看源码吧, 以下代码都省略了无关代码:

// main.go
func MainAction(c *cli.Context) {
    os.Setenv("PORT", appPort)

  wd, err := os.Getwd()

  buildArgs, err := shellwords.Parse(c.GlobalString("buildArgs"))

  buildPath := c.GlobalString("build")
  builder := gin.NewBuilder(buildPath, c.GlobalString("bin"), c.GlobalBool("godep"), wd, buildArgs)
  runner := gin.NewRunner(filepath.Join(wd, builder.Binary()), c.Args()...)
  runner.SetWriter(os.Stdout)
  proxy := gin.NewProxy(builder, runner)

  config := &gin.Config{
    Laddr:    laddr,
    Port:     port,
    ProxyTo:  "http://localhost:" + appPort,
    KeyFile:  keyFile,
    CertFile: certFile,
  }

  err = proxy.Run(config)

  shutdown(runner)

  build(builder, runner, logger)

  // scan for changes
  scanChanges(c.GlobalString("path"), c.GlobalStringSlice("excludeDir"), all, func(path string) {
    runner.Kill()
    build(builder, runner, logger)
  })
}

在这段入口里, 首先把需要转发的端口放到了环境变量里, 然后取了三个在编译 go server 时需要用到的常量:

  1. wd: 当前的工作目录;
  2. buildArgs: 构建参数;
  3. buildPath: 构建 go server 的路径.

接下来我们可以看到, 整个 gin 项目把代码分成了三个模块, 分别是:

  1. builder: 使用上面的三个常量来构建内部服务器;

     // lib/builder.go
     type builder struct {
       dir       string                  // 构建的目录
       binary    string                  // 构建得到的二进制文件
       wd        string                  // 当前工作目录
       buildArgs []string                // 构建参数
     }
  2. runner: 负责运行和停止内部服务器;

     // lib/runner.go
     type runner struct {
       bin       string                  // builder 构建的二进制文件路径
       command   *exec.Cmd               // 使用二进制文件得到的 Command 实例
       starttime time.Time               // 当前内部服务器 进程开始的时间
     }
  3. proxy: 将外部的 http/https 请求转发到内部的 go server 上.

     // lib/proxy.go
     type Proxy struct {
       listener net.Listener             // 监听网络请求
       proxy    *httputil.ReverseProxy   // ReverseProxy 实例, 实现反响代码数据转发
       builder  Builder                  // Builder 接口实例
       runner   Runner                   // Runner 接口实例
       to       *url.URL                 // 反响代理的地址
     }

下面就是针对这三个模块的 new 函数:

  1. NewBuilder: 编译内部 server, 获得二进制文件信息, 返回实现了 Builder 接口的 builder 实例;
  2. NewRunner: 使用 builder 信息生成 exec.Command 实例, 返回实现了 Runner 接口的 runner 实例;
  3. NewProxy: 使用 builderrunner 生成 Proxy 实例, 其他字段暂时置为空.

接下俩就是调用 Run 方法来启动 proxy, 实现网络请求的转发:

config := &gin.Config{
  Laddr:    laddr,
  Port:     port,
  ProxyTo:  "http://localhost:" + appPort,
  KeyFile:  keyFile,
  CertFile: certFile,
}

err = proxy.Run(config)

下面我们来看一下 Run 方法的具体实现:

// proxy.go
func (p *Proxy) Run(config *Config) error {
  url, err := url.Parse(config.ProxyTo)
  p.proxy = httputil.NewSingleHostReverseProxy(url)
  p.to = url

  server := http.Server{Handler: http.HandlerFunc(p.defaultHandler)}

  // 省略 https 的处理代码
  p.listener, err = net.Listen("tcp", fmt.Sprintf("%s:%d", config.Laddr, config.Port))

  go server.Serve(p.listener)

  return nil
}

func (p *Proxy) defaultHandler(res http.ResponseWriter, req *http.Request) {
  errors := p.builder.Errors()
  if len(errors) > 0 {
    res.Write([]byte(errors))
  } else {
    p.runner.Run()
    p.proxy.ServeHTTP(res, req)
  }
}

也就是说, proxy 实例本质上是一个简单的 http 服务器, 这个服务器的请求都会打到 defaultHanlder 上, 而这个 handler 的作用有两个, 那就是在有请求到达的时候:

  1. 通过 runner.Run 方法, 确保内部服务器 已经在运行;
  2. 通过 *httputil.ReverseProxy#ServeHTTP 方法, 将请求转发到内部服务器 上.

我们在来看一下运行内部服务器 的 runner.Run 方法:

func (r *runner) Run() (*exec.Cmd, error) {
  if r.command == nil || r.Exited() {
    err := r.runBin()
    time.Sleep(250 * time.Millisecond)
    return r.command, err
  } else {
    return r.command, nil
  }

}

func (r *runner) runBin() error {
  r.command = exec.Command(r.bin, r.args...)
  err = r.command.Start()
  r.starttime = time.Now()

  go r.command.Wait()

  return nil
}

我们可以看到 runner.Run 方法其实是调用了内部的 runBin 方法, 在 runBin 方法里通过 os/exec 包生成了 *exec.CMD 对象, 通过 Start 方法执行之后, 会在一个新的协程里执行 Wait 方法, 使后台的服务器进程不会阻塞主进程.

回到 Run 方法中, 在启动内部服务器 之后还有一个 250ms 的停顿, 应该是等待服务器启动的时间.

到这里我们就算是弄明白了上文中的第一个问题, 简单的说, 就是通过 os/exec 来进行内部服务器启动, 通过 net/http/httputil 进行 http 请求转发, 当然通过阅读源码我们也可以发现一些实现上的小瑕疵:

  1. 在 build 完二进制文件, 只有当有 http 请求进来的时候, 才会执行这个二进制文件启动内部服务器, 而如果内部服务器启动时间大于 250ms, 那么修改文件之后的第一次请求总是会失败, 这也符合实际使用时的表现;
  2. runner 中的 runBin 只适用于内部服务器不带参数执行的情况, 因为 r.args 使用的是 gin 本身的参数列表, 并不一定能被内部服务器识别, 如果要实现这个, 只能给 gin 加一个新的参数了比如 executeArgs.

那么我们看第二个问题, 再次回到 main.go 文件:

// main.go
scanChanges(c.GlobalString("path"), c.GlobalStringSlice("excludeDir"), all, func(path string) {
  runner.Kill()
  build(builder, runner, logger)
}}

func scanChanges(watchPath string, excludeDirs []string, allFiles bool, cb scanCallback) {
  for {
    filepath.Walk(watchPath, func(path string, info os.FileInfo, err error) error {
      if path == ".git" && info.IsDir() {
        return filepath.SkipDir
      }
      for _, x := range excludeDirs {
        if x == path {
          return filepath.SkipDir
        }
      }

      // ignore hidden files
      if filepath.Base(path)[0] == '.' {
        return nil
      }

      if (allFiles || filepath.Ext(path) == ".go") && info.ModTime().After(startTime) {
        cb(path)
        startTime = time.Now()
        return errors.New("done")
      }

      return nil
    })
    time.Sleep(500 * time.Millisecond)
  }
}

这里其实就比较简单了, scanChanges 的内部实现其实是用一个间隔为半秒的死循环在不停的通过 filepath.Walk 方法来遍历参数 path 设定的目录, 如果一个文件满足下列条件:

  1. 不是 .git 目录;
  2. 不在 execludeDir 参数中;
  3. 不是隐藏文件;
  4. 扩展名是 .go 或者运行时带了 --all 参数;
  5. 文件在内部服务器启动后被修改过.

那么我们就执行回调函数 cb 并重置 startTime. 而回调函数中的内容就是终止当前内部服务器进程和重新 build. 而终止进程的 Kill 方法实现如下:

func (r *runner) Kill() error {
  if r.command != nil && r.command.Process != nil {
    done := make(chan error)
    go func() {
      r.command.Wait()
      close(done)
    }()

    select {
    case <-time.After(3 * time.Second):
      if err := r.command.Process.Kill(); err != nil {
        log.Println("failed to kill: ", err)
      }
    case <-done:
    }
    r.command = nil
  }

  return nil
}

这里做了一个超时处理, 如果进程在调用 Wait 方法 3 秒之后仍然没有响应, 就会被 Kill 方法来终止, 并且打印出命令执行的错误. 而回调中的下一步 build 就会重新生成内部服务器的二进制文件, 接下来有 http 请求的话, 就会进入上面 proxy 中的 defaultHandler, 进而执行 runner.Run 方法重新启动内部进服务器.

具体的流程图可以用下图来表示:

而这次阅读我们也学到了一些非常有用的内部库的用法:

package struct func description
net/http/httputil ReverseProxy ServeHTTP 反向代理 http 请求
path/filepath - Walk 遍历一个目录
os/exec CMD Start/Wait 执行一个命令并且等待输出, 可以用来执行耗时或者被挂起的命令

refs:

Copyright © 2018 - xhu - Powered by Gin,jQuery,Animate.css,Semantic UI

fammhF iBSbYp hKTLac bcBwio jMfvbc gFwJbt ikkyVr bPjPUf cofShm ekvxDO a"> /* sc-component-id: sc-bdVaJa */ .sc-bdVaJa {} .jTcPxY{height:100%;overflow:auto;} /* sc-component-id: sc-bwzfXH */ .sc-bwzfXH {} .iYgZUj{color:#BBB;margin:10px 0 20px 0;} /* sc-component-id: sc-htpNat */ .sc-htpNat {} .knYzzU{position:relative;padding:10px 0 10px 0;width:calc(100% - 10px);height:50px;-webkit-transition:-webkit-transform 0.6s;-webkit-transition:transform 0.6s;transition:transform 0.6s;} .knYzzU:hover{background-color:#EEE;-webkit-transform:translate(10px,0);-ms-transform:translate(10px,0);transform:translate(10px,0);} /* sc-component-id: sc-bxivhb */ .sc-bxivhb {} .fpBhvp{position:absolute;color:#888;} /* sc-component-id: sc-ifAKCX */ .sc-ifAKCX {} .eMFtQj{position:absolute;top:7px;left:60px;color:#666;font-size:18px;line-height:24px;-webkit-text-decoration:underline;text-decoration:underline;} .eMFtQj:hover{color:#666;-webkit-text-decoration:underline;text-decoration:underline;}.kDHEfQ{position:absolute;top:7px;left:60px;color:#666;font-size:18px;line-height:24px;-webkit-text-decoration:underline;text-decoration:underline;} .kDHEfQ:hover{color:#666;-webkit-text-decoration:underline;text-decoration:underline;} /* sc-component-id: sc-EHOje */ .sc-EHOje {} .dwejLW{display:inline-block;text-align:center;width:110px;font-size:14px;-webkit-letter-spacing:2px;-moz-letter-spacing:2px;-ms-letter-spacing:2px;letter-spacing:2px;color:#666;border:1px solid #DADADA;background:#FFF;padding:7px 8px 7px 10px;margin:30px 20px 50px 0;} .dwejLW:hover{background:#EEE;} /* sc-component-id: sc-bZQynM */ .sc-bZQynM {} .jMfvbc{height:100%;overflow:auto;} /* sc-component-id: sc-gzVnrw */ .sc-gzVnrw {} .gFwJbt{margin:40px 0;text-align:center;font-weight:500;color:#646464;font-family:"Lato",sans-serif;} /* sc-component-id: sc-htoDjs */ .sc-htoDjs {} .ikkyVr{margin:20px 0 40px 0 !important;} /* sc-component-id: sc-dnqmqq */ .sc-dnqmqq {} .bPjPUf a{color:#A3717F;} /* sc-component-id: sc-iwsKbI */ .sc-iwsKbI {} .cofShm{-webkit-letter-spacing:.2px;-moz-letter-spacing:.2px;-ms-letter-spacing:.2px;letter-spacing:.2px;font-size:15px;color:#555;} .cofShm h1,.cofShm h2,.cofShm h3,.cofShm h4,.cofShm h5,.cofShm h6{margin:20px 0 15px;font-weight:500;color:#646464;} .cofShm p,.cofShm li{line-height:1.9;} .cofShm blockquote{padding:15px 0 15px 15px;margin:0 0 18px;border-left:5px solid #D1D0CE;line-height:28px;font-weight:normal;font-size:15px;font-style:italic;color:#696969;} .cofShm img{max-width:100%;} .cofShm a{color:#4183c4;-webkit-text-decoration:none;text-decoration:none;} .cofShm hr{border:0;color:#ddd;background-color:#ddd;height:2px;margin:5px 0 19px 0;} .cofShm code{display:inline;word-wrap:break-word;font-size:14px;color:rgb(85,85,85);background:rgb(255,255,255);border-width:1px;border-style:solid;border-color:rgb(221,221,221);border-image:initial;border-radius:4px;padding:1px 3px;margin:-1px 1px 0px;} .cofShm pre code{display:block;font-size:11.8px;line-height:18px;font-weight:12px;-webkit-letter-spacing:.5px;-moz-letter-spacing:.5px;-ms-letter-spacing:.5px;letter-spacing:.5px;margin:0 0 20px 0;padding:15px !important;background-color:#f7f7f7 !important;border-width:0;} /* sc-component-id: sc-gZMcBi */ .sc-gZMcBi {} .ekvxDO{padding:18px 0 54px 0;}.a{padding:18px 0 54px 0;} /* sc-component-id: sc-VigVT */ .sc-VigVT {} .hbtmav{width:100%;height:100%;} /* sc-component-id: sc-jTzLTM */ .sc-jTzLTM {} .bchbfv{padding:60px 0 0 0;min-height:calc(100% - 60px);width:720px;margin-left:calc((100% - 720px) / 2);margin-right:calc((100% - 720px) / 2);} @media screen and (max-width:720px){.bchbfv{width:100%;margin-left:0;margin-right:0;padding:60px 15px 0 15px;}}.iZSXmW{padding:60px 0 0 0;min-height:calc(100% - 60px);width:720px;margin-left:calc((100% - 720px) / 2);margin-right:calc((100% - 720px) / 2);} @media screen and (max-width:720px){.iZSXmW{width:100%;margin-left:0;margin-right:0;padding:60px 15px 0 15px;}} /* sc-component-id: sc-fjdhpX */ .sc-fjdhpX {} .hzPhWj{margin:0 0 45px 0;font-size:18px;} /* sc-component-id: sc-keyframes-blDzHL */ @-webkit-keyframes blDzHL{0%{opacity:1;}50%{opacity:0;}100%{opacity:1;}} @keyframes blDzHL{0%{opacity:1;}50%{opacity:0;}100%{opacity:1;}} /* sc-component-id: sc-jzJRlG */ .sc-jzJRlG {} .kkvUer{margin:0 0 0 6px;-webkit-animation:blDzHL 1.2s infinite linear;animation:blDzHL 1.2s infinite linear;} /* sc-component-id: sc-cSHVUG */ .sc-cSHVUG {} .iBSbYp{background-color:#FFFEEC;height:60px;padding:18px 0 0 0;text-align:center;color:#444444;opacity:.8;-webkit-letter-spacing:.8px;-moz-letter-spacing:.8px;-ms-letter-spacing:.8px;letter-spacing:.8px;font-family:Lato,sans-serif;} @media screen and (max-width:720px){.iBSbYp{height:60px;padding:10px 0 18px 0;}}.hKTLac{background-color:#FFFEEC;height:60px;padding:18px 0 0 0;text-align:center;color:#444444;opacity:.8;-webkit-letter-spacing:.8px;-moz-letter-spacing:.8px;-ms-letter-spacing:.8px;letter-spacing:.8px;font-family:Lato,sans-serif;} @media screen and (max-width:720px){.hKTLac{height:60px;padding:10px 0 18px 0;}}.bcBwio{background-color:#FFFEEC;height:60px;padding:18px 0 0 0;text-align:center;color:#444444;opacity:.8;-webkit-letter-spacing:.8px;-moz-letter-spacing:.8px;-ms-letter-spacing:.8px;letter-spacing:.8px;font-family:Lato,sans-serif;} @media screen and (max-width:720px){.bcBwio{height:60px;padding:10px 0 18px 0;}} /* sc-component-id: sc-kAzzGY */ .sc-kAzzGY {} .bpqJVJ{position:fixed;right:-10px;bottom:70px;} /* sc-component-id: sc-chPdSV */ .sc-chPdSV {} .iGXldS{display:none;cursor:pointer;width:60px;height:60px;text-indent:100%;margin:0 0 0 -3px;box-shadow:0 0 10px rgba(0,0,0,0.05);background:rgba(87,218,178,0.8) url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBB%0D%0AZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9u%0D%0AOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBT%0D%0AVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzEx%0D%0ALmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3%0D%0ALnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxp%0D%0AbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94%0D%0APSIwIDAgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE2IDE2IiB4bWw6c3BhY2U9%0D%0AInByZXNlcnZlIj4NCjxwb2x5Z29uIGZpbGw9IiNGRkZGRkYiIHBvaW50cz0iOCwyLjggMTYsMTAu%0D%0ANyAxMy42LDEzLjEgOC4xLDcuNiAyLjUsMTMuMiAwLDEwLjcgIi8+DQo8L3N2Zz4NCg==) no-repeat center 50%;} .iGXldS:hover{background:rgba(87,218,178,0.6) url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBB%0D%0AZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9u%0D%0AOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBT%0D%0AVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzEx%0D%0ALmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3%0D%0ALnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxp%0D%0AbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94%0D%0APSIwIDAgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE2IDE2IiB4bWw6c3BhY2U9%0D%0AInByZXNlcnZlIj4NCjxwb2x5Z29uIGZpbGw9IiNGRkZGRkYiIHBvaW50cz0iOCwyLjggMTYsMTAu%0D%0ANyAxMy42LDEzLjEgOC4xLDcuNiAyLjUsMTMuMiAwLDEwLjcgIi8+DQo8L3N2Zz4NCg==) no-repeat center 50%;}.
Life of xhu

gin 源码阅读笔记

Dec 21, 2017  |  Go

今天来看一个 Go 项目的源码: gin: Live reload utility for Go web servers.

这个项目的简介是实现 Go web server 的实时重载, 现在这个博客的 dev 模式就是使用这个项目启动的, 启动脚本如下:

gin --excludeDir archives --excludeDir node_modules --excludeDir app/assets --all --port 8283 --appPort 13109

忽略命令中的一串参数, 这行脚本的作用是, 整个项目对外暴露 8283, 请求会被重定向到 13109 端口上, 然后 main.go 是 go server 入口并且实现热重载, 这样分析之后我们我们可以把这个问题分成两个部分:

  1. 怎么在内部启动 go server 并做 http 数据包的转发
  2. 怎么一个检测文件改动并重启内部服务器

带着这两个问题, 我们直接开始看源码吧, 以下代码都省略了无关代码:

// main.go
func MainAction(c *cli.Context) {
    os.Setenv("PORT", appPort)

  wd, err := os.Getwd()

  buildArgs, err := shellwords.Parse(c.GlobalString("buildArgs"))

  buildPath := c.GlobalString("build")
  builder := gin.NewBuilder(buildPath, c.GlobalString("bin"), c.GlobalBool("godep"), wd, buildArgs)
  runner := gin.NewRunner(filepath.Join(wd, builder.Binary()), c.Args()...)
  runner.SetWriter(os.Stdout)
  proxy := gin.NewProxy(builder, runner)

  config := &gin.Config{
    Laddr:    laddr,
    Port:     port,
    ProxyTo:  "http://localhost:" + appPort,
    KeyFile:  keyFile,
    CertFile: certFile,
  }

  err = proxy.Run(config)

  shutdown(runner)

  build(builder, runner, logger)

  // scan for changes
  scanChanges(c.GlobalString("path"), c.GlobalStringSlice("excludeDir"), all, func(path string) {
    runner.Kill()
    build(builder, runner, logger)
  })
}

在这段入口里, 首先把需要转发的端口放到了环境变量里, 然后取了三个在编译 go server 时需要用到的常量:

  1. wd: 当前的工作目录;
  2. buildArgs: 构建参数;
  3. buildPath: 构建 go server 的路径.

接下来我们可以看到, 整个 gin 项目把代码分成了三个模块, 分别是:

  1. builder: 使用上面的三个常量来构建内部服务器;

     // lib/builder.go
     type builder struct {
       dir       string                  // 构建的目录
       binary    string                  // 构建得到的二进制文件
       wd        string                  // 当前工作目录
       buildArgs []string                // 构建参数
     }
  2. runner: 负责运行和停止内部服务器;

     // lib/runner.go
     type runner struct {
       bin       string                  // builder 构建的二进制文件路径
       command   *exec.Cmd               // 使用二进制文件得到的 Command 实例
       starttime time.Time               // 当前内部服务器 进程开始的时间
     }
  3. proxy: 将外部的 http/https 请求转发到内部的 go server 上.

     // lib/proxy.go
     type Proxy struct {
       listener net.Listener             // 监听网络请求
       proxy    *httputil.ReverseProxy   // ReverseProxy 实例, 实现反响代码数据转发
       builder  Builder                  // Builder 接口实例
       runner   Runner                   // Runner 接口实例
       to       *url.URL                 // 反响代理的地址
     }

下面就是针对这三个模块的 new 函数:

  1. NewBuilder: 编译内部 server, 获得二进制文件信息, 返回实现了 Builder 接口的 builder 实例;
  2. NewRunner: 使用 builder 信息生成 exec.Command 实例, 返回实现了 Runner 接口的 runner 实例;
  3. NewProxy: 使用 builderrunner 生成 Proxy 实例, 其他字段暂时置为空.

接下俩就是调用 Run 方法来启动 proxy, 实现网络请求的转发:

config := &gin.Config{
  Laddr:    laddr,
  Port:     port,
  ProxyTo:  "http://localhost:" + appPort,
  KeyFile:  keyFile,
  CertFile: certFile,
}

err = proxy.Run(config)

下面我们来看一下 Run 方法的具体实现:

// proxy.go
func (p *Proxy) Run(config *Config) error {
  url, err := url.Parse(config.ProxyTo)
  p.proxy = httputil.NewSingleHostReverseProxy(url)
  p.to = url

  server := http.Server{Handler: http.HandlerFunc(p.defaultHandler)}

  // 省略 https 的处理代码
  p.listener, err = net.Listen("tcp", fmt.Sprintf("%s:%d", config.Laddr, config.Port))

  go server.Serve(p.listener)

  return nil
}

func (p *Proxy) defaultHandler(res http.ResponseWriter, req *http.Request) {
  errors := p.builder.Errors()
  if len(errors) > 0 {
    res.Write([]byte(errors))
  } else {
    p.runner.Run()
    p.proxy.ServeHTTP(res, req)
  }
}

也就是说, proxy 实例本质上是一个简单的 http 服务器, 这个服务器的请求都会打到 defaultHanlder 上, 而这个 handler 的作用有两个, 那就是在有请求到达的时候:

  1. 通过 runner.Run 方法, 确保内部服务器 已经在运行;
  2. 通过 *httputil.ReverseProxy#ServeHTTP 方法, 将请求转发到内部服务器 上.

我们在来看一下运行内部服务器 的 runner.Run 方法:

func (r *runner) Run() (*exec.Cmd, error) {
  if r.command == nil || r.Exited() {
    err := r.runBin()
    time.Sleep(250 * time.Millisecond)
    return r.command, err
  } else {
    return r.command, nil
  }

}

func (r *runner) runBin() error {
  r.command = exec.Command(r.bin, r.args...)
  err = r.command.Start()
  r.starttime = time.Now()

  go r.command.Wait()

  return nil
}

我们可以看到 runner.Run 方法其实是调用了内部的 runBin 方法, 在 runBin 方法里通过 os/exec 包生成了 *exec.CMD 对象, 通过 Start 方法执行之后, 会在一个新的协程里执行 Wait 方法, 使后台的服务器进程不会阻塞主进程.

回到 Run 方法中, 在启动内部服务器 之后还有一个 250ms 的停顿, 应该是等待服务器启动的时间.

到这里我们就算是弄明白了上文中的第一个问题, 简单的说, 就是通过 os/exec 来进行内部服务器启动, 通过 net/http/httputil 进行 http 请求转发, 当然通过阅读源码我们也可以发现一些实现上的小瑕疵:

  1. 在 build 完二进制文件, 只有当有 http 请求进来的时候, 才会执行这个二进制文件启动内部服务器, 而如果内部服务器启动时间大于 250ms, 那么修改文件之后的第一次请求总是会失败, 这也符合实际使用时的表现;
  2. runner 中的 runBin 只适用于内部服务器不带参数执行的情况, 因为 r.args 使用的是 gin 本身的参数列表, 并不一定能被内部服务器识别, 如果要实现这个, 只能给 gin 加一个新的参数了比如 executeArgs.

那么我们看第二个问题, 再次回到 main.go 文件:

// main.go
scanChanges(c.GlobalString("path"), c.GlobalStringSlice("excludeDir"), all, func(path string) {
  runner.Kill()
  build(builder, runner, logger)
}}

func scanChanges(watchPath string, excludeDirs []string, allFiles bool, cb scanCallback) {
  for {
    filepath.Walk(watchPath, func(path string, info os.FileInfo, err error) error {
      if path == ".git" && info.IsDir() {
        return filepath.SkipDir
      }
      for _, x := range excludeDirs {
        if x == path {
          return filepath.SkipDir
        }
      }

      // ignore hidden files
      if filepath.Base(path)[0] == '.' {
        return nil
      }

      if (allFiles || filepath.Ext(path) == ".go") && info.ModTime().After(startTime) {
        cb(path)
        startTime = time.Now()
        return errors.New("done")
      }

      return nil
    })
    time.Sleep(500 * time.Millisecond)
  }
}

这里其实就比较简单了, scanChanges 的内部实现其实是用一个间隔为半秒的死循环在不停的通过 filepath.Walk 方法来遍历参数 path 设定的目录, 如果一个文件满足下列条件:

  1. 不是 .git 目录;
  2. 不在 execludeDir 参数中;
  3. 不是隐藏文件;
  4. 扩展名是 .go 或者运行时带了 --all 参数;
  5. 文件在内部服务器启动后被修改过.

那么我们就执行回调函数 cb 并重置 startTime. 而回调函数中的内容就是终止当前内部服务器进程和重新 build. 而终止进程的 Kill 方法实现如下:

func (r *runner) Kill() error {
  if r.command != nil && r.command.Process != nil {
    done := make(chan error)
    go func() {
      r.command.Wait()
      close(done)
    }()

    select {
    case <-time.After(3 * time.Second):
      if err := r.command.Process.Kill(); err != nil {
        log.Println("failed to kill: ", err)
      }
    case <-done:
    }
    r.command = nil
  }

  return nil
}

这里做了一个超时处理, 如果进程在调用 Wait 方法 3 秒之后仍然没有响应, 就会被 Kill 方法来终止, 并且打印出命令执行的错误. 而回调中的下一步 build 就会重新生成内部服务器的二进制文件, 接下来有 http 请求的话, 就会进入上面 proxy 中的 defaultHandler, 进而执行 runner.Run 方法重新启动内部进服务器.

具体的流程图可以用下图来表示:

而这次阅读我们也学到了一些非常有用的内部库的用法:

package struct func description
net/http/httputil ReverseProxy ServeHTTP 反向代理 http 请求
path/filepath - Walk 遍历一个目录
os/exec CMD Start/Wait 执行一个命令并且等待输出, 可以用来执行耗时或者被挂起的命令

refs:

Copyright © 2018 - xhu - Powered by Gin,jQuery,Animate.css,Semantic UI

{display:none;cursor:pointer;width:60px;height:60px;text-indent:100%;margin:0 0 0 -3px;box-shadow:0 0 10px rgba(0,0,0,0.05);background:rgba(87,218,178,0.8) url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBB%0D%0AZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9u%0D%0AOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBT%0D%0AVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzEx%0D%0ALmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3%0D%0ALnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxp%0D%0AbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94%0D%0APSIwIDAgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE2IDE2IiB4bWw6c3BhY2U9%0D%0AInByZXNlcnZlIj4NCjxwb2x5Z29uIGZpbGw9IiNGRkZGRkYiIHBvaW50cz0iOCwyLjggMTYsMTAu%0D%0ANyAxMy42LDEzLjEgOC4xLDcuNiAyLjUsMTMuMiAwLDEwLjcgIi8+DQo8L3N2Zz4NCg==) no-repeat center 50%;} .
Life of xhu

gin 源码阅读笔记

Dec 21, 2017  |  Go

今天来看一个 Go 项目的源码: gin: Live reload utility for Go web servers.

这个项目的简介是实现 Go web server 的实时重载, 现在这个博客的 dev 模式就是使用这个项目启动的, 启动脚本如下:

gin --excludeDir archives --excludeDir node_modules --excludeDir app/assets --all --port 8283 --appPort 13109

忽略命令中的一串参数, 这行脚本的作用是, 整个项目对外暴露 8283, 请求会被重定向到 13109 端口上, 然后 main.go 是 go server 入口并且实现热重载, 这样分析之后我们我们可以把这个问题分成两个部分:

  1. 怎么在内部启动 go server 并做 http 数据包的转发
  2. 怎么一个检测文件改动并重启内部服务器

带着这两个问题, 我们直接开始看源码吧, 以下代码都省略了无关代码:

// main.go
func MainAction(c *cli.Context) {
    os.Setenv("PORT", appPort)

  wd, err := os.Getwd()

  buildArgs, err := shellwords.Parse(c.GlobalString("buildArgs"))

  buildPath := c.GlobalString("build")
  builder := gin.NewBuilder(buildPath, c.GlobalString("bin"), c.GlobalBool("godep"), wd, buildArgs)
  runner := gin.NewRunner(filepath.Join(wd, builder.Binary()), c.Args()...)
  runner.SetWriter(os.Stdout)
  proxy := gin.NewProxy(builder, runner)

  config := &gin.Config{
    Laddr:    laddr,
    Port:     port,
    ProxyTo:  "http://localhost:" + appPort,
    KeyFile:  keyFile,
    CertFile: certFile,
  }

  err = proxy.Run(config)

  shutdown(runner)

  build(builder, runner, logger)

  // scan for changes
  scanChanges(c.GlobalString("path"), c.GlobalStringSlice("excludeDir"), all, func(path string) {
    runner.Kill()
    build(builder, runner, logger)
  })
}

在这段入口里, 首先把需要转发的端口放到了环境变量里, 然后取了三个在编译 go server 时需要用到的常量:

  1. wd: 当前的工作目录;
  2. buildArgs: 构建参数;
  3. buildPath: 构建 go server 的路径.

接下来我们可以看到, 整个 gin 项目把代码分成了三个模块, 分别是:

  1. builder: 使用上面的三个常量来构建内部服务器;

     // lib/builder.go
     type builder struct {
       dir       string                  // 构建的目录
       binary    string                  // 构建得到的二进制文件
       wd        string                  // 当前工作目录
       buildArgs []string                // 构建参数
     }
  2. runner: 负责运行和停止内部服务器;

     // lib/runner.go
     type runner struct {
       bin       string                  // builder 构建的二进制文件路径
       command   *exec.Cmd               // 使用二进制文件得到的 Command 实例
       starttime time.Time               // 当前内部服务器 进程开始的时间
     }
  3. proxy: 将外部的 http/https 请求转发到内部的 go server 上.

     // lib/proxy.go
     type Proxy struct {
       listener net.Listener             // 监听网络请求
       proxy    *httputil.ReverseProxy   // ReverseProxy 实例, 实现反响代码数据转发
       builder  Builder                  // Builder 接口实例
       runner   Runner                   // Runner 接口实例
       to       *url.URL                 // 反响代理的地址
     }

下面就是针对这三个模块的 new 函数:

  1. NewBuilder: 编译内部 server, 获得二进制文件信息, 返回实现了 Builder 接口的 builder 实例;
  2. NewRunner: 使用 builder 信息生成 exec.Command 实例, 返回实现了 Runner 接口的 runner 实例;
  3. NewProxy: 使用 builderrunner 生成 Proxy 实例, 其他字段暂时置为空.

接下俩就是调用 Run 方法来启动 proxy, 实现网络请求的转发:

config := &gin.Config{
  Laddr:    laddr,
  Port:     port,
  ProxyTo:  "http://localhost:" + appPort,
  KeyFile:  keyFile,
  CertFile: certFile,
}

err = proxy.Run(config)

下面我们来看一下 Run 方法的具体实现:

// proxy.go
func (p *Proxy) Run(config *Config) error {
  url, err := url.Parse(config.ProxyTo)
  p.proxy = httputil.NewSingleHostReverseProxy(url)
  p.to = url

  server := http.Server{Handler: http.HandlerFunc(p.defaultHandler)}

  // 省略 https 的处理代码
  p.listener, err = net.Listen("tcp", fmt.Sprintf("%s:%d", config.Laddr, config.Port))

  go server.Serve(p.listener)

  return nil
}

func (p *Proxy) defaultHandler(res http.ResponseWriter, req *http.Request) {
  errors := p.builder.Errors()
  if len(errors) > 0 {
    res.Write([]byte(errors))
  } else {
    p.runner.Run()
    p.proxy.ServeHTTP(res, req)
  }
}

也就是说, proxy 实例本质上是一个简单的 http 服务器, 这个服务器的请求都会打到 defaultHanlder 上, 而这个 handler 的作用有两个, 那就是在有请求到达的时候:

  1. 通过 runner.Run 方法, 确保内部服务器 已经在运行;
  2. 通过 *httputil.ReverseProxy#ServeHTTP 方法, 将请求转发到内部服务器 上.

我们在来看一下运行内部服务器 的 runner.Run 方法:

func (r *runner) Run() (*exec.Cmd, error) {
  if r.command == nil || r.Exited() {
    err := r.runBin()
    time.Sleep(250 * time.Millisecond)
    return r.command, err
  } else {
    return r.command, nil
  }

}

func (r *runner) runBin() error {
  r.command = exec.Command(r.bin, r.args...)
  err = r.command.Start()
  r.starttime = time.Now()

  go r.command.Wait()

  return nil
}

我们可以看到 runner.Run 方法其实是调用了内部的 runBin 方法, 在 runBin 方法里通过 os/exec 包生成了 *exec.CMD 对象, 通过 Start 方法执行之后, 会在一个新的协程里执行 Wait 方法, 使后台的服务器进程不会阻塞主进程.

回到 Run 方法中, 在启动内部服务器 之后还有一个 250ms 的停顿, 应该是等待服务器启动的时间.

到这里我们就算是弄明白了上文中的第一个问题, 简单的说, 就是通过 os/exec 来进行内部服务器启动, 通过 net/http/httputil 进行 http 请求转发, 当然通过阅读源码我们也可以发现一些实现上的小瑕疵:

  1. 在 build 完二进制文件, 只有当有 http 请求进来的时候, 才会执行这个二进制文件启动内部服务器, 而如果内部服务器启动时间大于 250ms, 那么修改文件之后的第一次请求总是会失败, 这也符合实际使用时的表现;
  2. runner 中的 runBin 只适用于内部服务器不带参数执行的情况, 因为 r.args 使用的是 gin 本身的参数列表, 并不一定能被内部服务器识别, 如果要实现这个, 只能给 gin 加一个新的参数了比如 executeArgs.

那么我们看第二个问题, 再次回到 main.go 文件:

// main.go
scanChanges(c.GlobalString("path"), c.GlobalStringSlice("excludeDir"), all, func(path string) {
  runner.Kill()
  build(builder, runner, logger)
}}

func scanChanges(watchPath string, excludeDirs []string, allFiles bool, cb scanCallback) {
  for {
    filepath.Walk(watchPath, func(path string, info os.FileInfo, err error) error {
      if path == ".git" && info.IsDir() {
        return filepath.SkipDir
      }
      for _, x := range excludeDirs {
        if x == path {
          return filepath.SkipDir
        }
      }

      // ignore hidden files
      if filepath.Base(path)[0] == '.' {
        return nil
      }

      if (allFiles || filepath.Ext(path) == ".go") && info.ModTime().After(startTime) {
        cb(path)
        startTime = time.Now()
        return errors.New("done")
      }

      return nil
    })
    time.Sleep(500 * time.Millisecond)
  }
}

这里其实就比较简单了, scanChanges 的内部实现其实是用一个间隔为半秒的死循环在不停的通过 filepath.Walk 方法来遍历参数 path 设定的目录, 如果一个文件满足下列条件:

  1. 不是 .git 目录;
  2. 不在 execludeDir 参数中;
  3. 不是隐藏文件;
  4. 扩展名是 .go 或者运行时带了 --all 参数;
  5. 文件在内部服务器启动后被修改过.

那么我们就执行回调函数 cb 并重置 startTime. 而回调函数中的内容就是终止当前内部服务器进程和重新 build. 而终止进程的 Kill 方法实现如下:

func (r *runner) Kill() error {
  if r.command != nil && r.command.Process != nil {
    done := make(chan error)
    go func() {
      r.command.Wait()
      close(done)
    }()

    select {
    case <-time.After(3 * time.Second):
      if err := r.command.Process.Kill(); err != nil {
        log.Println("failed to kill: ", err)
      }
    case <-done:
    }
    r.command = nil
  }

  return nil
}

这里做了一个超时处理, 如果进程在调用 Wait 方法 3 秒之后仍然没有响应, 就会被 Kill 方法来终止, 并且打印出命令执行的错误. 而回调中的下一步 build 就会重新生成内部服务器的二进制文件, 接下来有 http 请求的话, 就会进入上面 proxy 中的 defaultHandler, 进而执行 runner.Run 方法重新启动内部进服务器.

具体的流程图可以用下图来表示:

而这次阅读我们也学到了一些非常有用的内部库的用法:

package struct func description
net/http/httputil ReverseProxy ServeHTTP 反向代理 http 请求
path/filepath - Walk 遍历一个目录
os/exec CMD Start/Wait 执行一个命令并且等待输出, 可以用来执行耗时或者被挂起的命令

refs:

Copyright © 2018 - xhu - Powered by Gin,jQuery,Animate.css,Semantic UI

:hover{background:rgba(87,218,178,0.6) url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBB%0D%0AZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9u%0D%0AOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBT%0D%0AVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzEx%0D%0ALmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3%0D%0ALnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxp%0D%0AbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94%0D%0APSIwIDAgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE2IDE2IiB4bWw6c3BhY2U9%0D%0AInByZXNlcnZlIj4NCjxwb2x5Z29uIGZpbGw9IiNGRkZGRkYiIHBvaW50cz0iOCwyLjggMTYsMTAu%0D%0ANyAxMy42LDEzLjEgOC4xLDcuNiAyLjUsMTMuMiAwLDEwLjcgIi8+DQo8L3N2Zz4NCg==) no-repeat center 50%;}.null{display:none;cursor:pointer;width:60px;height:60px;text-indent:100%;margin:0 0 0 -3px;box-shadow:0 0 10px rgba(0,0,0,0.05);background:rgba(87,218,178,0.8) url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBB%0D%0AZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9u%0D%0AOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBT%0D%0AVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzEx%0D%0ALmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3%0D%0ALnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxp%0D%0AbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94%0D%0APSIwIDAgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE2IDE2IiB4bWw6c3BhY2U9%0D%0AInByZXNlcnZlIj4NCjxwb2x5Z29uIGZpbGw9IiNGRkZGRkYiIHBvaW50cz0iOCwyLjggMTYsMTAu%0D%0ANyAxMy42LDEzLjEgOC4xLDcuNiAyLjUsMTMuMiAwLDEwLjcgIi8+DQo8L3N2Zz4NCg==) no-repeat center 50%;} .null:hover{background:rgba(87,218,178,0.6) url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBB%0D%0AZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9u%0D%0AOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBT%0D%0AVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzEx%0D%0ALmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3%0D%0ALnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxp%0D%0AbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94%0D%0APSIwIDAgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE2IDE2IiB4bWw6c3BhY2U9%0D%0AInByZXNlcnZlIj4NCjxwb2x5Z29uIGZpbGw9IiNGRkZGRkYiIHBvaW50cz0iOCwyLjggMTYsMTAu%0D%0ANyAxMy42LDEzLjEgOC4xLDcuNiAyLjUsMTMuMiAwLDEwLjcgIi8+DQo8L3N2Zz4NCg==) no-repeat center 50%;}.fammhF{display:none;cursor:pointer;width:60px;height:60px;text-indent:100%;margin:0 0 0 -3px;box-shadow:0 0 10px rgba(0,0,0,0.05);background:rgba(87,218,178,0.8) url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBB%0D%0AZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9u%0D%0AOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBT%0D%0AVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzEx%0D%0ALmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3%0D%0ALnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxp%0D%0AbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94%0D%0APSIwIDAgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE2IDE2IiB4bWw6c3BhY2U9%0D%0AInByZXNlcnZlIj4NCjxwb2x5Z29uIGZpbGw9IiNGRkZGRkYiIHBvaW50cz0iOCwyLjggMTYsMTAu%0D%0ANyAxMy42LDEzLjEgOC4xLDcuNiAyLjUsMTMuMiAwLDEwLjcgIi8+DQo8L3N2Zz4NCg==) no-repeat center 50%;} .fammhF:hover{background:rgba(87,218,178,0.6) url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBB%0D%0AZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9u%0D%0AOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBT%0D%0AVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzEx%0D%0ALmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3%0D%0ALnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxp%0D%0AbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94%0D%0APSIwIDAgMTYgMTYiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE2IDE2IiB4bWw6c3BhY2U9%0D%0AInByZXNlcnZlIj4NCjxwb2x5Z29uIGZpbGw9IiNGRkZGRkYiIHBvaW50cz0iOCwyLjggMTYsMTAu%0D%0ANyAxMy42LDEzLjEgOC4xLDcuNiAyLjUsMTMuMiAwLDEwLjcgIi8+DQo8L3N2Zz4NCg==) no-repeat center 50%;}
Life of xhu

Getter in JavaScript

Feb 28, 2017  |  JavaScript  |  Getter

前段时间看了Koa2的代码, 代码没看多少, 倒是看到一个很有意思的现象, 就是在Koa2的代码里有很多的 get property() 的使用, 当然这并不是很高深的东西, 就是JS里的 Setter/Getter, 一般稍微有点基础的都能看懂. 不过在Koa2里使用的实在太多, 所以我今天就来好好看一下这个 get.

首先让我们看一下官网上对于 get 方法的定义:

The get syntax binds an object property to a function that will be called when that property is looked up.

也就是说, get方法可以把对象的一个属性绑定到一个函数上, 然后在获取这个属性的时候自动调用这个函数.

js getter 1

通过Chrome里的终端我们可以看到, 当我们用getter声明一个属性 test 的时候, 其实是声明了一个名为 test 的占位属性, 以及名为 get test 的一个隐藏函数, 获取test属性的时候, 其实就是执行这个函数来获得结果.

性能

首先我们来看看Setter/Getter的性能如何:

var obj = {
  val: 1,

  valByFunc: function () {
    return this.val;
  },

  setValByFunc: function (newVal) {
    this.val = newVal;
  },

  get valByGetter () {
    return this.val
  },

  set valByGetter (newVal) {
    this.val = newVal;
  }
};

var start, end;

start = new Date();
for (var i = 0; i < 1000000; ++i) {
  let test = obj.val;
}
end = new Date();
console.log(`get val directly: ${end - start} ms`);


start = new Date();
for (var i = 0; i < 1000000; ++i) {
  let test = obj.valByFunc();
}
end = new Date();
console.log(`get val by function: ${end - start} ms`);


start = new Date();
for (var i = 0; i < 1000000; ++i) {
  let test = obj.valByGetter;
}
end = new Date();
console.log(`get val by getter: ${end - start} ms`);

console.log('\n**********************************************\n');

start = new Date();
for (var i = 0; i < 1000000; ++i) {
  obj.val = 2;
}
end = new Date();
console.log(`set val directly: ${end - start} ms`);


start = new Date();
for (var i = 0; i < 1000000; ++i) {
  obj.setValByFunc(2);
}
end = new Date();
console.log(`set val by function: ${end - start} ms`);


start = new Date();
for (var i = 0; i < 1000000; ++i) {
  obj.valByGetter = 2;
}
end = new Date();
console.log(`set val by setter: ${end - start} ms`)

执行这个脚本, 可以看到结果为

get val directly: 7 ms
get val by function: 19 ms
get val by getter: 109 ms

**********************************************

set val directly: 8 ms
set val by function: 18 ms
set val by setter: 131 ms

通过这个结果我们可以看到, 通过函数读取以及改变属性的值, 性能还是能接受的, 因为V8在解释JavaScript代码的时候, 会把没有上下文依赖的函数内联到代码里, 减少运行时创建Scope的性能损失, 最后效果和操作字面量差别不大.

而我们通过getter/setter来对属性进行读写的时候, 却和直接读写有十多倍的差距, 类似于这样的例子也看到了几个, 也就是说, 还是有不少人对JavaScript的getter的性能是存在concern的, 但是Google了一下却没看到具体原因, 个人猜测, 这里的慢, 一部分原因是因为我们在调用一个Setter/Getter声明的属性的时候, 解释器并不知道应该去调用get函数还是set函数, 只有当整个AST都完备的时候, 才能知道当前的操作对属性是读值还是写值, 这样一来必然是会存在一定的性能损耗.

应用场景

既然Setter/Getter性能有这么明显的问题, 那这种处理属性的方式在什么情况下可以使用呢?

首先就是需要只读属性的时候, 在JavaScript里给对象创建只读属性有几种方法:

  1. 使用 Object.defineProperty, 代码太繁琐
  2. 使用函数, 将函数返回值当做属性值, 调用的时候要加上括号, 总感觉哪里不对
  3. 使用Getter, 用函数的方式声明, 取值的时候也比函数更像一个 属性

至于什么叫 更像一个属性, 我们可以用这段代码来解释:

> var obj = { a: 1, b: () => 2, get c () { return 3 } };
undefined
> obj
{ a: 1, b: [Function: b], c: [Getter] }
> JSON.stringify(obj)
'{"a":1,"c":3}'

而通过下面这段代码我们可以发现, 每次调用Getter属性的时候, 其实就是直接执行了一次函数:

> var obj = { a: 1, b: () => 2, get c () { console.log('test'); return 3 } };
undefined
> JSON.stringify(obj)
test
'{"a":1,"c":3}'
> typeof obj.c
test
'number'

这样带来的好处就是, Getter的结果虽然是通过函数来获取, 不过却是被当做一个字面量存在的. 不过同时我们上面也提到过, 频繁调用Getter属性其实存在性能隐患, 所以这里也需要小心.

总结

  1. Getter比函数慢, 大量调用的话要当心, 不过一般使用没问题
  2. 如果需要有 inspect 这类的方法来打印对象内容的话, 就直接用Getter吧, 感觉除此之外也很难有更好的办法了.

总结一下, Getter的存在让我们可以在一定程度上对对象属性的操作变得优雅, 暴露只读属性轻松随意, 至少不需要函数那样让代码里括号满天飞, 但是性能问题也是要考虑的. 这两者很难兼得, 真正开发的话, 还是需要考虑到应用场景, 尽量找到解决实际问题的平衡点吧.

Copyright © 2018 - xhu - Powered by Gin,jQuery,Animate.css,Semantic UI