如何写出优雅的 Golang 代码

https://draveness.me/golang-101

Go 语言是一门简单、易学的编程语言,对于有编程背景的工程师来说,学习 Go 语言并写出能够运行的代码并不是一件困难的事情,对于之前有过其他语言经验的开发者来说,写什么语言都像自己学过的语言其实是有问题的,想要真正融入生态写出优雅的代码就一定要花一些时间和精力了解语言背后的设计哲学和最佳实践。

如果你之前没有 Go 语言的开发经历,正在学习和使用 Go 语言,相信这篇文章能够帮助你更快地写出优雅的 Go 语言代码;在这篇文章中,我们并不会给一个长长地列表介绍变量、方法和结构体应该怎么命名,这些 Go 语言的代码规范可以在 Go Code Review Comments 中找到,它们非常重要但并不是这篇文章想要介绍的重点,我们将从代码结构、最佳实践以及单元测试几个不同的方面介绍如何写出优雅的 Go 语言代码。

写在前面

想要写出好的代码并不是一件容易的事情,它需要我们不断地对现有的代码进行反思 — 如何改写这段代码才能让它变得更加优雅。优雅听起来是一个非常感性、难以量化的结果,然而这却是好的代码能够带来的最直观感受,它可能隐式地包含了以下特性:

  • 容易阅读和理解;
  • 容易测试、维护和扩展;
  • 命名清晰、无歧义、注释完善清楚;

相信读完了这篇文章,我们也不能立刻写出优雅的 Go 语言代码,但是如果我们遵循这里介绍几个的容易操作并且切实可行的方法,就帮助我们走出第一步,作者写这篇文章有以下的几个目的:

  • 帮助 Go 语言的开发者了解生态中的规范与工具,写出更优雅的代码;
  • 为代码和项目的管理提供被社区广泛认同的规则、共识以及最佳实践;

代码规范

代码规范其实是一个老生常态的问题,我们也不能免俗还是要简单介绍一下相关的内容,Go 语言比较常见并且使用广泛的代码规范就是官方提供的 Go Code Review Comments,无论你是短期还是长期使用 Go 语言编程,都应该至少完整地阅读一遍这个官方的代码规范指南,它既是我们在写代码时应该遵守的规则,也是在代码审查时需要注意的规范。

学习 Go 语言相关的代码规范是一件非常重要的事情,也是让我们的项目遵循统一规范的第一步,虽然阅读代码规范相关的文档非常重要,但是在实际操作时我们并不能靠工程师自觉地遵守以及经常被当做形式的代码审查,而是需要借助工具来辅助执行。

辅助工具

使用自动化的工具保证项目遵守一些最基本的代码规范是非常容易操作和有效的事情,相比之下人肉审查代码的方式更加容易出错,也会出现一些违反规则和约定的特例,维护代码规范的最好方式就是『尽量自动化一切能够自动化的步骤,让工程师审查真正重要的逻辑和设计』

我们在这一节中就会介绍两种非常切实有效的办法帮助我们在项目中自动化地进行一些代码规范检查和静态检查保证项目的质量。

goimports

goimports 是 Go 语言官方提供的工具,它能够为我们自动格式化 Go 语言代码并对所有引入的包进行管理,包括自动增删依赖的包引用、将依赖包按字母序排序并分类。相信很多人使用的 IDE 都会将另一个官方提供的工具 gofmt 对代码进行格式化,而 goimports 就是等于 gofmt 加上依赖包管理。

golang-goimports

建议所有 Go 语言的开发者都在开发时使用 goimports,虽然 goimports 有时会引入错误的包,但是与带来的好处相比,这些偶尔出现的错误在作者看来也是可以接受的;当然,不想使用 goimports 的开发者也一定要在 IDE 或者编辑器中开启自动地 gofmt(保存时自动格式化)。

在 IDE 和 CI 检查中开启自动地 gofmt 或者 goimports 检查是没有、也不应该有讨论的必要的,这就是一件使用和开发 Go 语言必须要做的事情。

golint

另一个比较常用的静态检查工具就是 golint 了,作为官方提供的工具,它在可定制化上有着非常差的支持,我们只能通过如下所示的方式运行 golint 对我们的项目进行检查:

$ golint ./pkg/...
pkg/liquidity/liquidity_pool.go:18:2: exported var ErrOrderBookNotFound should have comment or be unexported
pkg/liquidity/liquidity_pool.go:23:6: exported type LiquidityPool should have comment or be unexported
pkg/liquidity/liquidity_pool.go:23:6: type name will be used as liquidity.LiquidityPool by other packages, and that stutters; consider calling this Pool
pkg/liquidity/liquidity_pool.go:31:1: exported function NewLiquidityPool should have comment or be unexported
...

社区上有关于 golint 定制化的 讨论golint 的开发者给出了以下的几个观点解释为什么 golint 不支持定制化的功能:

  • lint 的目的就是在 Go 语言社区中鼓励统一、一致的编程风格,某些开发者也许不会同意其中的某些规范,但是使用统一的风格对于 Go 语言社区有比较强的好处,而能够开关指定规则的功能会导致 golint 不能够有效地完成这个工作;
  • 有一些静态检查的规则会导致一些错误的警告,这些情况确实非常让人头疼,但是我会选择支持在 golint 中直接保留或者删除这些规则,而不是随提供意增删规则的能力;
  • 能够通过 min_confidence 过滤一些静态检查规则,但是需要我们选择合适的值;

golint 作者的观点在 issue 中得到了非常多的 👎,但是这件事情很难说对错;在社区中保证一致的编程规范是一件非常有益的事情,不过对于很多公司内部的服务或者项目,可能在业务服务上就会发生一些比较棘手的情况,使用这种过强的约束没有太多明显地收益。

golang-lint

更推荐的方法是在基础库或者框架中使用 golint 进行静态检查(或者同时使用 golint 和 golangci-lint),在其他的项目中使用可定制化的 golangci-lint 来进行静态检查,因为在基础库和框架中施加强限制对于整体的代码质量有着更大的收益。

作者会在自己的 Go 项目中使用 golint + golangci-lint 并开启全部的检查尽量尽早发现代码中包含文档在内的全部缺陷。

自动化

无论是用于检查代码规范和依赖包的 goimports 还是静态检查工具 glint 或者 golangci-lint,只要我们在项目中引入这些工具就一定要在代码的 CI 流程中加入对应的自动化检查:

在自建的或者其他的代码托管平台上也应该想尽办法寻找合适的工具,现代的代码托管工具应该都会对 CI/CD 有着非常不错的支持;我们需要通过这些 CI 工具将代码的自动化检查变成 PR 合并和发版的一个前置条件,减少工程师 Review 代码时可能发生的疏漏。

最佳实践

我们在上一节中介绍了一些能通过自动化工具发现的问题,这一节提到的最佳实践可能就没有办法通过自动化工具进行保证,这些最佳实践更像是 Go 语言社区内部发展过程中积累的一些工程经验和共识,遵循这些最佳实践能够帮助我们写出符合 Go 语言『味道』的代码,我们将在这一小节覆盖以下的几部分内容:

  • 目录结构;
  • 模块拆分;
  • 显式调用;
  • 面向接口;

这四部分内容是在社区中相对来说比较常见的约定,如果我们学习并遵循了这些约定,同时在 Go 语言的项目中实践这几部分内容,相信一定会对我们设计 Go 语言项目有所帮助。

目录结构

目录结构基本上就是一个项目的门面,很多时候我们从目录结构中就能够看出开发者对这门语言是否有足够的经验,所以在这里首先要介绍的最佳实践就是如何在 Go 语言的项目或者服务中组织代码。

官方并没有给出一个推荐的目录划分方式,很多项目对于目录结构的划分也非常随意,这其实也是没有什么问题的,但是社区中还是有一些比较常见的约定,例如:golang-standards/project-layout 项目中就定义了一个比较标准的目录结构。

├── LICENSE.md
├── Makefile
├── README.md
├── api
├── assets
├── build
├── cmd
├── configs
├── deployments
├── docs
├── examples
├── githooks
├── init
├── internal
├── pkg
├── scripts
├── test
├── third_party
├── tools
├── vendor
├── web
└── website

我们在这里就像简单介绍其中几个比较常见并且重要的目录和文件,帮助我们快速理解如何使用如上所示的目录结构,如果各位读者想要了解使用其他目录的原因,可以从 golang-standards/project-layout 项目中的 README 了解更详细的内容。

/pkg

/pkg 目录是 Go 语言项目中非常常见的目录,我们几乎能够在所有知名的开源项目(非框架)中找到它的身影,例如:

这个目录中存放的就是项目中可以被外部应用使用的代码库,其他的项目可以直接通过 import 引入这里的代码,所以当我们将代码放入 pkg 时一定要慎重,不过如果我们开发的是 HTTP 或者 RPC 的接口服务或者公司的内部服务,将私有和公有的代码都放到 /pkg 中也没有太多的不妥,因为作为最顶层的项目来说很少会被其他应用直接依赖,当然严格遵循公有和私有代码划分是非常好的做法,作者也建议各位开发者对项目中公有和私有的代码进行妥善的划分。

私有代码

私有代码推荐放到 /internal 目录中,真正的项目代码应该写在 /internal/app 里,同时这些内部应用依赖的代码库应该在 /internal/pkg 子目录和 /pkg 中,下图展示了一个使用 /internal 目录的项目结构:

golang-internal-app-and-pkg

当我们在其他项目引入包含 internal 的依赖时,Go 语言会在编译时报错:

An import of a path containing the element “internal” is disallowed
if the importing code is outside the tree rooted at the parent of the 
"internal" directory.

这种错误只有在被引入的 internal 包不存在于当前项目树中才会发生,如果在同一个项目中引入该项目的 internal 包并不会出现这种错误。

/src

在 Go 语言的项目最不应该有的目录结构其实就是 /src 了,社区中的一些项目确实有 /src 文件夹,但是这些项目的开发者之前大多数都有 Java 的编程经验,这在 Java 和其他语言中其实是一个比较常见的代码组织方式,但是作为一个 Go 语言的开发者,我们不应该允许项目中存在 /src 目录。

最重要的原因其实是 Go 语言的项目在默认情况下都会被放置到 $GOPATH/src 目录下,这个目录中存储着我们开发和依赖的全部项目代码,如果我们在自己的项目中使用 /src 目录,该项目的 PATH 中就会出现两个 src

$GOPATH/src/github.com/draveness/project/src/code.go

上面的目录结构看起来非常奇怪,这也是我们在 Go 语言中不建议使用 /src 目录的最重要原因。

当然哪怕我们在 Go 语言的项目中使用 /src 目录也不会导致编译不通过或者其他问题,如果坚持这种做法对于项目的可用性也没有任何的影响,但是如果想让我们『看起来』更专业,还是遵循社区中既定的约定减少其他 Go 语言开发者的理解成本,这对于社区来说是一件好事。

平铺

另一种在 Go 语言中组织代码的方式就是项目的根目录下放项目的代码,这种方式在很多框架或者库中非常常见,如果想要引入一个使用 pkg 目录结构的框架时,我们往往需要使用 github.com/draveness/project/pkg/somepkg,当代码都平铺在项目的根目录时只需要使用 github.com/draveness/project,很明显地减少了引用依赖包语句的长度。

所以对于一个 Go 语言的框架或者库,将代码平铺在根目录下也很正常,但是在一个 Go 语言的服务中使用这种代码组织方法可能就没有那么合适了。

/cmd

/cmd 目录中存储的都是当前项目中的可执行文件,该目录下的每一个子目录都应该包含我们希望有的可执行文件,如果我们的项目是一个 grpc 服务的话,可能在 /cmd/server/main.go 中就包含了启动服务进程的代码,编译后生成的可执行文件就是 server

我们不应该在 /cmd 目录中放置太多的代码,我们应该将公有代码放置到 /pkg 中并将私有代码放置到 /internal 中并在 /cmd 中引入这些包,保证 main 函数中的代码尽可能简单和少。

/api

/api 目录中存放的就是当前项目对外提供的各种不同类型的 API 接口定义文件了,其中可能包含类似 /api/protobuf-spec/api/thrift-spec 或者 /api/http-spec 的目录,这些目录中包含了当前项目对外提供的和依赖的所有 API 文件:

$ tree ./api
api
└── protobuf-spec
    └── oceanbookpb
        ├── oceanbook.pb.go
        └── oceanbook.proto

二级目录的主要作用就是在一个项目同时提供了多种不同的访问方式时,用这种办法避免可能存在的潜在冲突问题,也可以让项目结构的组织更加清晰。

Makefile

最后要介绍的 Makefile 文件也非常值得被关注,在任何一个项目中都会存在一些需要运行的脚本,这些脚本文件应该被放到 /scripts 目录中并由 Makefile 触发,将这些经常需要运行的命令固化成脚本减少『祖传命令』的出现。

小结

总的来说,每一个项目都应该按照固定的组织方式进行实现,这种约定虽然并不是强制的,但是无论是组内、公司内还是整个 Go 语言社区中,只要达成了一致,对于其他工程师快速梳理和理解项目都是很有帮助的。

这一节介绍的 Go 语言项目的组织方式也并不是强制要求的,这只是 Go 语言社区中经常出现的项目组织方式,一个大型项目在使用这种目录结构时也会对其进行微调,不过这种组织方式确实更为常见并且合理。

模块拆分

我们既然已经介绍过了如何从顶层对项目的结构进行组织,接下来就会深入到项目的内部介绍 Go 语言对模块的一些拆分方法。

Go 语言的一些顶层设计最终导致了它在划分模块上与其他的编程语言有着非常明显的不同,很多其他语言的 Web 框架都采用 MVC 的架构模式,例如 Rails 和 Spring MVC,Go 语言对模块划分的方法就与 Ruby 和 Java 完全不同。

按层拆分

无论是 Java 还是 Ruby,它们最著名的框架都深受 MVC 架构模式 的影响,我们从 Spring MVC 的名字中就能体会到 MVC 对它的影响,而 Ruby 社区的 Rails 框架也与 MVC 的关系非常紧密,这是一种 Web 框架的最常见架构方式,将服务中的不同组件分成了 Model、View 和 Controller 三层。

divide-by-laye

这种模块拆分的方式其实就是按照层级进行拆分,Rails 脚手架默认生成的代码其实就是将这三层不同的源文件放在对应的目录下:modelsviews 和 controllers,我们通过 rails new example 生成一个新的 Rails 项目后可以看到其中的目录结构:

$ tree -L 2 app
app
├── controllers
│   ├── application_controller.rb
│   └── concerns
├── models
│   ├── application_record.rb
│   └── concerns
└── views
    └── layouts

而很多 Spring MVC 的项目中也会出现类似 modeldaoview 的目录,这种按层拆分模块的设计其实有以下的几方面原因:

  1. MVC 架构模式 — MVC 本身就强调了按层划分职责的设计,所以遵循该模式设计的框架自然有着一脉相承的思路;
  2. 扁平的命名空间 — 无论是 Spring MVC 还是 Rails,同一个项目中命名空间非常扁平,跨文件夹使用其他文件夹中定义的类或者方法不需要引入新的包,使用其他文件定义的类时也不需要增加额外的前缀,多个文件定义的类被『合并』到了同一个命名空间中;
  3. 单体服务的场景 — Spring MVC 和 Rails 刚出现时,SOA 和微服务架构还不像今天这么普遍,绝大多数的场景也不需要通过拆分服务;

上面的几个原因共同决定了 Spring MVC 和 Rails 会出现 modelsviews 和 controllers 的目录并按照层级的方式对模块进行拆分。

按职责拆分

Go 语言在拆分模块时就使用了完全不同的思路,虽然 MVC 架构模式是在我们写 Web 服务时无法避开的,但是相比于横向地切分不同的层级,Go 语言的项目往往都按照职责对模块进行拆分:

divide-by-responsibility

对于一个比较常见的博客系统,使用 Go 语言的项目会按照不同的职责将其纵向拆分成 postusercomment三个模块,每一个模块都对外提供相应的功能,post 模块中就包含相关的模型和视图定义以及用于处理 API 请求的控制器(或者服务):

$ tree pkg
pkg
├── comment
├── post
│   ├── handler.go
│   └── post.go
└── user

Go 语言项目中的每一个文件目录都代表着一个独立的命名空间,也就是一个单独的包,当我们想要引用其他文件夹的目录时,首先需要使用 import 关键字引入相应的文件目录,再通过 pkg.xxx 的形式引用其他目录定义的结构体、函数或者常量,如果我们在 Go 语言中使用 modelview 和 controller 来划分层级,你会在其他的模块中看到非常多的 model.Postmodel.Comment 和 view.PostView

这种划分层级的方法在 Go 语言中会显得非常冗余,并且如果对项目依赖包的管理不够谨慎时,很容易发生引用循环,出现这些问题的最根本原因其实也非常简单:

  1. Go 语言对同一个项目中不同目录的命名空间做了隔离,整个项目中定义的类和方法并不是在同一个命名空间下的,这也就需要工程师自己维护不同包之间的依赖关系;
  2. 按照职责垂直拆分的方式在单体服务遇到瓶颈时非常容易对微服务进行拆分,我们可以直接将一个负责独立功能的 package 拆出去,对这部分性能热点单独进行扩容;

小结

项目是按照层级还是按照职责对模块进行拆分其实并没有绝对的好与不好,语言和框架层面的设计最终决定了我们应该采用哪种方式对项目和代码进行组织。

Java 和 Ruby 这些语言在框架中往往采用水平拆分的方式划分不同层级的职责,而 Go 语言项目的最佳实践就是按照职责对模块进行垂直拆分,将代码按照功能的方式分到多个 package 中,这并不是说 Go 语言中不存在模块的水平拆分,只是因为 package 作为一个 Go 语言访问控制的最小粒度,所以我们应该遵循顶层的设计使用这种方式构建高内聚的模块。

显式与隐式

从开始学习、使用 Go 语言到参与社区上一些开源的 Golang 项目,作者发现 Go 语言社区对于显式的初始化、方法调用和错误处理非常推崇,类似 Spring Boot 和 Rails 的框架其实都广泛地采纳了『约定优于配置』的中心思想,简化了开发者和工程师的工作量。

然而 Go 语言社区虽然达成了很多的共识与约定,但是从语言的设计以及工具上的使用我们就能发现显式地调用方法和错误处理是被鼓励的。

init

我们在这里先以一个非常常见的函数 init 为例,介绍 Go 语言社区对显式调用的推崇;相信很多人都在一些 package 中阅读过这样的代码:

var grpcClient *grpc.Client

func init() {
    var err error
    grpcClient, err = grpc.Dial(...)
    if err != nil {
        panic(err)
    }
}

func GetPost(postID int64) (*Post, error) {
    post, err := grpcClient.FindPost(context.Background(), &pb.FindPostRequest{PostID: postID})
    if err != nil {
        return nil, err
    }
    
    return post, nil
}

这种代码虽然能够通过编译并且正常工作,然而这里的 init 函数其实隐式地初始化了 grpc 的连接资源,如果另一个 package 依赖了当前的包,那么引入这个依赖的工程师可能会在遇到错误时非常困惑,因为在 init 函数中做这种资源的初始化是非常耗时并且容易出现问题的。

一种更加合理的做法其实是这样的,首先我们定义一个新的 Client 结构体以及一个用于初始化结构的 NewClient 函数,这个函数接收了一个 grpc 连接作为入参返回一个用于获取 Post 资源的客户端,GetPost 成为了这个结构体的方法,每当我们调用 client.GetPost 时都会用到结构体中保存的 grpc 连接:

// pkg/post/client.go
type Client struct {
    grpcClient *grpc.ClientConn    
}

func NewClient(grpcClient *grpcClientConn) Client {
    return &Client{
        grpcClient: grpcClient,
    }
}

func (c *Client) GetPost(postID int64) (*Post, error) {
    post, err := c.grpcClient.FindPost(context.Background(), &pb.FindPostRequest{PostID: postID})
    if err != nil {
        return nil, err
    }
    
    return post, nil
}

初始化 grpc 连接的代码应该放到 main 函数或者 main 函数调用的其他函数中执行,如果我们在 main 函数中显式的初始化这种依赖,对于其他的工程师来说就非常易于理解,我们从 main 函数开始就能梳理出程序启动的整个过程。

// cmd/grpc/main.go
func main() {
    grpcClient, err := grpc.Dial(...)
    if err != nil {
        panic(err)
    }
    
    postClient := post.NewClient(grpcClient)
    // ...
}

各个模块之间会构成一种树形的结构和依赖关系,上层的模块会持有下层模块中的接口或者结构体,不会存在孤立的、不被引用的对象。

golang-project-and-tree-structure

上图中出现的两个 Database 其实是在 main 函数中初始化的数据库连接,在项目运行期间,它们可能表示同一个内存中的数据库连接

当我们使用 golangci-lint 并开启 gochecknoinits 和 gochecknoglobals 静态检查时,它其实严格地限制我们对 init 函数和全局变量的使用。

当然这并不是说我们一定不能使用 init 函数,作为 Go 语言赋予开发者的能力,因为它能在包被引入时隐式地执行了一些代码,所以我们更应该慎重地使用它们。

一些框架会在 init 中判断是否满足使用的前置条件,但是对于很多的 Web 或者 API 服务来说,大量使用 init 往往意味着代码质量的下降以及不合理的设计。

func init() {
    if user == "" {
        log.Fatal("$USER not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath may be overridden by --gopath flag on command line.
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

上述代码其实是 Effective Go 在介绍 init 方法使用是展示的实例代码,这是一个比较合理地 init 函数使用示例,我们不应该在 init 中做过重的初始化逻辑,而是做一些简单、轻量的前置条件判断。

error

另一个要介绍的就是 Go 语言的错误处理机制了,虽然 Golang 的错误处理被开发者诟病已久,但是工程师每天都在写 if err != nil { return nil, err } 的错误处理逻辑其实就是在显式地对错误处理,关注所有可能会发生错误的方法调用并在无法处理时抛给上层模块。

func ListPosts(...) ([]Post, error) {
    conn, err := gorm.Open(...)
    if err != nil {
        return []Post{}, err
    }
    
    var posts []Post
    if err := conn.Find(&posts).Error; err != nil {
        return []Post{}, err
    }
    
    return posts, nil
}

上述代码只是简单展示 Go 语言常见的错误处理逻辑,我们不应该在这种方法中初始化数据库的连接。

虽然 Golang 中也有类似 Java 或者 Ruby try/catch 关键字,但是很少有人会在代码中使用 panic 和 recover 来实现错误和异常的处理,与 init 函数一样,Go 语言对于 panic 和 recover 的使用也非常谨慎。

当我们在 Go 语言中处理错误相关的逻辑时,最重要的其实就是以下几点:

  1. 使用 error 实现错误处理 — 尽管这看起来非常啰嗦;
  2. 将错误抛给上层处理 — 对于一个方法是否需要返回 error 也需要我们仔细地思考,向上抛出错误时可以通过 errors.Wrap 携带一些额外的信息方便上层进行判断;
  3. 处理所有可能返回的错误 — 所有可能返回错误的地方最终一定会返回错误,考虑全面才能帮助我们构建更加健壮的项目;

小结

作者在使用 Go 语言的这段时间,能够深刻地体会到它对于显式方法调用与错误处理的鼓励,这不仅能够帮助项目的其他开发者快速地理解上下文,也能够帮助我们构建更加健壮、容错性与可维护性更好的工程。

面向接口

面向接口编程是一个老生常谈的话题,接口 的作用其实就是为不同层级的模块提供了一个定义好的中间层,上游不再需要依赖下游的具体实现,充分地对上下游进行了解耦。

golang-interface

这种编程方式不仅是在 Go 语言中是被推荐的,在几乎所有的编程语言中,我们都会推荐这种编程的方式,它为我们的程序提供了非常强的灵活性,想要构建一个稳定、健壮的 Go 语言项目,不使用接口是完全无法做到的。

如果一个略有规模的项目中没有出现任何 type ... interface 的定义,那么作者可以推测出这在很大的概率上是一个工程质量堪忧并且没有多少单元测试覆盖的项目,我们确实需要认真考虑一下如何使用接口对项目进行重构。

单元测试是一个项目保证工程质量最有效并且投资回报率最高的方法之一,作为静态语言的 Golang,想要写出覆盖率足够(最少覆盖核心逻辑)的单元测试本身就比较困难,因为我们不能像动态语言一样随意修改函数和方法的行为,而接口就成了我们的救命稻草,写出抽象良好的接口并通过接口隔离依赖能够帮助我们有效地提升项目的质量和可测试性,我们会在下一节中详细介绍如何写单元测试。

package post

var client *grpc.ClientConn

func init() {
    var err error
    client, err = grpc.Dial(...)
    if err != nil {
        panic(err)
    }
}

func ListPosts() ([]*Post, error) {
    posts, err := client.ListPosts(...)
    if err != nil {
        return []*Post{}, err
    }
    
    return posts, nil
}

上述代码其实就不是一个设计良好的代码,它不仅在 init 函数中隐式地初始化了 grpc 连接这种全局变量,而且没有将 ListPosts 通过接口的方式暴露出去,这会让依赖 ListPosts 的上层模块难以测试。

我们可以使用下面的代码改写原有的逻辑,使得同样地逻辑变得更容易测试和维护:

package post

type Service interface {
    ListPosts() ([]*Post, error)
}

type service struct {
    conn *grpc.ClientConn
}

func NewService(conn *grpc.ClientConn) Service {
    return &service{
        conn: conn,
    }
}

func (s *service) ListPosts() ([]*Post, error) {
    posts, err := s.conn.ListPosts(...)
    if err != nil {
        return []*Post{}, err
    }
    
    return posts, nil
}
  1. 通过接口 Service 暴露对外的 ListPosts 方法;
  2. 使用 NewService 函数初始化 Service 接口的实现并通过私有的结构体 service 持有 grpc 连接;
  3. ListPosts 不再依赖全局变量,而是依赖接口体 service 持有的连接;

当我们使用这种方式重构代码之后,就可以在 main 函数中显式的初始化 grpc 连接、创建 Service 接口的实现并调用 ListPosts 方法:

package main

import ...

func main() {
    conn, err = grpc.Dial(...)
    if err != nil {
        panic(err)
    }
    
    svc := post.NewService(conn)
    posts, err := svc.ListPosts()
    if err != nil {
        panic(err)
    }
    
    fmt.Println(posts)
}

这种使用接口组织代码的方式在 Go 语言中非常常见,我们应该在代码中尽可能地使用这种思想和模式对外提供功能:

  1. 使用大写的 Service 对外暴露方法;
  2. 使用小写的 service 实现接口中定义的方法;
  3. 通过 NewService 函数初始化 Service 接口;

当我们使用上述方法组织代码之后,其实就对不同模块的依赖进行了解耦,也正遵循了软件设计中经常被提到的一句话 — 『依赖接口,不要依赖实现』,也就是面向接口编程

小结

在这一小节中总共介绍了 Go 语言中三个经常会打交道的『元素』— init 函数、error 和接口,我们在这里主要是想通过三个不同的例子为大家传达的一个主要思想就是尽量使用显式的(explicit)的方式编写 Go 语言代码。

单元测试

一个代码质量和工程质量有保证的项目一定有比较合理的单元测试覆盖率,没有单元测试的项目一定是不合格的或者不重要的,单元测试应该是所有项目都必须有的代码,每一个单元测试都表示一个可能发生的情况,单元测试就是业务逻辑

作为软件工程师,重构现有的项目对于我们来说应该是一件比较正常的事情,如果项目中没有单元测试,我们很难在不改变已有业务逻辑的情况对项目进行重构,一些业务的边界情况很可能会在重构的过程中丢失,当时参与相应 case 开发的工程师可能已经不在团队中,而项目相关的文档可能也消失在了归档的 wiki 中(更多的项目可能完全没有文档),我们能够在重构中相信的东西其实只有当前的代码逻辑(很可能是错误的)以及单元测试(很可能是没有的)。

简单总结一下,单元测试的缺失不仅会意味着较低的工程质量,而且意味着重构的难以进行,一个有单元测试的项目尚且不能够保证重构前后的逻辑完全相同,一个没有单元测试的项目很可能本身的项目质量就堪忧,更不用说如何在不丢失业务逻辑的情况下进行重构了

可测试

写代码并不是一件多困难的事情,不过想要在项目中写出可以测试的代码并不容易,而优雅的代码一定是可以测试的,我们在这一节中需要讨论的就是什么样的代码是可以测试的。

如果想要想清楚什么样的才是可测试的,我们首先要知道测试是什么?作者对于测试的理解就是控制变量,在我们隔离了待测试方法中一些依赖之后,当函数的入参确定时,就应该得到期望的返回值。

golang-unit-test

如何控制待测试方法中依赖的模块是写单元测试时至关重要的,控制依赖也就是对目标函数的依赖进行 Mock 消灭不确定性,为了减少每一个单元测试的复杂度,我们需要:

  1. 尽可能减少目标方法的依赖,让目标方法只依赖必要的模块;
  2. 依赖的模块也应该非常容易地进行 Mock

单元测试的执行不应该依赖于任何的外部模块,无论是调用外部的 HTTP 请求还是数据库中的数据,我们都应该想尽办法模拟可能出现的情况,因为单元测试不是集成测试的,它的运行不应该依赖除项目代码外的其他任何系统。

接口

在 Go 语言中如果我们完全不使用接口,是写不出易于测试的代码的,作为静态语言的 Golang,只有我们使用接口才能脱离依赖具体实现的窘境,接口的使用能够为我们带来更清晰的抽象,帮助我们思考如何对代码进行设计,也能让我们更方便地对依赖进行 Mock

我们再来回顾一下上一节对接口进行介绍时展示的常见模式:

type Service interface { ... }

type service struct { ... }

func NewService(...) (Service, error) {
    return &service{...}, nil
}

上述代码在 Go 语言中是非常常见的,如果你不知道应不应该使用接口对外提供服务,这时就应该无脑地使用上述模式对外暴露方法了,这种模式可以在绝大多数的场景下工作,至少作者到目前还没有见到过不适用的。

函数简单

另一个建议就是保证每一个函数尽可能简单,这里的简单不止是指功能上的简单、单一,还意味着函数容易理解并且命名能够自解释。

一些语言的 lint 工具其实会对函数的理解复杂度(PerceivedComplexity)进行检查,也就是检查函数中出现的 if/elseswitch/case 分支以及方法的调用的数量,一旦超过约定的阈值就会报错,Ruby 社区中的 Rubocop 和上面提到的 golangci-lint 都有这个功能。

Ruby 社区中的 Rubocop 对于函数的长度和理解复杂度都有着非常严格的限制,在默认情况下函数的行数不能超过 10 行,理解复杂度也不能超过 7,除此之外,Rubocop 其实还有其他的复杂度限制,例如循环复杂度(CyclomaticComplexity),这些复杂度的限制都是为了保证函数的简单和容易理解。

组织方式

如何对测试进行组织也是一个值得讨论的话题,Golang 中的单元测试文件和代码都是与源代码放在同一个目录下按照 package 进行组织的,server.go 文件对应的测试代码应该放在同一目录下的 server_test.go 文件中。

如果文件不是以 _test.go 结尾,当我们运行 go test ./pkg 时就不会找到该文件中的测试用例,其中的代码也就不会被执行,这也是 Go 语言对于测试组织方法的一个约定。

Test

单元测试的最常见以及默认组织方式就是写在以 _test.go 结尾的文件中,所有的测试方法也都是以 Test 开头并且只接受一个 testing.T 类型的参数:

func TestAuthor(t *testing.T) {
    author := blog.Author()
    assert.Equal(t, "draveness", author)
}

如果我们要给函数名为 Add 的方法写单元测试,那么对应的测试方法一般会被写成 TestAdd,为了同时测试多个分支的内容,我们可以通过以下的方式组织 Add 函数相关的测试:

func TestAdd(t *testing.T) {
    assert.Equal(t, 5, Add(2, 3))
}

func TestAddWithNegativeNumber(t *testing.T) {
    assert.Equal(t, -2, Add(-1, -1))
}

除了这种将一个函数相关的测试分散到多个 Test 方法之外,我们可以使用 for 循环来减少重复的测试代码,这在逻辑比较复杂的测试中会非常好用,能够减少大量的重复代码,不过也需要我们小心地进行设计:

func TestAdd(t *testing.T) {
    tests := []struct{
        name     string
        first    int64
        second   int64
        expected int64
    } {
        {
            name:     "HappyPath":
            first:    2,
            second:   3,
            expected: 5,
        },
        {
            name:     "NegativeNumber":
            first:    -1,
            second:   -1,
            expected: -2,
        },
    }
    
    for _, test := range tests {
        t.Run(test.name, func(t *testing.T) {
            assert.Equal(t, test.expected, Add(test.first, test.second))
        })
    }
}

这种方式其实也能生成树形的测试结果,将 Add 相关的测试分成一组方便我们进行观察和理解,不过这种测试组织方法需要我们保证测试代码的通用性,当函数依赖的上下文较多时往往需要我们写很多的 if/else 条件判断语句影响我们对测试的快速理解。

作者通常会在测试代码比较简单时使用第一种组织方式,而在依赖较多、函数功能较为复杂时使用第二种方式,不过这也不是定论,我们需要根据实际情况决定如何对测试进行设计。

Suite

第二种比较常见的方式是按照簇进行组织,其实就是对 Go 语言默认的测试方式进行简单的封装,我们可以使用 stretchr/testify 中的 suite 包对测试进行组织:

import (
    "testing"
    "github.com/stretchr/testify/suite"
)

type ExampleTestSuite struct {
    suite.Suite
    VariableThatShouldStartAtFive int
}

func (suite *ExampleTestSuite) SetupTest() {
    suite.VariableThatShouldStartAtFive = 5
}

func (suite *ExampleTestSuite) TestExample() {
    suite.Equal(suite.VariableThatShouldStartAtFive, 5)
}

func TestExampleTestSuite(t *testing.T) {
    suite.Run(t, new(ExampleTestSuite))
}

我们可以使用 suite 包,以结构体的方式对测试簇进行组织,suite 提供的 SetupTest/SetupSuite 和 TearDownTest/TearDownSuite 是执行测试前后以及执行测试簇前后的钩子方法,我们能在其中完成一些共享资源的初始化,减少测试中的初始化代码。

BDD

最后一种组织代码的方式就是使用 BDD 的风格对单元测试进行组织,ginkgo 就是 Golang 社区最常见的 BDD 框架了,这里提到的行为驱动开发(BDD)和测试驱动开发(TDD)都是一种保证工程质量的方法论。想要在项目中实践这种思想还是需要一些思维上的转变和适应,也就是先通过写单元测试或者行为测试约定方法的 Spec,再实现方法让我们的测试通过,这是一种比较科学的方法,它能为我们带来比较强的信心。

我们虽然不一定要使用 BDD/TDD 的思想对项目进行开发,但是却可以使用 BDD 的风格方式组织非常易读的测试代码:

var _ = Describe("Book", func() {
    var (
        book Book
        err error
    )

    BeforeEach(func() {
        book, err = NewBookFromJSON(`{
            "title":"Les Miserables",
            "author":"Victor Hugo",
            "pages":1488
        }`)
    })

    Describe("loading from JSON", func() {
        Context("when the JSON fails to parse", func() {
            BeforeEach(func() {
                book, err = NewBookFromJSON(`{
                    "title":"Les Miserables",
                    "author":"Victor Hugo",
                    "pages":1488oops
                }`)
            })

            It("should return the zero-value for the book", func() {
                Expect(book).To(BeZero())
            })

            It("should error", func() {
                Expect(err).To(HaveOccurred())
            })
        })
    })
})

BDD 框架中一般都包含 DescribeContext 以及 It 等代码块,其中 Describe 的作用是描述代码的独立行为、Context 是在一个独立行为中的多个不同上下文,最后的 It 用于描述期望的行为,这些代码块最终都构成了类似『描述……,当……时,它应该……』的句式帮助我们快速地理解测试代码。

Mock 方法

项目中的单元测试应该是稳定的并且不依赖任何的外部项目,它只是对项目中函数和方法的测试,所以我们需要在单元测试中对所有的第三方的不稳定依赖进行 Mock,也就是模拟这些第三方服务的接口;除此之外,为了简化一次单元测试的上下文,在同一个项目中我们也会对其他模块进行 Mock,模拟这些依赖模块的返回值。

单元测试的核心就是隔离依赖并验证输入和输出的正确性,Go 语言作为一个静态语言提供了比较少的运行时特性,这也让我们在 Go 语言中 Mock 依赖变得非常困难。

Mock 的主要作用就是保证待测试方法依赖的上下文固定,在这时无论我们对当前方法运行多少次单元测试,如果业务逻辑不改变,它都应该返回完全相同的结果,在具体介绍 Mock 的不同方法之前,我们首先要清楚一些常见的依赖,一个函数或者方法的常见依赖可以有以下几种:

  1. 接口
  2. 数据库
  3. HTTP 请求
  4. Redis、缓存以及其他依赖

这些不同的场景基本涵盖了写单元测试时会遇到的情况,我们会在接下来的内容中分别介绍如何处理以上几种不同的依赖。

接口

首先要介绍的其实就是 Go 语言中最常见也是最通用的 Mock 方法,也就是能够对接口进行 Mock 的 golang/mock 框架,它能够根据接口生成 Mock 实现,假设我们有以下代码:

package blog

type Post struct {}

type Blog interface {
	ListPosts() []Post
}

type jekyll struct {}

func (b *jekyll) ListPosts() []Post {
 	return []Post{}
}

type wordpress struct{}

func (b *wordpress) ListPosts() []Post {
	return []Post{}
}

我们的博客可能使用 jekyll 或者 wordpress 作为引擎,但是它们都会提供 ListsPosts 方法用于返回全部的文章列表,在这时我们就需要定义一个 Post 接口,接口要求遵循 Blog 的结构体必须实现 ListPosts 方法。

golang-interface-blog-example

当我们定义好了 Blog 接口之后,上层 Service 就不再需要依赖某个具体的博客引擎实现了,只需要依赖 Blog接口就可以完成对文章的批量获取功能:

package service

type Service interface {
	ListPosts() ([]Post, error)
}

type service struct {
    blog blog.Blog
}

func NewService(b blog.Blog) *Service {
    return &service{
        blog: b,
    }
}

func (s *service) ListPosts() ([]Post, error) {
    return s.blog.ListPosts(), nil
}

如果我们想要对 Service 进行测试,我们就可以使用 gomock 提供的 mockgen 工具命令生成 MockBlog 结构体,使用如下所示的命令:

$ mockgen -package=mblog -source=pkg/blog/blog.go > test/mocks/blog/blog.go

$ cat test/mocks/blog/blog.go
// Code generated by MockGen. DO NOT EDIT.
// Source: blog.go

// Package mblog is a generated GoMock package.
...
// NewMockBlog creates a new mock instance
func NewMockBlog(ctrl *gomock.Controller) *MockBlog {
	mock := &MockBlog{ctrl: ctrl}
	mock.recorder = &MockBlogMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockBlog) EXPECT() *MockBlogMockRecorder {
	return m.recorder
}

// ListPosts mocks base method
func (m *MockBlog) ListPosts() []Post {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ListPosts")
	ret0, _ := ret[0].([]Post)
	return ret0
}

// ListPosts indicates an expected call of ListPosts
func (mr *MockBlogMockRecorder) ListPosts() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPosts", reflect.TypeOf((*MockBlog)(nil).ListPosts))
}

这段 mockgen 生成的代码非常长的,所以我们只展示了其中的一部分,它的功能就是帮助我们验证任意接口的输入参数并且模拟接口的返回值;而在生成 Mock 实现的过程中,作者总结了一些可以分享的经验:

  1. 在 test/mocks 目录中放置所有的 Mock 实现,子目录与接口所在文件的二级目录相同,在这里源文件的位置在 pkg/blog/blog.go,它的二级目录就是 blog/,所以对应的 Mock 实现会被生成到 test/mocks/blog/ 目录中;
  2. 指定 package 为 mxxx,默认的 mock_xxx 看起来非常冗余,上述 blog 包对应的 Mock 包也就是 mblog
  3. mockgen 命令放置到 Makefile 中的 mock 下统一管理,减少祖传命令的出现; mock: rm -rf test/mocks mkdir -p test/mocks/blog mockgen -package=mblog -source=pkg/blog/blog.go > test/mocks/blog/blog.go

当我们生成了上述的 Mock 实现代码之后,就可以使用如下的方式为 Service 写单元测试了,这段代码通过 NewMockBlog 生成一个 Blog 接口的 Mock 实现,然后通过 EXPECT 方法控制该实现会在调用 ListPosts 时返回空的 Post 数组:


func TestListPosts(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

 	mockBlog := mblog.NewMockBlog(ctrl)
 	mockBlog.EXPECT().ListPosts().Return([]Post{})
  
 	service := NewService(mockBlog)
  
 	assert.Equal(t, []Post{}, service.ListPosts())
}

由于当前 Service 只依赖于 Blog 的实现,所以在这时我们就能够断言当前方法一定会返回 []Post{},这时我们的方法的返回值就只与传入的参数有关(虽然 ListPosts 方法没有入参),我们能够减少一次关注的上下文并保证测试的稳定和可信。

这是 Go 语言中最标准的单元测试写法,所有依赖的 package 无论是项目内外都应该使用这种方式处理(在有接口的情况下),如果没有接口 Go 语言的单元测试就会非常难写,这也是为什么从项目中是否有接口就能判断工程质量的原因了。

SQL

另一个项目中比较常见的依赖其实就是数据库,在遇到数据库的依赖时,我们一般都会使用 sqlmock 来模拟数据库的连接,当我们使用 sqlmock 时会写出如下所示的单元测试:

func (s *suiteServerTester) TestRemovePost() {
	entry := pb.Post{
		Id: 1,
	}

	rows := sqlmock.NewRows([]string{"id", "author"}).AddRow(1, "draveness")

	s.Mock.ExpectQuery(`SELECT (.+) FROM "posts"`).WillReturnRows(rows)
	s.Mock.ExpectExec(`DELETE FROM "posts"`).
		WithArgs(1).
		WillReturnResult(sqlmock.NewResult(1, 1))

	response, err := s.server.RemovePost(context.Background(), &entry)

	s.NoError(err)
	s.EqualValues(response, &entry)
	s.NoError(s.Mock.ExpectationsWereMet())
}

最常用的几个方法就是 ExpectQuery 和 ExpectExec,前者主要用于模拟 SQL 的查询语句,后者用于模拟 SQL 的增删,从上面的实例中我们可以看到这个这两种方法的使用方式,建议各位先阅读相关的 文档 再尝试使用。

HTTP

HTTP 请求也是我们在项目中经常会遇到的依赖,httpmock 就是一个用于 Mock 所有 HTTP 依赖的包,它使用模式匹配的方式匹配 HTTP 请求的 URL,在匹配到特定的请求时就会返回预先设置好的响应。

func TestFetchArticles(t *testing.T) {
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()

	httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles",
		httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Article"}]`))

	httpmock.RegisterResponder("GET", `=~^https://api\.mybiz\.com/articles/id/\d+\z`,
		httpmock.NewStringResponder(200, `{"id": 1, "name": "My Great Article"}`))

	...
}

如果遇到 HTTP 请求的依赖时,就可以使用上述 httpmock 包模拟依赖的 HTTP 请求。

猴子补丁

最后要介绍的猴子补丁其实就是一个大杀器了,bouk/monkey 能够通过替换函数指针的方式修改任意函数的实现,所以如果上述的几种方法都不能满足我们的需求,我们就只能够通过猴子补丁这种比较 hack 的方法 Mock 依赖了:

func main() {
	monkey.Patch(fmt.Println, func(a ...interface{}) (n int, err error) {
		s := make([]interface{}, len(a))
		for i, v := range a {
			s[i] = strings.Replace(fmt.Sprint(v), "hell", "*bleep*", -1)
		}
		return fmt.Fprintln(os.Stdout, s...)
	})
	fmt.Println("what the hell?") // what the *bleep*?
}

然而这种方法的使用其实有一些限制,由于它是在运行时替换了函数的指针,所以如果遇到一些简单的函数,例如 rand.Int63n 和 time.Now,编译器可能会直接将这种函数内联到调用实际发生的代码处并不会调用原有的方法,所以使用这种方式往往需要我们在测试时额外指定 -gcflags=-l 禁止编译器的内联优化。

$ go test -gcflags=-l ./...

bouk/monkey 的 README 对于它的使用给出了一些注意事项,除了内联编译之外,我们需要注意的是不要在单元测试之外的地方使用猴子补丁,我们应该只在必要的时候使用这种方法,例如依赖的第三方库没有提供 interface 或者修改 time.Now 以及 rand.Int63n 等内置函数的返回值用于测试时。

从理论上来说,通过猴子补丁这种方式我们能够在运行时 Mock Go 语言中的一切函数,这也为我们提供了单元测试 Mock 依赖的最终解决方案。

断言

在最后,我们简单介绍一下辅助单元测试的 assert 包,它提供了非常多的断言方法帮助我们快速对期望的返回值进行测试,减少我们的工作量:

func TestSomething(t *testing.T) {
  assert.Equal(t, 123, 123, "they should be equal")

  assert.NotEqual(t, 123, 456, "they should not be equal")

  assert.Nil(t, object)

  if assert.NotNil(t, object) {
    assert.Equal(t, "Something", object.Value)
  }
}

在这里我们也是简单展示一下 assert 的示例,更详细的内容可以阅读它的相关文档,在这里也就不多做展示了。

小结

如果之前完全没有写过单元测试或者没有写过 Go 语言的单元测试,相信这篇文章已经给了足够多的上下文帮助我们开始做这件事情,我们要知道的是单元测试其实并不会阻碍我们的开发进度,它能够为我们的上线提供信心,也是质量保证上投资回报率最高的方法。

学习写好单元测试一定会有一些学习曲线和不适应,甚至会在短期内影响我们的开发效率,但是熟悉了这一套流程和接口之后,单元测试对我们的帮助会非常大,每一个单元测试都表示一个业务逻辑,每次提交时执行单元测试就能够帮助我们确定新的代码大概率上不会影响已有的业务逻辑,能够明显地降低重构的风险以及线上事故的数量

总结

在这篇文章中我们从三个方面分别介绍了如何写优雅的 Go 语言代码,作者尽可能地给出了最容易操作和最有效的方法:

  • 代码规范:使用辅助工具帮助我们在每次提交 PR 时自动化地对代码进行检查,减少工程师人工审查的工作量;
  • 最佳实践
    • 目录结构:遵循 Go 语言社区中被广泛达成共识的 目录结构,减少项目的沟通成本;
    • 模块拆分:按照职责对不同的模块进行拆分,Go 语言的项目中也不应该出现 modelcontroller 这种违反语言顶层设计思路的包名;
    • 显示与隐式:尽可能地消灭项目中的 init 函数,保证显式地进行方法的调用以及错误的处理;
    • 面向接口:面向接口是 Go 语言鼓励的开发方式,也能够为我们写单元测试提供方便,我们应该遵循固定的模式对外提供功能;
      1. 使用大写的 Service 对外暴露方法;
      2. 使用小写的 service 实现接口中定义的方法;
      3. 通过 func NewService(...) (Service, error) 函数初始化 Service 接口;
  • 单元测试:保证项目工程质量的最有效办法;
    • 可测试:意味着面向接口编程以及减少单个函数中包含的逻辑,使用『小方法』;
    • 组织方式:使用 Go 语言默认的 Test 框架、开源的 suite 或者 BDD 的风格对单元测试进行合理组织;
    • Mock 方法:四种不同的单元测试 Mock 方法;
      • gomock:最标准的也是最被鼓励的方式;
      • sqlmock:处理依赖的数据库;
      • httpmock:处理依赖的 HTTP 请求;
      • monkey:万能的方法,但是只在万不得已时使用,类似的代码写起来非常冗长而且不直观;
    • 断言:使用社区的 testify 快速验证方法的返回值;

想要写出优雅的代码本身就不是一件容易的事情,它需要我们不断地对自己的知识体系进行更新和优化,推倒之前的经验并对项目持续进行完善和重构,而只有真正经过思考和设计的代码才能够经过时间的检验(代码是需要不断重构的),随意堆砌代码的行为是不能鼓励也不应该发生的,每一行代码都应该按照最高的标准去设计和开发,这是我们保证工程质量的唯一方法。

作者也一直在努力学习如何写出更加优雅的代码,写出好的代码真的不是一件容易的事情,作者也希望能通过这篇文章帮助使用 Go 语言的工程师写出更有 Golang 风格的项目。

出镜率比较高的Golang项目。

记录用的人较多或者出镜率比较高的Golang项目。

如果我漏了你觉得重要的项目,麻烦帮我指出,我尽快补上,谢谢!

下面列表中的每一个项目都配上了Star增长趋势的图片,可以看出该项目的热度。

特别是与区块链相关的两个项目fabricgo-ethereum,能够明显看出区块链行业的起伏。

整理过程中收获:

1、了解到docker项目已经改名为moby,当前仓库为:https://github.com/moby/moby

2、了解到时间序列数据库,库中每一个数据都有时间属性。

项目列表

Gin

仓库地址:https://github.com/gin-gonic/gin

Gin是用Go语言实现的一款web框架。

它的特点和Martini类似,但是API的性能更好,大概快40倍。如果你对性能要求极高,尝试一下Gin,不会让你失望。

Beego

仓库地址:https://github.com/astaxie/beego

一个使用 Go 的思维来帮助您构建并开发 Go 应用程序的开源框架。

一个快速开发 Go 应用的 HTTP 框架,可以用来快速开发 API、Web 及后端服务等各种应用,是一个 RESTful 的框架,主要设计灵感来源于 tornado、sinatra 和 flask 这三个框架,但是结合了 Go 本身的一些特性(interface、struct 嵌入等)而设计的一个框架。

框架特性:简单化智能化模块化高性能

Caddy:

仓库地址:https://github.com/mholt/caddy

一款可以用于生产的开源服务器,具有速度快,易使用,生产效率高的特点。

当前已经可以在WindowsMacLinuxBSDSolaris, and Android使用。

具有如下特点:

  1. 使用Caddyfile方便配置
  2. Auto HTTPS Caddy 使用 Let’s Encrypt 让你的站点全自动变成全站HTTPS,无需任何配置。当然你想使用自己的证书也行。
  3. HTTP/2 全自动支持HTTP/2协议,无需任何配置。
  4. 主机虚拟化使多个站点工作
  5. 可使用插件扩展
  6. 无需依赖即可运行
  7. 为了保证安全连接,使用了TLS session ticket key rotation

Nsq

仓库地址:https://github.com/nsqio/nsq

实时分发的消息平台,用于极大规模的数据处理,处理量级10亿+。

它提升了分布式和去中心化的拓扑结构,没有单点故障,支持容错和高可用性,并保证消息传递的可靠性。

在操作上,NSQ易于配置和部署(所有参数都在命令行上指定,编译后的二进制文件没有运行时依赖项)。为了获得最大的灵活性,它与数据格式无关(消息可以是JSON、MSGPack、协议缓冲区或其他任何格式)。官方的go和python库是现成的(以及许多其他客户机库),如果您有兴趣构建自己的库,这就是一个协议规范。

Hugo

仓库地址:https://github.com/gohugoio/hugo

一个静态的,可伸缩的网页生成器,宣称世界上最快的建站框架,不过这点和wordpress怎么比呢。

Go语言编写的静态网站生成器,速度快,易用,可配置。

Hugo获取一个包含内容和模板的目录,并将其呈现为完整的HTML网站。

Gogs

仓库地址:https://github.com/gogs/gogs

Gogs是一款极易搭建的自助Git服务。

该项目旨在打造一个以最简便的方式搭建简单、稳定和可扩展的自助Git服务。

使用Go语言开发使得Gogs能够通过独立的二进制分发,并且支持Go语言支持的 所有平台,包括 Linux、macOS、Windows 以及 ARM 平台。

Frp

仓库地址:https://github.com/fatedier/frp

frp是一个可用于内网穿透的高性能的反向代理应用,支持 tcp, udp 协议,为 http 和 https 应用协议提供了额外的能力,且尝试性支持了点对点穿透。

Proxypool

仓库地址:https://github.com/henson/proxypool

采集免费的代理资源为爬虫提供有效的IP代理

设计架构:

Getter:代理获取接口,目前有9个免费代理源,每调用一次就会抓取这些网站最新的100个代理放入Channel,可自行添加额外的代理获取接口;

Channel:临时存放采集来的代理,通过访问稳定的网站去验证代理的有效性,有效则存入数据库;

Schedule:用定时的计划任务去检测数据库中代理IP的可用性,删除不可用的代理。同时也会主动通过Getter去获取最新代理;

Api:代理池的访问接口,提供get接口输出JSON,方便爬虫直接使用。

Lantern

仓库地址:https://github.com/getlantern/lantern

区别于,SS,它是分布式的,点对点的,通过蓝灯,你可以和自由上网的用户共享网络,对方可以自由上网,你也就自由了。

SS-go

仓库地址:https://github.com/SS/SS-go

该项目为SS的Go语言实现,项目名称使用SS替代,你懂吧?

Syncthing

仓库地址:https://github.com/syncthing/syncthing

Syncthing是一个持续不断的文件同步项目,能够在两台或者多台电脑上同步文件,使用了其独有的对等自由块交换协议,速度极快。

主要特点:

确保数据的安全性:保护用户的数据是责无旁贷,该项目采取所有的合理的预防措施来避免用户的文件损坏。

确保数据不被攻击:不循序任何未经授权方的窃听或修改。

易于使用

自动化

能够在大多数通用的电脑上使用

Kubernetes

仓库地址:https://github.com/kubernetes/kubernetes

容器编排工具,实现自动化部署,更新,下线,负载均衡,容错处理等。

三个特点:

优化部署:快速而有预期地部署你的应用, 极速地扩展你的应用,增加项目的实例,能够实现自动布局、自动重启、自动复制、自动伸缩,并实现应用的状态检查自我修复

优化资源利用:跨主机编排容器, 更充分地利用硬件资源来最大化地满足企业应用的需求

声明式配置etcd声明式的容器管理,保证所部署的应用按照我们部署的方式运作.

Etcd

仓库地址:https://github.com/etcd-io/etcd

etcd

分布式可靠的键值存储,尤其是分布式系统中极其重要的数据,其特点:

Simple: API设计合理,面向用户

Secure: 自动的TLS连接,支持客户定制认证

Fast: 写入能力大于1w+每秒

Reliable: 使用Raft恰当的分发

etcd当前频繁的和Kubernetes,locksmith,vulcandDoorman等项目配合使用。

Moby

仓库地址:https://github.com/moby/moby

该项目是在容器化生态中组装容器时使用,以前的大名叫做:docker,这个大家都知道。后来经过一段纠结的时刻,改名字了,原因在这儿

docker

Moby是一个开放式项目,旨在维持模块化和灵活性。

模块化:该项目包括的许多组件,优秀的函数和API共同协作。

可交换:Moby包含足够的组件来构建功能齐全的容器系统,但其模块化架构确保大部分组件可以通过不同的实现进行交换。

可用安全性:Moby提供安全的缺省值,无需特殊配置。

Traefik

仓库地址:https://github.com/containous/traefik

Traefik是一款开源的反向代理与负载均衡工具。它最大的优点是能够与常见的微服务系统直接整合,可以实现自动化动态配置。 目前支持 Docker、Swarm、Mesos/Marathon、 Mesos、Kubernetes、Consul、Etcd、Zookeeper、BoltDB、Rest API 等等后端模型。

Influxdb

仓库地址:https://github.com/influxdata/influxdb

influxdb是目前比较流行的时间序列数据库。

时间序列数据库:数据格式里包含Timestamp字段的数据,几乎所有的数据其实都可以打上一个Timestamp字段。

Influxdb是一个开源的分布式时序、时间和指标数据库,使用go语言编写,无需外部依赖。

三大特性:

时序性(Time Series):与时间相关的函数的灵活使用(诸如最大、最小、求和等);

度量(Metrics):对实时大量数据进行计算;

事件(Event):任意事件的数据我们都可以做操作。

influxdb

Prometheus

仓库地址: https://github.com/prometheus/prometheus

一个开源的服务监控系统和时间序列数据库。

Prometheus提供的是一整套监控体系, 包括数据的采集,数据存储,报警,甚至是绘图(只不过很烂,官方也推荐使用 grafana)。 而InfluxDB只是一个时序数据库。同为时间序列数据库,两者对比:prometheus和influxdb对比

Grafana

仓库地址:https://github.com/grafana/grafana

Grafana是一款开源的,具有丰富功能的度量标准仪表板和图形编辑器,用于显示Graphite,Elasticsearch,OpenTSDB,Prometheus和InfluxDB等数据,定制化高。

Go-ethereum

仓库地址:https://github.com/ethereum/go-ethereum

以太坊协议使用Go语言的官方实现。

Fabric

仓库地址:https://github.com/hyperledger/fabric

区块链超级账本Hyperledger Fabric实现,用于联盟链开发。

Drone

仓库地址:https://github.com/drone/drone

Drone是一种基于容器技术的持续交付系统。

Drone使用简单的YAML配置文件(docker-compose的超集)来定义和执行Docker容器中的Pipelines

Drone与流行的源代码管理系统无缝集成,包括GitHub,GitHub Enterprise,Bitbucket等。

欢迎关注公号:程序员的金融圈

作者:大漠胡萝卜
链接:https://juejin.im/post/5cfa2cfef265da1bcc19333e
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Things I Learnt The Hard Way (in 30 Years of Software Development)

I discover a useful posts when searching , just repost it.

https://blog.juliobiason.net/thoughts/things-i-learnt-the-hard-way/

Following the original posts

—————————————————————————————-

This is a cynical, clinical collection of things I learnt in 30 years working with software development.

Again, some things are really cynical, others are long observations on different jobs.

Software Development

Spec first, then code

If you don’t know what you’re trying to solve, you don’t know what to code.

Write something specifying how the application works before writing any code.

“Without requirements or design, programming is the art of adding bugs to an empty text file.” — Louis Srygley

Sometimes, even an “elevator pitch” — up to two paragraphs that describe what the application does — is enough.

The times I stood longer looking at my own code wondering what to do next were when we didn’t have the next step defined. It is a good sign that it’s time to stop and discuss it with your coworkers — or maybe rethink the solution.

Write steps as comments

If you have no idea how to start, describe the flow of the application in high level, pure English/your language first. Then fill the spaces between comments with the code.

Better yet: think of every comment as a function, then write the function that does exactly that.

Gherkin is your friend to understand expectations

Gherkin is a test description format which points “Given that the system is in a certain state, When something happens, then this is expected”. Even if you don’t use any testing tool that reads Gherkin, it will give you a good understanding of what it is expected from the app.

Unit tests are good, integration tests are gooder

On my current job, we do test modules and classes only (for example, we write tests for the view layer only, then tests for the controller layer only, and so on). It gives us some idea if things are going right or not, but they lack a view of how the whole is going on — a thing integration tests, which tests how the system as a whole behaves — do better.

Tests make better APIs

We code in layers: There is the storage layer, which should make our data permanent; there is a processing layer, which should do some transformation on the data stored; there is a view layer, which has information on how the data must be present; and so on.

As I mentioned, integration tests feel better, but testing layers by themselves can give you a better view on how their API looks like. Then you can have a better look on how to call things: Is the API too complex? Do you have to keep to much data around to be able to make a single call?

Make tests that you know how to run on the command line

Not that command lines are important for any projects, but when you know the command to run the tests, you know how to automate the execution of the tests, which you then can use in a continuous integration tool.

Be ready to throw your code away

A lot of people, when they start with TDD, get annoyed when you say that you may have to rewrite a lot of stuff, including whatever your already wrote.

TDD was designed to throw code away: The more you learn about your problem, the more you understand that, whatever you wrote, won’t solve the problem in the long run.

You shouldn’t worry about this. Your code is not a wall: if you have to throw it always, it is not wasted material. Surely it means your time writing code was lost, but you got a better understanding about the problem now.

Good languages come with integrated tests

You can be sure that if a language brings a testing framework — even minimal — in its standard library, the ecosystem around it will have better tests than a language that doesn’t carry a testing framework, no matter how good the external testing frameworks for the language are.

Future thinking is future trashing

When developers try to solve a problem, they sometimes try to find a way that will solve all the problems, including the ones that may appear in the future.

But here is the thing: The problems from the future will never come and you’ll end up either having to maintain a huge behemoth of code that will never be fully used or you’ll end up rewriting the whole thing ’cause there is a shitton of unused stuff.

Solve the problem you have right now. Then solve the next one. And the next one. At one point, you’ll realize there is a pattern emerging from those solutions and then you’ll find your “solve everything”.

Documentation is a love letter to your future self

We all know writing the damn docs for functions and classes and modules is a pain in the backside. But realizing what you were thinking when you wrote the function will save your butt in the future.

The function documentation is its contract

When you start the code by writing the documentation, you’re actually making a contract (probably with your future self): I’m saying this function does thisand this is what it does.

If later you find out that the code doesn’t match the documentation, you have a code problem, not a documentation problem.

If a function description includes an “and”, it’s wrong

Functions should do one thing and one thing only. When you’re writing the function documentation and find that you added an “and”, it means the function is doing more than one thing. Break that function into two and remove the “and”.

Don’t use Booleans as parameters

When you’re designing a function, you may be tempted to add a flag. Don’t do this.

Here, let me show you an example: Suppose you have a messaging system and you have a function that returns all the messages to an user, calledgetUserMessages. But there is a case where you need to return a summary of each message (say, the first paragraph) or the full message. So you add a flag/Boolean parameter called retrieveFullMessage.

Again, don’t do that.

‘Cause anyone reading your code will see getUserMessage(userId, true) and wonder what the heck that true means.

You can either rename the function to getUserMessageSummaries and have another getUserMessagesFull or something around those lines, but each function just call the original getUserMessage with true or false — but the interface to the outside of your class/module will still be clear.

But don’t add flags/Boolean parameters to your functions.

Beware of interface changes

In the point above, I mentioned about renaming the function. If you control the whole source where the function is used, that’s not issue, it’s just a matter of search and replace.

But if that function is actually exposed by a library, you shouldn’t change function names in a whim. That will break a lot of other applications beyond your control and make a lot of other people unhappy.

You can create the new functions and mark the current one as deprecated, either by documentation or by some code feature. Then, after a few released, you can finally kill the original function.

(A dickish move you can do is to create the new functions, mark the current function as deprecated and add a sleep at the start of the function, in a way that people using the old function are forced to update.)

Good languages come with integrated documentation

If the language comes with its own way of documenting functions/classes/modules/whatever and it comes even with the simplest doc generator, you can be sure that all the language functions/classes/modules/libraries/frameworks will have a good documentation (not great, but at least good).

Languages that do not have integrated documentation will, most of the time, have a bad documentation.

A language is much more than a language

A programming language is that thing that you write and make things “go”. But it has much more beyond special words: It has a build system, it has a dependency control system, it has a way of making tools/libraries/frameworks interact, it has a community, it has a way of dealing with people.

Don’t pick languages just ’cause they easier to use. Always remember that you may approve the syntax of a language for being that easy, but you’re also enabling the way maintainers deal with the community by choosing that language.

Sometimes, it’s better to let the application crash than do nothing

Although that sounds weird, it’s better to not add any error handling than silently capturing errors and doing nothing.

A sadly common pattern in Java is





This does nothing to deal with the exception — besides printing it, that is.

If you don’t know how to handle it, let it happen,so you can figure out whenit will happen.

If you know how to handle the issue, handle it

Counter-point to the previous point: If you know when something will raise an exception/error/result and you know how to handle it, handle it. Show an error message, try to save the data somewhere else, capture the user input in a log file to later processing, but handle it.

Types say what you data is

Memory is just a sequence of bytes; bytes are just numbers from 0 to 255; what those numbers mean is described on the language type system.

For example, in C, a char type of value 65 is most probably the letter “A”, which an int of value is 65 is the number 65.

Remember this when dealing with your data.

This is what most people get wrong about adding booleans to check the number of True values. Here, let me show you an example of JavaScript that I saw recently:





If your data has a schema, use a structure to keep it

You may be tempted to use a list (or tuple, if your language allows) to keep your data if it’s simple — like, say, only 2 fields.

But if you data has a schema — it has a fixed format — you should always use some structure to keep it, but it a struct or a class.

Understand and stay way of cargo cult

“Cargo cult” is the idea that, if someone else did, so can we. Most of the time, cargo cult is simply an “easy way out” of a problem: Why would we think about how to properly store our users if X did that?

“If BigCompany stores data like this, so can we”.

“If BigCompany is behind this, this is good.”

“Right tool for the job” is just to push an agenda

“Right tool for the job” should be an expression that meant that there is a right and a wrong tool to do something — e.g., using a certain language/framework instead of the current language/framework.

But every time I heard someone mention it, they were trying to push their favourite language/framework instead of, say, the right language/framework.

“The right tool” is more obvious than you think

Maybe you’re in a project that needs to process some text. Maybe you’re tempted to say “Let’s use Perl” ’cause you know that Perl is very strong in processing text.

What you’re missing: You’re working on a C shop. Everybody knows C, not Perl.

Sure, if it is a small, “on the corner” kind of project, it’s fine to be in Perl; if it is important for the company, it’s better that if it is a C project.

PS: Your hero project (more about it later in this doc) may fail due this.

Don’t mess with things outside your project

Sometimes people are tempted to, instead of using the proper extension tools, change external libraries/frameworks — for example, making changes directly into WordPress or Django.

This is an easy way to make the project unmaintainable really really fast. As soon as a new version is released, you’ll have to keep up your changes in sync with the main project and, pretty soon, you’ll find that the changes don’t apply anymore and you’ll leave the external project in an old version, full of security bugs.

Data flows beat patterns

(This is personal opinion) When you understand how the data must flow in your code, you’ll end up with better code than if you applied a bunch of design patterns.

Design patterns are used to describe solutions, not to find them

(Again, personal opinion) Most of the time I saw design patterns being applied, they were applied as a way to find a solution, so you end up twisting a solution — and, sometimes, the problem it self — to fit the pattern.

First, solve your problem; find a good solution; then you can check the patterns to know how you name that solution.

I saw this happens a lot: We have this problem; a design pattern gets close to the proper solution; let’s use the design pattern; now we need to add a lot of things around the proper solution to make it fit the pattern.

Learn the basics functional programming

You don’t need to go deep into “what is a monad” and “is this a functor”. But remember to not keep changing your data all the time, create a new element with the new values (treat your data as immutable) and make functions/classes that don’t keep some internal state (pure functions/classes) if possible.

Cognitive Cost is the readability killer

Cognitive dissonance” is a fancy way of saying “I need to remember two (or more) different things at the same time to understand this.” Keeping those different things in your head creates a cost and it keeps accumulating the more indirect the things are (’cause you’ll have to keep all those in your head).

For example, adding booleans to count the number of True values is a mild cognitive dissonance; if you’re reading a piece of code and see a sum()function, which you know makes the sum of all numbers in a list, you’d expect the list to be composed of numbers, but I’ve seen people using sum() to count number of True values in a list of booleans, which is confusing as heck.

The Magical Number Seven, Plus or Minus Two

The magical number” is a psychology article about the number of things one can keep in their mind at the same time.

If you have a function, that calls a function, that calls a function, that calls a function, that calls a function, that calls function, you may be sure it will be a hell to read later.

Think more about: I’ll get the result of this function, then pass it to the second function, get its result, pass to the third an so on.

But:

  1. Today, psychologists talk more about the magical number FOUR, not seven.
  2. Think function composition (as in “I’ll call that function, then that function, then that function…”), not function calling (as in “That function will call that function, that will call that function…”).

Shortcuts are nice, but only in the short run

A lot of languages/libraries/frameworks add a way to make things shorter, reducing the number of things you need to type.

But, later, that will bite you and you’ll have to remove the shortcut and do the long things.

So learn what the shortcut does before using it.

You don’t need to write things the hard way first and then clean up using the shortcuts: All you need to do is what the shortcut does in the background, so you at least have knowledge of what can go wrong using it, or how to replace it with the non-shortcut version.

Resist the temptation of “easy”

Sure that IDE will help you with a ton of autocomplete stuff and let you easily build your project, but do you understand what’s going on?

Do you understand how your build system works? If you had to run it without the IDE, would you know how?

Can you remember your function names without autocomplete? Isn’t there a way to break/rename things to make them easier to understand?

Be curious about what goes behind the curtains.

ALWAYS use timezones with your dates

When dealing with dates, always always add the timezone with it. There will be always a problem with your computer timezone and the production server timezone (or one of the instances timezones) and you’ll lose a lot of time trying to debug what the heck the interface is showing the wrong time.

ALWAYS use UTF-8

The same problem you’ll have with dates, you’ll have with character encoding. So always convert your strings to UTF8; save them in the database as UTF8; return UTF8 on your APIs.

(You may convert to any other encoding, but UTF8 won the encoding wars, so it is easier to keep it this way.)

Start stupid

One way to get away from the IDE is to “start stupid”: Just get the compiler and get an editor (ANY editor) with code highlight and do your thing: Code, build it, run it.

No, it’s not easy. But when you jump into some IDE, you’ll think of buttons of simply “Yeah, it runs that” (which is exactly what IDEs do, by the way.)

Logs are for events, not user interface

For a long time, I used logs to show the user whatever was happening — ’cause, you know, it’s a lot easier to use a single thing instead of two.

Use the standard output to inform the user of events, standard err to inform the user about errors but use logs to capture something that you can later process easily.

Think about logs of something you’ll have to parse to extract some information at that time, not user interface; it doesn’t have to be human-readable.

Debuggers are over-rated

I heard a lot of people complaining that code editors that don’t come with debugging are terrible, exactly because they don’t come with debugging.

But when your code is in production, you can’t run your favorite debugger. Heck, you can’t even run your favourite IDE. But logging… Logging runs everywhere. You may not have the information you want at the time of the crash (different logging levels, for example) but you can enable logging to figure out something later.

(Not saying debuggers are bad, they just not as helpful as most people would think.)

Always use a Version Control System

“This is my stupid application that I just want to learn something” is not even a good excuse to not use a version control system.

If you start using a VCS right from the start, it will be easier to roll back when you do something stupid.

One commit per change

I’ve seen people writing commit messages like “Fixes issues #1, #2 and #3”. Unless all those issues are duplicates — in which two of those should be already closed — they should be 3 commits, not one.

Try to keep a change in a single commit (and by change I don’t mean “one file change”; if a change requires changes in three files, you should commit those three files together. Think “if I revert this back, what must go away?”)

“git add -p” is your friend when you overchange

(Git topic only) Git allows merging a file partially with “-p”. This allows you to pick only the related changes and leave the other behind — probably for a new commit.

Organize projects by data/type, not functionality

Most projects keep an organization like:





in other words, they keep data organized by functionality (all the incoming models are in the same directory/package, all the filters are in the same directory/package and so on).

This is fine and works. But when you organize by data, it’ll make a lot easier to split your project in smaller projects — ’cause, at some point, you may want to do almost the same thing as you’re doing right now, but with small differences.





Now you can make a module that deals only with Data1, another that works only with Data2 and so on. And then you can break them into isolated modules.

And then when you have another project that also have Data1 but also deals with Data3, you can reuse most of the stuff in the Data1 module.

Create libraries

I’ve seen a lot of projects that either make a mega repository with different projects or keep different branches that instead of just being a temporary environment for later joining the main development area, are just to keep that small, different thing going (picking the point above about modularization, imagine that instead of building a new project that reuse the Data1 type, I have a branch that has a completely different main function and the Data3 type).

Why not split the common parts into libraries and require it in different projects?

The reason is, most of the time, ’cause people don’t know how to either create libraries or they worry how they are goint to “publish” those libraries into the dependency sources without giving it around (so maybe it’s a good idea to also understand how your project management tool retrieves dependencies, so you can create your own dependency repository).

Learn to monitor

On a previous life, to understand how a system behaved, I added a ton of metrics: how fast things were going in, how fast things were going out, how many things were in the middle, how many the job processed…

It gives a really good view of how a system is behaving. Is the speed going down? If it is, I can check what is going into the system to understand why. Is it normal going down at some point?

Thing is, after this, it is really weird trying to figure out how “healthy” a system without any monitoring is after that. Checking a system health with just “Is it answering requests” doesn’t fly anymore.

Adding monitoring early will help you understand how your system behaves.

The config file is friend

Imagine you wrote a function that you have to pass a value for it to start processing (say, a twitter user account id). But then you have to do that with two values and you just call the function again with the other value.

It makes more sense to use a config file and just run the application twice with two different config files.

Command line options are weird, but helpful

If you move things to config files, you could also help your users by adding an option to select the config file and expose it.

There are libraries to handling command line options for every language today, which will help you into building a good command line and giving your users a standard interface for everything.

Not just function composition, but application composition

Unix came with the idea of “applications that do one thing and do it well”.

Now, I said you could use one application with two config files, but what if you need the result of both applications?

That’s when you can write an application that reads the results of the first one with both config files) and turn into a single result.

Even for app composition, start stupid

Application composition may lead to microservices — which is good — but microservices require some ideas about how applications “talk” between them over the wire (protocols and such).

You don’t need to start with that. Both applications can write and read from files, which is way easier.

Worry about talking over the wire later, when you understand how networks work.

Optimization is for compilers

Let’s say you need more performance. You may be tempted to look at your code and thing “where I can squeeze a little bit more performance here” or “How can I remove a few cycles here to get more speed”.

Well, guess what? Compilers know how to do that. Smarted compilers can even delete your code ’cause it will always generate the same result.

What you need to do is think a better design for your code, not how to improve the current code.

Code is humans to read. ALWAYS. Optimization is what compilers do. So find a smarted way to explain what you’re trying to do (in code) instead of using shorter words.

By lazy (evaluated)

A long time ago, a small language made the rounds by not evaluating expressions when they appeared, but when they were needed.

Lisp did this a long time ago, and now most languages are getting it too.

For example, Python have the yield statement, which will stop the execution of the current function and return the value immediately, yielding a new value only when the function is called again. If you chain functions that keepyielding results, you won’t need as much memory as functions that keep returning lists.

On a Team/Work

Code reviews are not for style

Take your time on code reviews to point architectural or design problems, not code style problems. Nobody really likes the person whose code reviews are only “you left blanks in this line” or “missing space before parenthesis” and such.

Now, if you do find architectural or design problems, then you can add your code style problems.

Code formatting tools are ok, but they are no silver bullet

One thing a team may be tempted to do to avoid discussing style in code reviews is to use a code formatting tool to auto-format code before committing.

Now yeah, that kinda solves the problem, but there is one small problem: we, humans, are not as flexible to read code as computers are; what is readable by a computer may not be readable by a human. Surely they try to create some heuristics on what is good for human reading, but that doesn’t mean it gets right.

If you do use a code formatting tool, use it to find out where it changes the code the most; you probably need to simplify that part of the code to avoid it messing so much.

Code style: Follow it

If your project have a defined code style, you must follow it. Sometimes it may not be clear (“this struct/class should be singular or plural”?), but do your best to follow it.

… unless that code style is the Google Code style

(Totally personal opinion, feel free to disagree) Every freaking time Google comes with their own coding style, it’s a garbage fire. The community came with a better style way before and Google seem to come with a style with high contrasting parts just to call it theirs.

There is only one coding style for C/C++: K&R

(Totally personal opinion again) Every other coding style is WRONG. 🙂

There is only one coding style for Python: PEP8

The community (most of it) writes code in PEP8. Follow it and your code smoothly integrate with the rest of the ecosystem.

Explicit is better than implicit

You know what’s one of the worst function names ever? sleep().

Sleep for how long? It is seconds or milliseconds?

Be explicit with what you use; sleepForSecs and sleepForMs are not perfect, but are better than sleep.

(Think about this when you’re writing your app command line interface or its config file.)

(I could throw the whole “Zen of Python” here, but I’m trying to focus on personal, direct experience.)

Companies look for specialists but keep generalists longer

If you know a lot about one single language, it may make it easier to get a job, but in the long run, language usage dies and you’ll need to find something else. Knowing a bit about a lot of other languages helps in the long run, not to mention that may help you think of better solutions.

“A language that doesn’t affect the way you think about programming, is not worth knowing.” — Alan Perlis

For a long time, I kept a simple programming rule: The language I’m playing at home should not be the same language I’m using at work. This allowed me to learn new things that later I applied in the work codebase.

I learnt how generics work in Java by writing Rust code; I understood how Spring does dependency injection by reading how to do it in C++.

Think of the users

Think how the data you’re collecting from your users will be used — this is more prevalent on these days, where “privacy” is a premium.

If you capture any used data, remember to protect it against unauthorized use.

The best secure way to deal with user data is not to capture it

You can be sure that, at some point, the data will leak, either by some security flaw or human interference.

If you don’t capture any user data — or store it in anonymized way — you won’t have any problems.

Keep a record of “stupid errors that took me more than 1 hour to solve”

I tried but never managed to create a list of stupid errors I kept finding that took more than 1 hour to solve it, which were simply “forgot to add dependency” or “add annotation”, mostly because there was more than once that I kept fighting some stupid error for more than 1 hour.

But you should try to keep a list of stupid errors that took you 1 hour to solve, ’cause later you can use it to not stay more than 1 hour to solve some stupid error.

If it doesn’t run on your computer, you have a problem

I’ve seen a lot of systems that would never run on a isolated computer, like the developer tool, ’cause the system requires running on a specialized environment.

This is something that really kills productivity.

If your system will run on a specialized environment — and I’m including “the cloud” here — look for something that can abstract whatever you’re using. For example, if you’re using AWS SQS, which is a queue, look for a library that can abstract the way a queue works so you can also run with RabbitMQ, which can be easily run on your own computer.

If you’re using a very specialized thing, you may have to write the abstraction yourself, isolating it from the main system, so you can develop the main product in peace.

Personal

When it’s time to stop, it’s time to stop

Learn when you can’t code anymore. Learn when you can’t process things anymore. Don’t push beyond that, it will just make things worse in the future.

I tried to keep coding once when I had a migraine (not strong, but not mild). Next day, when I was better, I had to rewrite most of the stuff I did, ’cause it was all shit.

Code of conduct protect you, not them

When you’re beginning with any language/library/framework, check their CoC; they will protect you from being harassed for not immediately getting what is going on instead of blocking you from telling them what you think.

I’m mentioning this ’cause a lot of people complain about CoC, but they forget that they allow them to join in any project without being called “freaking noob” or “just go read the docs before annoying us”.

Also, remember that most people that are against CoCs are the ones that want to be able to call names on everyone.

Learn to say no

Sometimes, you’ll have to say no: No, I can’t do it; no, it can’t be made in this time; no, I don’t feel capable of doing this; no, I don’t feel comfortable writing this.

Once I had to say to our CTO: “Ok, I’ll do it, but I want to note that I don’t agree with what we are doing.” In the end, the app was barred exactly because the thing we were doing.

You’re responsible for the use of your code

This is hard. Very very hard. It’s the difference between “freedom” and “responsibility”.

There is nothing wrong in writing, for example, a software to capture people’s faces and detect their ethnicity, but you have to think about what that will be used on.

Don’t tell “It’s done” when it’s not

You are tired of running the same thing over and over again. You kinda remember that something weird may happen, but because you’re tired, you tell everyone that “It’s finished”.

Don’t do that.

Someone will try that weird case on the first run and immediately tell you that it is not working.

You’ll learn about yourself the hard way

We get frustrated with code that doesn’t compile. We get angry with customers asking things back and forth.

And we lash out on other when that happens.

And that will get you in trouble.

It happens.

People get pissed/annoyed about code/architecture because they care

You’ll find yourself in the other side of the coin: You’ll describe some solution and people will seem annoyed/pissed about some solution.

When people care about a product/code, they do that.

“Yeah, you don’t like that hushed solution ’cause you care” was one of the nicest things someone told about myself.

Learn from your troubles

You’ll get annoyed, pissed, frustrated, and angry. You’ll get you in trouble. You’ll see people getting in trouble because of this kind of stuff.

You must learn about it. Don’t ignore it.

One thing I learnt the hard way was that I get really aggressive when I’m frustrated. Now, when I notice I start to get frustrated, I ask help from someone else. It’s really therapeutic to see that someone else also struggles with your problem, and that’s not just you.

Pay attention on how people react to you

I have a “angry man resting face” kind of face.

Sometimes I’ll ask things and people will move a bit back — like I’m telling them their solution is wrong.

That’s when I have to add “I’m not saying it’s wrong, I’m just confused”.

That may help you to not get in trouble.

Learn to recognize toxic people; stay away from them

You’ll find people that, even if they don’t small talk you, they will bad mouth everything else — even some other people — openly.

Stay away from those people.

You have no idea how that kind of attitude will drive you down.

Beware of micro-aggressions

“Micro-aggressions” are aggressive comments in small doses. Like someone that keeps calling you “that person” or seemingly innocuous comments about your position in some policy.

Those are hard to fight, ’cause PR won’t listen to you saying that they are attacking you. Also, they are hard to detect, ’cause they seem small enough, but they do pile up and you’ll blow your anger all at once.

Better just stay away and avoid contact as possible.

No, I don’t think they are “fixable”

(Personal opinion) Someone could say “Hey, maybe if you spoke to that person, they would stop”.

Personally, I don’t think they would. This kind of stuff is going for so long to them that it feels natural and, most of the time, you’re the wrong one (for not seeing that they are joking, for example, in true “Schrödinger’s asshole” style.)

Toxic/micro-aggressors are only fixable if they are YOU

Unless it’s you realizing you’re acting like a toxic person or micro-attacking someone, and realize that you’re actually doing more harm than good being that way, there is no way to fix those traits (again, personal opinion).

…mostly ’cause hearing from someone else may feel “they are the ones against me!” to them.

Hero Projects: You’ll have to do it someday

An “hero project” is a project/spec change/framework that you personally think will solve a group of problems in your project. It could be a different architecture, a new framework or even a new language.

That means you’ll spent your free time to write something that is already being worked/exists just to prove a point.

Sometimes it proves you where wrong.

(But you got something from it, nonetheless.)

Don’t confuse “hero project” with “hero syndrome”

I have seen this at least two times: Someone claims things don’t work when they aren’t around or that they don’t need help.

This is “hero syndrome”, the idea that that person is the only one capable of solving all the problems.

Don’t be that person.

Learn when to quit

You tell your boss that you didn’t finish on time because something weird happened and he lashed out at you.

One of your coworkers is constantly micro-attacking you.

Another one is the guy that keeps doing stupid pranks, saying bullshit and small talking other groups all the time.

A third is always complaining that when he’s not around, things don’t work.

It’s time to start sending your resume around, no matter how good the pay is or how awesome the project is.

… unless you want to be a constantly pissed off, annoyed person when you’re in the forties.

I.T. world is a very small egg

We have a expression here: “The world of something is a small egg”, which means that you don’t live in a large world; the world is small.

I.T. world is really small.

The person you work with today will find you again in 15 years after you both changed 3 or 4 jobs already.

And you’ll meet a lot of other I.T. people in the way.

And they will talk about themselves.

And whatever you say/do will be talked around, which one person will hear and pass along another company, which will pass along other people, which will pass the story along to another company and, suddenly, when you realized, nobody will hire you locally ’cause everybody knows that time when you fucked up a project or punched a colleague in the face.

Paper notes are actually helpful

I tried to become “paperless” many times. At some point, I did keep the papers away, but in the very end, it really do help to have a small notebook and a pen right next to you write that damn URL you need to send the data.

Trello is cool and all, but Post-its are nicer

Nothing says “I’m really busy, but organized” like having a bunch of post-its on your desk.

Blogging about your stupid solution is still better than being quiet

You may feel “I’m not start enough to talk about this” or “This must be so stupid I shouldn’t talk about it”.

Create a blog. Post about your stupid solutions. They are still smarter than someone else’s solution.

Also, come back later and fight your own solutions with better ones.

Show your growth.

On top of that, they help you keep small notes or things you need to do.

… but turn off the comments

One thing about posting your stupid solution is that it will attract people who just want to mess with you. “This is stupid”, for example. “Your dumb” may someone say, unaware of who’s actually dumb.

Turn it off. Don’t let those people stop you.

Post your stupid solution online

Don’t keep a Github only for those “cool, almost perfect” projects. You’re free to show that, at some point, you were a beginner.

You can always come back and improve your code.

(Or don’t: I still have a public repo of my first Python project that looks like I just translated Java into Python, without the Pythonic part.)

Keep a list of “Things I Don’t Know”

Richard Feymann, famous physicist, kept a notebook with the title “Things I Don’t Know”.

When you find something that seems cool and you’d like to know more, create a file/note/whatever with it in the title. Then make notes about what you find/figure out.

GoCN每日新闻(2019-07-03)

1. Go并发编程与同步原语 https://draveness.me/golang-sync-primitives.html

2. Go Modules踩坑总结 https://juejin.im/post/5d1ae1b2f265da1b8466fecb

3. 彻底弄懂return和defer的微妙关系 https://juejin.im/post/5d173ce16fb9a07ea803df75

4. net/http输出context引起的map panic http://xiaorui.cc/?p=5919

5. []byte和string类型相互转换时的性能分析和优化 https://www.pengrl.com/p/31544/

编辑: 周云轩

订阅新闻: http://tinyletter.com/gocn

GoCN归档: https://gocn.vip/question/3544

GoCN每日新闻(2019-07-02)

1. Go 实现的广告拦截工具:https://github.com/kaepora/resilience 

2. Go 压缩/解压缩文件: https://github.com/mholt/archiver

3. Go vs Python: 选择适合你的语言 https://medium.com/swlh/python-vs-golang-select-the-best-one-to-level-up-your-business-1a6d0fb32991

4. 企业构建 Serverless 团队的五个建议:https://thenewstack.io/five-tips-building-serverless-teams-enterprise

5. 使用 AWS Lambda 构建 PDF 生成器:https://medium.com/@_rich/richard-keller-61d9cb0f430 

编辑: 薛锦

订阅新闻: http://tinyletter.com/gocn

GoCN归档:https://gocn.vip/question/3541

GoCN每日新闻(2019-07-01)

1. 微服务系列(一):Go Rpc 源码解读 https://juejin.im/post/5d1760455188255cfc1a019f

2. 记一次 goroutine 泄漏问题查找原因与解决 https://github.com/cdongyang/note/blob/master/go/goroutine-leak.md

3. 使用git操作svn仓库 https://tonybai.com/2019/06/25/using-git-with-svn-repo

4. Excelize发布2.0.1版本, Go语言最受欢迎的Excel基础库 https://mp.weixin.qq.com/s/LmIBHOEjAK7N9oru6TeT5A

5. 开源项目之个人视频流服务器 tube https://github.com/wybiral/tube

编辑: 宋佳洋

订阅新闻: http://tinyletter.com/gocn

GoCN归档:https://gocn.vip/question/3540

GoCN每日新闻(2019-06-30)

1. 关于改进 “if err != nil” 的讨论 https://github.com/golang/go/issues/32825

2. 为 Go 开发者准备的 Makefiles 教程 https://www.youtube.com/watch?v=QztvWSCbQLU

3. gRPC+gRPC Gateway 能不能不用证书?https://github.com/EDDYCJY/blog/blob/master/golang/2019-06-22-grpc-gateway-tls.md

4. 还原 panic 时的函数信息 https://lekstu.ga/posts/pclntab-function-recovery/

5. 性能持续测试工具 performabot https://github.com/saschagrunert/performabot

编辑: lwhile

订阅新闻: http://tinyletter.com/gocn

GoCN归档:https://gocn.vip/question/3539

GoCN每日新闻(2019-06-29)

1. 提案:内置的Go错误检查 https://go.googlesource.com/proposal/+/master/design/32437-try-builtin.md

2. Go Modules 语义版本控制的问题 https://blog.dgraph.io/post/serialization-versioning

3. 尝试更严格的gofmt https://www.reddit.com/r/golang/comments/c66cz2/experimenting_with_a_stricter_gofmt

4. 减少类型层次 https://www.ardanlabs.com/blog/2016/10/reducing-type-hierarchies.html

5. 分布式时序数据库 LinDB https://zhuanlan.zhihu.com/p/35998778

编辑: 马怀博 

订阅新闻: http://tinyletter.com/gocn

GoCN归档:https://gocn.vip/question/3538

GoCN每日新闻(2019-06-28)

1. 关于内置catch函数捕获错误的讨论 https://github.com/golang/go/issues/32811

2. Go单元测试 https://levelup.gitconnected.com/unit-testing-in-go-205ada2b9d7c

3. Go defer 会有性能损耗,尽量不要用?https://github.com/EDDYCJY/blog/blob/master/golang/pkg/2019-06-16-Go-defer-loss.md

4. 如何快速提升 Go 程序性能? https://mp.weixin.qq.com/s/rta0Prdc5xq7Zd7mLzvcWA

5. Go Modules踩坑总结 https://mp.weixin.qq.com/s/2v8kGm8T9BQFmpLfQE7-wg

编辑: yulibaozi

订阅新闻: http://tinyletter.com/gocn

GoCN归档:https://gocn.vip/question/3537