Skip to main content

practical go

📅 2026-02-05 ✏️ 2026-03-08 CS GO
No related notes

https://dave.cheney.net/practical-go/presentations/qcon-china.html

1 · 实践go:真实世界中编写可维护go程序的建议#

介绍:你好,接下来两个阶段,我的目标是给你一些我的建议,关于编写Go代码的最佳实践。
这是一个研讨会形式的展示,我将省去寻常的幻灯片,我们将直接从今天你可以带走的文档开始工作。

1.1 · 1. 指导准则#

将最佳实践,就需要定义最佳,用RussCox的名言:
Software engineering is what happens to programming when you add time and other programmers. — Russ Cox
指出了软件程序与软件工程的区别。以前自己为自己写程序,后来多人合作花时间写程序。工程师来来走走,团队扩张或收缩,需求会改变,新功能会添加,缺陷会修复。这就是软件工程的本质。
指导准则:简单、可读、生产
note:为什么没有提到性能、并发。有一些语言是比go快一些的,但是绝对的没有go简单;有一些语言,将并发视作最高的目标,但是既不具有可读性、也不具有生产性。性能和并发是重要的属性,但是没有简单、可读、生成 重要。

1.1.1 · 简单

Simplicity is prerequisite for reliability. — Edsger W. Dijkstra
简单是可靠的前提。
为什么要追求简单?为什么go程序变简单很重要?
不理解代码,害怕修改代码,担心破坏程序,不知道怎么修复。

There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult. — C. A. R. Hoare
有两种方法去构建一个软件设计:其一,使其特别简单,所以明显没有缺点;其二,使其特别复杂,所以没有明显缺点。第一种方法特别难。
复制性使可靠的软件变的不可靠。复制性会杀死软件项目。所以简单性是go的最高目标,无论我们写什么程序,必须承认是很简单的。

1.1.2 · 可读

Readability is essential for maintainability. — Mark Reinhold
对于可维护性来说,可读性很重要。
为什么我们追求可读性。
Programs must be written for people to read, and only incidentally for machines to execute — Hal Abelson and Gerald Sussman
程序必须是为了人们读而写,仅仅顺便可以被机器执行。
一段程序也许会因为被许多人读,而超过其生命周期。
可读性是人们理解程序如何运行的关键,不能理解,就不可维护,那就需要重写。

1.1.3 · 生产

Design is the art of arranging code to work today, and be changeable forever. — Sandi Metz
设计是一门艺术,安排代码今天工作起来,并且永远可改变。

1.2 · 2. 标识符#

1.2.1 · 标识符长度

变量在声明和最后一次使用之间的距离很短时,短变量名可以工作得很好。  
长变量名需要证明自己的合理性,它们越长,需要提供的价值就越多。冗长的官僚主义变量名与它们在页面上的份量相比,传达的信息量很低。  
不要在变量名中包含变量类型的名称。  
常量应该描述它们所持有的值,而不是如何使用该值。
循环和分支使用单字母变量,参数和返回值使用单个单词,函数和包级别声明使用多个单词。  
对于方法、接口和包,更喜欢使用单个单词。  
请记住,包的名称是调用者调用名称的一部分,用来引用这个包,所以要利用它。  
使用空白行来分解函数的流程,就像使用段落分解文章的流程一样。

1.2.2 · 上下文是关键

1.2.3 · 不要在变量命名中加上类型

给猫狗命名不会带上猫、狗等字眼。

包名和变量名不要重复。

1.2.4 · 使用一致的命名风格

命名可预料,当阅读者第一次阅读时可以理解。
Go风格指示方法的接收者是单个字母名称,或从其类型派生的首字母缩写词。

1.2.5 · 使用一致的声明风格

go有六种不同的方法去声明变量:
var x int = 1
var x = 1
var x int;x = 1
var x = int(1)
x := 1 

1. 申明变量,不进行初始化,使用var,为零值,与包级别使用var关键字一致 : var players int ;var things []Thing
2. 声明变量,且进行初始化,使用 := ,让读者看到变量是刻意被初始化的: 
3. 使棘手的声明更加明显: var length uint32 = 0x80  

1.2.6 · 成为团队合作者

保持和团队的风格一致,推荐一开始使用gofmt。

1.3 · 3. 注释#

Good code has lots of comments, bad code requires lots of comments. — Dave Thomas and Andrew Hunt
注释对于代码的可读性是非常重要的。每一个注释必须做到以下三点之一:
1. waht,注释应该解释做了什么;  // 在公开的符号是理想形式
2. how,注释应该解释怎么做的;  // 在方法内部是理想形式
3. why,注释应该解释为什么;    // 解释代码的外部因素,比如上下文

1.3.1 · 变量和常量的注释应该描述其内容,而不是目的

给变量或者常量命名的时候,需要描述其目的;
而给变量或者常量注释的时候,需要描述其内容。
对于一个没有初始化的变量,注释应该描述由谁来负责初始化这个变量;
当变量命名需要注释解释的时候,也许最好的变量名就在注释里面,此时,注释就是多余的。

1.3.2 · 公开符号始终需要注释

godoc可以看到包的文档,因此需要对所有的公开符号进行注释--声明在此包内的变量、常量、函数、方法

google风格的指导规则:
1. 任何公开的函数不显而易见或简短,必须添加注释
2. 任何库中的函数,不管长度与复杂度,必须添加注释
有一个意外,实现接口的方法,不需要注释;在你实现函数之前,添加其注释,如果你觉得写注释很困难,那说明你打算编写的代码是不易理解的。

1.3.2.1 · 不要注释坏代码,重写它

Don’t comment bad code — rewrite it. — Brian Kernighan

1.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
好的代码就是最好的注释,当添加注释的时候,自己问下自己,怎么可以提高代码可读性,使得注释是不需要的。
函数应该只做一件事,抽取函数。

1.4 · 4. 包设计#

使用接口描述你的方法或函数所需要的行为。
阻止使用全局状态。

1.5 · 5. 项目结构#

开始的时候,一个包对应一个go文件,把这个go文件放到与包名相同的文件夹中;
包开始增大,根据不同职责拆分成不同go文件;
不同go文件负责包的不同部分。

首选名词作为go文件名。
go编译器并行编译每一个包,在一个包内,并行编译函数,在一个包内改变代码布局不影响编译时间。

1.6 · 6. API设计#

APIs should be easy to use and hard to misuse. — Josh Bloch

1.6.1 · 注意函数多个参数为相同类型时

1.6.2 · 针对其默认用例设计API#

1.6.3 · 让函数定义它们所需要的行为,排除其他不需要的

接口描述行为,接口隔离原理,限定了某种类型。

1.7 · 7. 错误处理#

1.7.1 · 通过消除错误消除错误处理

改变代码,避免错误,从而避免错误处理。

1.8 · 8. 并发#

1.8.1 · 使自己保持忙碌状态或者自己完成工作

预期让别人代理,等待结果,不如自己做。

1.8.2 · 将并发留给调用者

如果开启了一个goroutine,需要显示的提供暂停goroutine的方法给调用者。

1.8.3 · 如果不知道要开启的goroutine什么时候结束,那就不要开启它。#

goroutine内部等待外部的stop channel的关闭,从而停止goroutine内部的逻辑;
外部stop channel等待goroutine处理完成,传送done信息给外部,从而外部stop channel关闭。
	func serve(addr string, handler http.Handler, stop <-chan struct{}) error {
		s := http.Server{
			Addr:    addr,
			Handler: handler,
		}

		go func() {
			<-stop // wait for stop signal
			s.Shutdown(context.Background())
		}()

		return s.ListenAndServe()
	}

	func serveApp(stop <-chan struct{}) error {
		mux := http.NewServeMux()
		mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
			fmt.Fprintln(resp, "Hello, QCon!")
		})
		return serve("0.0.0.0:8080", mux, stop)
	}

	func serveDebug(stop <-chan struct{}) error {
		return serve("127.0.0.1:8001", http.DefaultServeMux, stop)
	}

	func main() {
		done := make(chan error, 2)
		stop := make(chan struct{})
		go func() {
			done <- serveDebug(stop)
		}()
		go func() {
			done <- serveApp(stop)
		}()

		var stopped bool
		for i := 0; i < cap(done); i++ {
			if err := <-done; err != nil {
				fmt.Println("error: %v", err)
			}
			if !stopped {
				stopped = true
				close(stop)
			}
		}
	}