4 22

第三届 GopherChina 大会

上周末参加 GopherChina 第三届大会,感受不错。经过三年时间,Go 的发展非常火爆。会议规模从原来的几百人到上千人,还有很多站在座位两侧听的朋友。大会的内容也是从 Go 本身,到架构,到容器等相关领域都有涉及,可以说干货不错。办大会大会是一件辛苦事,非常感谢 Astaxie 一直以来的努力。讲好一个主题,也会需要很多技巧的,也非常感谢参与大会的各个讲师。言归正传,来聊聊大会的内容。

语言上 的 Go

每种语言都有自己的特色,Go 也不例外。学习 Go 的时候,难免会带入别的语言经验,造成一定的麻烦。因此从 Go 的方式来理解 Go,是 Go 语言开发者必须经历的过程。Tony Bai 从 Go 语言的角度,分享了 Go 的思维模式。

Go is about orthogonal composition of simple concepts with preference in concurrency.
Go 是在偏好并发的环境下的简单概念/事物的正交组合

从这句话就可以总结出几个 Go 开发的原则:

  • 事物的简单化,逻辑单元不要太大;不是一个函数从头到尾,是多个小函数组合起来的大函数
  • 正交组合,逻辑单元之间的无关性;因为函数之间的无关性,才可以复用,也才可以并发
  • 并发背景的需要,即业务是可以同时的,不是线性的

这一些原则还是需要很多的开发技巧来实现的,比如接口的垂直组合,如:


type ReadWriter interface {
    Reader // 这里体现了逻辑单元的简单化
    Writer
}

根据接口来更适配的接受逻辑水平扩展:


func ReadAll(r io.Reader) ([]byte, error)
// 可以支持文件流,网络数据流等
ReadAll(*os.File)
ReadAll(*http.Response.Body)

最后我比较感兴趣的就是 error 处理的内敛,比如:


// *bufio.Writer
func (b *Writer) Write(p []byte) (nn int, err error) {
    if b.err != nil {
        return nn, b.err  // 错误在结构内部
    }
    ... ...
}

// usage
buf := bufio.NewWriter(fd)
buf.WriteString("hello, ") // 这样就不需要每行 if err != nil
buf.WriteString("gopherchina ")
buf.WriteString("2017")
if err := buf.Flush() ; err != nil {
    return err
}


上面提到接口的垂直扩展,francesc 的分享就更深入的聊了 Go 中 interface{} 的使用技巧。interface{} 我的理解有两个意思。当 interface{} 带有方法的时候,是行为的定义,如:


type Reader interface{
    Read(data []byte)(n int,err error) // 注意定义的时候写一下变量名,否则不是啥意思
}

这样的接口就可以弥补 Go 没有泛型的不足。泛型的时候返回如 <List,Map> 其实是不对的,真正的意义很可能是 <Iterator>。在 Go 的编程中就逼迫你需要这样的思维来理解,找出需要泛型的时候数据类型的共通之处,执行相同的操作(如果就是不同的行为,写俩函数不是更好?)。

interface{} 另一个特殊场景就是空接口,对应的代码就是需要类型推断:

func do(v interface{}){
    switch t := v.(type){
        case int:
            fmt.Printf("int - %d",t)
        case error:
            fmt.Printf("error - %s",t.Error())
        default:
            fmt.Printf("interface - %v",t)
    }
}

不到万不得已不要这么写代码。否则需要推断类型的 case 越来越多,代码可维护性瞬间下降。


ezbuy 的分享有很多微服务相关的内容,选型 gRPC 的使用,context tracing 等问题。不过我最在意的是他们的开发环境搭建工具 Goflow。Go 的包管理机制一直为人所诟病。就算 vendor 解决了一些第三方包依赖的问题,但是非常的粗糙和直接。同时项目管理时 GOPATH 中既有第三方库,又有自己的项目代码,也给工程实践造成麻烦。Goflow 从分享中看很像 gb + package registry。Goflow 修改系统变量 GOPATH 到当前目录,再从内网 registry 下载第三方包到 vendor 目录。真的非常方便,是个不错的解决方案。而且国内网络访问很多包资源不是很顺畅,内网有 registry 提供很大的便利。这一套东西在公司内部使用我觉得非常棒。

另外他是唯一提及用 internal 包的分享。例如代码结构:

--product
  |
  |---internal
  |   |
  |   |---product_get.go
  |   |---product_search.go
  |   |
  |---product.go

代码和功能更加清晰明确。回头我也尝试一下这样的使用技巧。

10 1

Go 内嵌静态资源

使用 Go 开发应用的时候,有时会遇到需要读取静态资源的情况。比如开发 Web 应用,程序需要加载模板文件生成输出的 HTML。在程序部署的时候,除了发布应用可执行文件外,还需要发布依赖的静态资源文件。这给发布过程添加了一些麻烦。既然发布单独一个可执行文件是非常简单的操作,就有人会想办法把静态资源文件打包进 Go 的程序文件中。下面就来看一些解决方案:

go-bindata

go-bindata 是目前我的程序 pugo 在用的嵌入静态资源的工具。它可以把静态文件嵌入到一个 go 文件中,并提供一些操作方法。

安装 go-bindata

go get -u github.com/jteeuwen/go-bindata/...

注意 go get 地址最后的三个点 ...。这样会分析所有子目录并下载依赖编译子目录内容。go-bindata 的命令工具在子目录中。(还要记得把 $GOPATH/bin 加入系统 PATH)。

使用命令工具 go-bindata ( pugo 的例子):

go-bindata -o=app/asset/asset.go -pkg=asset source/... theme/... doc/source/... doc/theme/... 

-o 输出文件到 app/asset/asset.go,包名 -pkg=asset,然后是需要打包的目录,三个点包括所有子目录。这样就可以把所有相关文件打包到 asset.go 且开头是 package asset 保持和目录一致。

pugo 里释放静态文件的代码:

dirs := []string{"source", "theme", "doc"} // 设置需要释放的目录

for _, dir := range dirs {
    // 解压dir目录到当前目录
    if err := asset.RestoreAssets("./", dir); err != nil {
        isSuccess = false
        break
    }
}
if !isSuccess {
    for _, dir := range dirs {
        os.RemoveAll(filepath.Join("./", dir))
    }
}

asset.go 内的静态内容还是根据实际的目录位置索引。所以我们可以直接通过目录或者文件地址去操作。

9 24

Go 开发 HTTP 的另一个选择 fasthttp

fasthttp 是 Go 的一款不同于标准库 net/http 的 HTTP 实现。fasthttp 的性能可以达到标准库的 10 倍,说明他魔性的实现方式。主要的点在于四个方面:

  • net/http 的实现是一个连接新建一个 goroutine;fasthttp 是利用一个 worker 复用 goroutine,减轻 runtime 调度 goroutine 的压力
  • net/http 解析的请求数据很多放在 map[string]string(http.Header) 或 map[string][]string(http.Request.Form),有不必要的 []byte 到 string 的转换,是可以规避的
  • net/http 解析 HTTP 请求每次生成新的 *http.Requesthttp.ResponseWriter; fasthttp 解析 HTTP 数据到 *fasthttp.RequestCtx,然后使用 sync.Pool 复用结构实例,减少对象的数量
  • fasthttp 会延迟解析 HTTP 请求中的数据,尤其是 Body 部分。这样节省了很多不直接操作 Body 的情况的消耗

但是因为 fasthttp 的实现与标准库差距较大,所以 API 的设计完全不同。使用时既需要理解 HTTP 的处理过程,又需要注意和标准库的差别。

package main

import (
    "fmt"

    "github.com/valyala/fasthttp"
)

// RequestHandler 类型,使用 RequestCtx 传递 HTTP 的数据
func httpHandle(ctx *fasthttp.RequestCtx) {
    fmt.Fprintf(ctx, "hello fasthttp") // *RequestCtx 实现了 io.Writer
}

func main() {
    // 一定要写 httpHandle,否则会有 nil pointer 的错误,没有处理 HTTP 数据的函数
    if err := fasthttp.ListenAndServe("0.0.0.0:12345", httpHandle); err != nil {
        fmt.Println("start fasthttp fail:", err.Error())
    }
}
9 20

Go 开发 HTTP

Go 是一门新语言。很多人都是用 Go 来开发 Web 服务。Web 开发很多同学急于求成,直接使用 beego, echoiris 等知名框架。对标准库 net/http 的了解甚少。这里我就主要聊一下标准库 net/http 开发 Web 服务时的使用细节。

创建 HTTP 服务

在 Go 中,创建 HTTP 服务很简单:

package main

// in main.go

import (
    "fmt"
    "net/http"
)

func main(){
    if err := http.ListenAndServe(":12345",nil); err != nil{
        fmt.Println("start http server fail:",err)
    }
}

这样就会启动一个 HTTP 服务在端口 12345。浏览器输入 http://localhost:12345/ 就可以访问。当然从代码看出,没有给这个 HTTP 服务添加实际的处理逻辑,所有的访问都是默认的 404 Not Found

以前的