If you’ve ever programmed with Javascript, you definitely know about Promise and async/await. C#, *Java, Python, *and some other programming languages apply the same pattern but with other names such as Task or Future.
On the contrary, Go doesn’t follow the pattern at all. Instead, it introduces goroutines and channels. However, it isn’t difficult to replicate the pattern with goroutines and channels.
Single async/await
First, let’s experiment with a simple use case: await a result from an async function.
funclongRunningTask() <-chanint32 { r := make(chanint32)
gofunc() { deferclose(r) // Simulate a workload. time.Sleep(time.Second * 3) r <- rand.Int31n(100) }()
return r }
funcmain() { r := <-longRunningTask() fmt.Println(r) }
Single async/await in Javascript vs. Golang
To declare an “async” function in Go:
The return type is <-chan ReturnType.
Within the function, create a channel by make(chan ReturnType) and return the created channel at the end of the function.
Start an anonymous goroutine by go func() {...} and implement the function’s logic inside that anonymous function.
Return the result by sending the value to channel.
At the beginning of the anonymous function, add defer close(r) to close the channel once done.
To “await” the result, simply read the value from channel by v := <- fn().
Promise.all()
It’s very common that we start multiple async tasks then wait for all of them to finish and gather their results. Doing that is quite simple in both Javascript and Golang.
funclongRunningTask() <-chanint32 { r := make(chanint32)
gofunc() { deferclose(r) // Simulate a workload. time.Sleep(time.Second * 3) r <- rand.Int31n(100) }()
return r }
funcmain() { aCh, bCh, cCh := longRunningTask(), longRunningTask(), longRunningTask() a, b, c := <-aCh, <-bCh, <-cCh fmt.Println(a, b, c) }
We have to do it in 2 lines of code and introduce 3 more variables, but it’s clean and simple enough.
We can not do <-longRun(), <-longRun(), <-longRun(), which will longRun() one by one instead all in once.
Promise.race()
Sometimes, a piece of data can be received from several sources to avoid high latencies, or there’re cases that multiple results are generated but they’re equivalent and the only first response is consumed. This first-response-win pattern, therefore, is quite popular. Achieving that in both Javascript and Go is very simple.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// Javascript.
const one = async () => { // Simulate a workload. sleep(Math.floor(Math.random() * Math.floor(2000))) return1 }
const two = async () => { // Simulate a workload. sleep(Math.floor(Math.random() * Math.floor(1000))) sleep(Math.floor(Math.random() * Math.floor(1000))) return2 }
const r = awaitPromise.race(one(), two()) console.log(r)
// Simulate a workload. time.Sleep(time.Millisecond * time.Duration(rand.Int63n(2000))) r <- 1 }()
return r }
functwo() <-chanint32 { r := make(chanint32)
gofunc() { deferclose(r)
// Simulate a workload. time.Sleep(time.Millisecond * time.Duration(rand.Int63n(1000))) time.Sleep(time.Millisecond * time.Duration(rand.Int63n(1000))) r <- 2 }()
return r }
funcmain() { var r int32 select { case r = <-one(): case r = <-two(): }
fmt.Println(r) }
select-case is the pattern that Go designed specifically for racing channel operations. We can even do more stuff within each case, but we’re focusing only on the result so we just leave them all empty.
Promise.then() and Promise.catch()
Because Go’s error propagation model is very different from Javascript, there’s any clean way to replicate Promise.then() and Promise.catch(). In Go, error is returned along with return values instead of being thrown as exception. Therefore, if your function can fail, you can consider changing your return <-chan ReturnType into <-chan ReturnAndErrorType, which is a struct holding both the result and error.
Readability is essential for maintainability. (可读性对于可维护性是至关重要的。) — Mark Reinhold (2018 JVM 语言高层会议)
为什么 Go 语言的代码可读性是很重要的?我们为什么要争取可读性?
Programs must be written for people to read, and only incidentally for machines to execute. (程序应该被写来让人们阅读,只是顺便为了机器执行。) — Hal Abelson 与 Gerald Sussman (计算机程序的结构与解释)
可读性很重要,因为所有软件不仅仅是 Go 语言程序,都是由人类编写的,供他人阅读。执行软件的计算机则是次要的。
代码的读取次数比写入次数多。一段代码在其生命周期内会被读取数百次,甚至数千次。
The most important skill for a programmer is the ability to effectively communicate ideas. (程序员最重要的技能是有效沟通想法的能力。) — Gastón Jorquera [1]
可读性是能够理解程序正在做什么的关键。如果你无法理解程序正在做什么,那你希望如何维护它?如果软件无法维护,那么它将被重写;最后这可能是你的公司最后一次投资 Go 语言。
Design is the art of arranging code to work today, and be changeable forever. (设计是安排代码到工作的艺术,并且永远可变。) — Sandi Metz
我要强调的最后一个基本原则是生产力。开发人员的工作效率是一个庞大的主题,但归结为此; 你花多少时间做有用的工作,而不是等待你的工具或迷失在一个外国的代码库里。 Go 程序员应该觉得他们可以通过 Go 语言完成很多工作。
有人开玩笑说, Go 语言是在等待 C++ 语言程序编译时设计的。快速编译是 Go 语言的一个关键特性,也是吸引新开发人员的关键工具。虽然编译速度仍然是一个持久的战场,但可以说,在其他语言中需要几分钟的编译,在 Go 语言中只需几秒钟。这有助于 Go 语言开发人员感受到与使用动态语言的同行一样的高效,而且没有那些语言固有的可靠性问题。
我可以选择 s 替代 sum 以及 c(或可能是 n)替代 count,但是这样做会将程序中的所有变量份量降低到同样的级别。我可以选择 p 来代替 people,但是用什么来调用 for ... range 迭代变量。如果用 person 的话看起来很奇怪,因为循环迭代变量的生命时间很短,其名字的长度超出了它的值。
贴士: 与使用段落分解文档的方式一样用空行来分解函数。 在 AverageAge 中,按顺序共有三个操作。 第一个是前提条件,检查 people 是否为空,第二个是 sum 和 count 的累积,最后是平均值的计算。
2.2.1. 上下文是关键
重要的是要意识到关于命名的大多数建议都是需要考虑上下文的。 我想说这是一个原则,而不是一个规则。
两个标识符 i 和 index 之间有什么区别。 我们不能断定一个就比另一个好,例如
1 2 3
for index := 0; index < len(s); index++ { // }
从根本上说,上面的代码更具有可读性
1 2 3
for i := 0; i < len(s); i++ { // }
我认为它不是,因为就此事而论, i 和 index 的范围很大可能上仅限于 for 循环的主体,后者的额外冗长性(指 index)几乎没有增加对于程序的理解。
但是,哪些功能更具可读性?
1
func(s *SNMP)Fetch(oid []int, index int)(int, error)
或
1
func(s *SNMP)Fetch(o []int, i int)(int, error)
在此示例中,oid 是 SNMP 对象 ID 的缩写,因此将其缩短为 o 意味着程序员必须要将文档中常用符号转换为代码中较短的符号。 类似地将 index 替换成 i,模糊了 i 所代表的含义,因为在 SNMP 消息中,每个 OID 的子值称为索引。
// ReadAll reads from r until an error or EOF and returns the data it read. // A successful call returns err == nil, not err == EOF. Because ReadAll is // defined to read from src until EOF, it does not treat an EOF from Read // as an error to be reported. funcReadAll(r io.Reader)([]byte, error)
这条规则有一个例外; 您不需要注释实现接口的方法。 具体不要像下面这样做:
1 2
// Read implements the io.Reader interface func(r *FileReader)Read(buf []byte)(int, error)
// LimitReader returns a Reader that reads from r // but stops with EOF after n bytes. // The underlying implementation is a *LimitedReader. funcLimitReader(r Reader, n int64)Reader { return &LimitedReader{r, n} }
// A LimitedReader reads from R but limits the amount of // data returned to just N bytes. Each call to Read // updates N to reflect the new amount remaining. // Read returns EOF when N <= 0 or when the underlying R returns EOF. type LimitedReader struct { R Reader // underlying reader N int64// max bytes remaining }
// TODO(dfc) this is O(N^2), find a faster way to do this.
注释 username 不是该人承诺要解决该问题,但在解决问题时他们可能是最好的人选。 其他项目使用 TODO 与日期或问题编号来注释。
3.2.2. 与其注释一段代码,不如重构它
Good code is its own best documentation. As you’re about to add a comment, ask yourself, ‘How can I improve the code so that this comment isn’t needed?’ Improve the code and then document it to make it even clearer. 好的代码是最好的文档。 在即将添加注释时,请问下自己,“如何改进代码以便不需要此注释?’ 改进代码使其更清晰。 — Steve McConnell
Write shy code - modules that don’t reveal anything unnecessary to other modules and that don’t rely on other modules’ implementations. 编写谨慎的代码 - 不向其他模块透露任何不必要的模块,并且不依赖于其他模块的实现。 — Dave Thomas
每个 Go 语言的包实际上都是它一个小小的 Go 语言程序。 正如函数或方法的实现对调用者而言并不重要一样,包的公共API-其函数、方法以及类型的实现对于调用者来说也并不重要。
一个好的 Go 语言包应该具有低程度的源码级耦合,这样,随着项目的增长,对一个包的更改不会跨代码库级联。 这些世界末日的重构严格限制了代码库的变化率以及在该代码库中工作的成员的生产率。
在本节中,我们将讨论如何设计包,包括包的名称,命名类型以及编写方法和函数的技巧。
4.1. 一个好的包从它的名字开始
编写一个好的 Go 语言包从包的名称开始。将你的包名用一个词来描述它。
正如我在上一节中谈到变量的名称一样,包的名称也非常重要。我遵循的经验法则不是“我应该在这个包中放入什么类型的?”。相反,我要问是“该包提供的服务是什么?”通常这个问题的答案不是“这个包提供 X 类型”,而是“这个包提供 HTTP”。
几年前,我就对 functional options[7] 进行过讨论[6],使 API 更易用于默认用例。
本演讲的主旨是你应该为常见用例设计 API。 另一方面, API 不应要求调用者提供他们不在乎参数。
6.2.1. 不鼓励使用 nil 作为参数
本章开始时我建议是不要强迫提供给 API 的调用者他们不在乎的参数。 这就是我要说的为默认用例设计 API。
这是 net/http 包中的一个例子
1 2 3 4 5 6 7 8 9 10
package http
// ListenAndServe listens on the TCP network address addr and then calls // Serve with handler to handle requests on incoming connections. // Accepted connections are configured to enable TCP keep-alives. // // The handler is typically nil, in which case the DefaultServeMux is used. // // ListenAndServe always returns a non-nil error. funcListenAndServe(addr string, handler Handler)error {
举一个这类 API 的例子,最近我重构了一条逻辑,要求我设置一些额外的字段,如果一组参数中至少有一个非零。 逻辑看起来像这样:
1 2 3
if svc.MaxConnections > 0 || svc.MaxPendingRequests > 0 || svc.MaxRequests > 0 || svc.MaxRetries > 0 { // apply the non zero parameters }
由于 if 语句变得很长,我想将签出的逻辑拉入其自己的函数中。 这就是我提出的:
1 2 3 4 5 6 7 8 9
// anyPostive indicates if any value is greater than zero. funcanyPositive(values ...int)bool { for _, v := range values { if v > 0 { returntrue } } returnfalse }
这就能够向读者明确内部块的执行条件:
1 2 3
if anyPositive(svc.MaxConnections, svc.MaxPendingRequests, svc.MaxRequests, svc.MaxRetries) { // apply the non zero parameters }
// anyPostive indicates if any value is greater than zero. funcanyPositive(first int, rest ...int)bool { if first > 0 { returntrue } for _, v := range rest { if v > 0 { returntrue } } returnfalse }
现在不能使用少于一个参数来调用 anyPositive。
6.3. 让函数定义它们所需的行为
假设我需要编写一个将 Document 结构保存到磁盘的函数的任务。
1 2
// Save writes the contents of doc to the file f. funcSave(f *os.File, doc *Document)error
original error: *os.PathError open /Users/dfc/.settings.xml: no such file or directory stack trace: open /Users/dfc/.settings.xml: no such file or directory open failed main.ReadFile /Users/dfc/devel/practical-go/src/errors/readfile2.go:16 main.ReadConfig /Users/dfc/devel/practical-go/src/errors/readfile2.go:29 main.main /Users/dfc/devel/practical-go/src/errors/readfile2.go:35 runtime.main /Users/dfc/go/src/runtime/proc.go:201 runtime.goexit /Users/dfc/go/src/runtime/asm_amd64.s:1333 could not read config
使用 errors 包,你可以以人和机器都可检查的方式向错误值添加上下文。 如果昨天你来听我的演讲,你会知道这个库在被移植到即将发布的 Go 语言版本的标准库中。
8. 并发
由于 Go 语言的并发功能,经常被选作项目编程语言。 Go 语言团队已经竭尽全力以廉价(在硬件资源方面)和高性能来实现并发,但是 Go 语言的并发功能也可以被用来编写性能不高同时也不太可靠的代码。在结尾,我想留下一些建议,以避免 Go 语言的并发功能带来的一些陷阱。
Go 语言以 channels 以及 select 和 go 语句来支持并发。如果你已经从书籍或培训课程中正式学习了 Go 语言,你可能已经注意到并发部分始终是这些课程的最后一部分。这个研讨会也没有什么不同,我选择最后覆盖并发,好像它是 Go 程序员应该掌握的常规技能的额外补充。
这里有一个二分法; Go 语言的最大特点是简单、轻量级的并发模型。作为一种产品,我们的语言几乎只推广这个功能。另一方面,有一种说法认为并发使用起来实际上并不容易,否则作者不会把它作为他们书中的最后一章,我们也不会遗憾地来回顾其形成过程。
贴士: 许多 Go 程序员过度使用 goroutine,特别是刚开始时。与生活中的所有事情一样,适度是成功的关键。
8.2. 将并发性留给调用者
以下两个 API 有什么区别?
1 2
// ListDirectory returns the contents of dir. funcListDirectory(dir string)([]string, error)
1 2 3 4
// ListDirectory returns a channel over which // directory entries will be published. When the list // of entries is exhausted, the channel will be closed. funcListDirectory(dir string)chanstring
Go 是作为系统语言(例如:操作系统,设备驱动程序)创建的,因此它针对的是 C 和 C++ 开发人员。按照 Go 团队的说法,应用程序开发人员已经成为 Go 的主要用户而不是系统开发人员了,这个说法我也是相信的。为什么?我不能权威的代表系统开发人员说话,但对于我们这些构建网站,服务,桌面应用程序等的人来说,它可以部分的归结为对一类系统的新兴需求,这类系统介于低级系统应用程序和更高级的应用程序之间。
可能 Go 语言有消息传递机制,缓存,重计算数据分析,命令行接口,体制和监控,我不知道给 Go 语言什么样的标签,但是在我的职业生涯中,随着系统的复杂性不断增加,以及动辄成千上万的并发,显然对定制基础类型系统的需求不断增加。你可以使用 Ruby 或者 Python 构建这样的系统(大多人都这样做),但这些类型的系统可以从更严格的类型系统和更高的性能中受益。类似地,你可以使用 Go 来构建网站(很多人都愿意这样做),但我仍然喜欢 Node 或者 Ruby 对这类系统展现出的表现力。
Go 语言还擅长其他领域。比如,当运行一个编译过的 Go 程序时,他没有依赖性。你不必担心用户是安装了 Ruby 或者 JVM,如果这样,你还要考虑版本。出于这个原因,Go 作为命令行程序以及其他并发类型的工具(日志收集器)的开发语言变得越来越流行。
坦白来说,学习 Go 可以有效利用你的时间。你不必担心会花费很长时间学习 Go 甚至掌握它,你最终会从你的努力中得到一些实用的东西。
作者注解
对于写这本书我犹豫再三,主要有两个原因。第一个是 Go 有自己的文档,特别是 Effective Go。
另一个是在写一本关于语言类的书的时候我会有点不安。当我们写 《 The Little MongoDB Book》 这本书的时候,我完全假设大多数读者已经理解了关系型数据库和建模的基本知识。在写 《The Little Redis Book》这本书的时候,你也可以同样假设读者已经熟悉键值存储。
在我考虑未来的某些章节的时候,我知道不能再做出同样的假设。你花多长时间学习并理解接口,这是个新的概念,我希望你从中学到的不仅仅是 Go 有提供接口,并且还有如何使用它们。最终,我希望你向我反馈本书的哪部分讲得太细或者太粗,我会感到很欣慰,也算是我对读者们的小小要求了。
第一章 · 基础
入门
如果你想去尝试运行 Go 的代码,你可以去看看 Go Playground ,它可以在线运行你的代码并且不要安装任何东西。这也是你在 Go 的论坛区和 StackOverflow 等地方寻求帮助时分享 Go 代码的最常用方法。
Go 的安装很简单。你可以用源码去安装,但是我还是建议你使用其中一个预编译的二进制文件。当你 跳转到下载页面,你将会看到 Go 语言的在各个平台上的安装包。我们会避免这些东西并且学会如何在自己的平台上安装好 Go。正如你所看到的的那样,安装 Go 并不是很难。
除了一些简单的例子, Go 被设计成代码在工作区内运行。工作区是一个文件夹,这个文件夹由 bin ,pkg,以及src子文件夹组成的。你可能会试图强迫 Go 遵循自己的风格-不要这么去做。
一般,我把我的项目放在 ~/code 文件夹下。比如,~/code/blog 目录就包含了我的 blog 项目。对于 Go 来说,我的工作区域就是 ~/code/go ,然后我的 Go 写的项目代码就在 ~/code/go/src/blog 文件夹下。
简单来说,无论你希望把你的项目放在哪里,你最好创建一个 go 的文件夹,再在里面创建一个 src 的子文件夹。
保存文件并命名为 main.go 。 你可以将文件保存在任何地方;不必将这些琐碎的例子放在 go 的工作空间内。
接下来,打开一个 shell 或者终端提示符,进入到文件保存的目录内, 对于我而言, 应该输入 cd ~/code 进入到文件保存目录。
最后,通过敲入以下命令来运行程序:
1
go run main.go
如果一切正常(即你的 golang 环境配置的正确),你将看到 it’s over 9000! 。
但是编译步骤是怎么样的呢? go run 命令已经包含了编译和运行。它使用一个临时目录来构建程序,执行完然后清理掉临时目录。你可以执行以下命令来查看临时文件的位置:
1
go run --work main.go
明确要编译代码的话,使用 go build:
1
go build main.go
这将产生一个可执行文件,名为 main ,你可以执行该文件。如果是在 Linux / OSX 系统中,别忘了使用 ./ 前缀来执行,也就是输入 ./main 。
在开发中,你既可以使用 go run 也可以使用 go build 。但当你正式部署代码的时候,你应该部署通过 go build 产生的二进制文件并且执行它。
入口函数 Main
希望刚才执行的代码是可以理解的。我们刚刚创建了一个函数,并且使用内置函数 println 打印出了字符串。难道仅因为这里只有一个选择,所以 go run 知道执行什么吗??不。在 go 中程序入口必须是 main 函数,并且在 main 包内。
我们将在后面的章节中详细介绍包。目前,我们将专注于理解 go 基础,一直会在 main 包中写代码。
如果你想尝试,你可以修改代码并且可以更改包名。使用 go run 执行程序将出现一个错误。 接着你可以将包名改回 main ,换一个不同的方法名,你会看到一个不同的错误。尝试使用 go build 代替 go run 来执行刚才的代码,注意代码编译时,没有入口点可以执行。但当你构建一个库时,确实完全正确的。
导入包
Go 有很多内建函数,例如 println,可以在没有引用情况下直接使用。但是,如果不使用 Go 的标准库直接使用第三方库,我们就无法走的更远。import 关键字被用于去声明文件中代码要使用的包。
我们现在用了 Go 的两个标准包:fmt 和 os 。我们也介绍了另一个内建函数 len 。len 返回字符串的长度,字典值的数量,或者我们这里看到的,它返回了数组元素的数量。如果你想知道我们这里为什么期望得到两个参数,它是因为第一个参数 – 索引0处 – 总是当前可运行程序的路径。(更改程序将它打印出来亲自看看就知道了)
同样很明显的是,复制一个指针比复制一个复杂的结构的消耗小多了。在 64 位的机器上面,一个指针占据 64 bit 的空间。如果我们有一个包含很多字段的结构,创建它的副本将会是一个很昂贵的操作。指针的真正价值在于能够分享它所指向的值。我们是想让 Super 修改 goku 的副本还是修改共享的 goku 值本身呢?
➜ ~ go mod Go mod provides access to operations on modules.
Note that support for modules is built into all the go commands, not just 'go mod'. For example, day-to-day adding, removing, upgrading, and downgrading of dependencies should be done using 'go get'. See 'go help modules' for an overview of module functionality.
Usage:
go mod <command> [arguments]
The commands are:
download download modules to local cache edit edit go.mod from tools or scripts graph print module requirement graph init initialize new module in current directory tidy add missing and remove unused modules vendor make vendored copy of dependencies verify verify dependencies have expected content why explain why packages or modules are needed
Use "go help mod <command>" for more information about a command.
go mod download: 下载依赖的module到本地cache
go mod edit: 编辑go.mod
go mod graph: 打印模块依赖图
go mod init: 在当前目录下初始化go.mod(就是会新建一个go.mod文件)
go mod tidy: 整理依赖关系,会添加丢失的module,删除不需要的module
go mod vender: 将依赖复制到vendor下
go mod verify: 校验依赖
go mod why: 解释为什么需要依赖
在新项目中使用
使用go mod并不要求你的项目源码放到$GOPATH下,所以你的新项目可以放到任意你喜欢的路径。在项目根目录下执行go mod init,会生成一个go.mod文件。然后你可以在其中增加你的依赖,如下:
Run groovy GithubTopRankCrawler.groovy -l go -d <path to store the 1000 repos> to clone all repositories locally. You can use -s to do the shallow clone and decrease disk usage.
Run groovy GoBuildToolScanner.groovy <path to store the 1000 repos> to analyze these repos.
funclistById(handler Handler) { var cursor = 0 for { sql := "select id, dianping_id from t_dianping_reply " if cursor > 0 { sql += " where id > " + strconv.Itoa(cursor) } sql += " order by id asc limit 10 "
rows, err := db.Query(sql) if err != nil { panic(err) } else { for rows.Next() { var id, dianpingId string
rows.Scan(&id, &dianpingId)
handler(id, dianpingId)
cursor, _ = strconv.Atoi(id) } } rows.Close() } }
funcgetCount(dianpingId string)int { rows, err := db.Query("select count(*) from t_dianping_reply where dianping_id = ? and status = 0", dianpingId) count := 0 if err != nil { panic(err) return count } if rows.Next() { rows.Scan(&count) } rows.Close() return count }