---
tags: golang
slideOptions:
transition: slide
---
# Practical Go: Real world advice for writing maintainable Go programs
> [original english version](https://dave.cheney.net/practical-go/presentations/qcon-china.html)
> [simple chinese version](https://github.com/llitfkitfk/go-best-practice)
:::warning
文長,會想睡覺,請打起精神
:::
---
# Table of contents
1. Guiding principles
2. Identifiers
3. Comments
4. Package Design
5. Project Structure
6. API Design
7. Error handling
8. Concurrency
---
> $Software\ engineering=Programming\ +\ Time\ +\ other\ programmers$
> [name=Go team lead, [Russ Cox](https://twitter.com/_rsc)]
---
# 1. Guiding principles
> the guiding principles are underlying Go itself
1. Simplicity
2. Readability
3. Productivity
> The joke goes that Go was designed while waiting for a C++ program to compile.
---
# 2. Identifiers
> An identifier is a fancy word for a **name**; the name of a variable, the name of a function, the name of a method, the name of a type, the name of a package, and so on.
## 2.1 Choose identifiers for clarity, not brevity
> Good naming is like a good joke. If you have to explain it, it’s not funny.
> [name=[Dave Cheney](https://twitter.com/davecheney/status/997155238929842176)]
- **A good name is concise.**
- **A good name is descriptive.**
- 若為一般變數,應描述**應用層的意義**
- 若為一個函數/方法/行為的輸出,描述它的**結果**
- 若為一個 packge,描述它的**目的**
- **A good name is should be predictable.**
- 看到該命名,就知道該怎麼使用它、而不會誤用
以下將根據上述原則來舉例。
## 2.2. Identifier length
**原則:**
1. **宣告**(declaration)的地方與**最後一次使用**的地方離得越近,命名越短;反之越長
2. **不要**在命名中包含資料型態(data type)
3. **Constants** 的命名只要告訴別人內容(only noun),不用告訴別人怎麼用(no verb)
4. **loop 和 if statement** 中的變數盡量使用**單字母**
5. **參數、回傳值、方法、界面(interface)、package** 盡量使用**單詞**
6. 何時考慮使用**多個單詞**命名?**函數與 package**的等級
7. 同一行,不要長短命名混用。i.e. 同時有**單詞**與**單字母**
**舉例:**
```go=
type Person struct {
Name string
Age int
}
// AverageAge returns the average age of people.
func AverageAge(people []Person) int {
if len(people) == 0 {
return 0
}
var count, sum int
for _, p := range people {
sum += p.Age
count += 1
}
return sum / count
}
```
- line 12: `p` - 宣告與使用離得超近
- `people`, `count` 和 `sum` 存活許多行,故給個好辨認的命名
- Use `blank lines` to break up the flow of a function
### 2.2.1 Context is key
```go=
for index := 0; index < len(s); index++ {
//
}
```
**vs.**
```go=
for i := 0; i < len(s); i++ {
//
}
```
上例相較下例,並沒有真的增加可讀性,故請避免這種冗餘的命名行為
## 2.3 Don’t name your variables for their types
> You don’t name your pets "dog" and "cat"
**Bad example 1**
```go=
var usersMap map[string]*User
```
> *Go is a statically typed language.*
> 如果改成 `users` 還是覺得不夠具有描述性?那麼 `userMap` 也一定沒有
**Bad example 2**
```go=
type Config struct {
//
}
func WriteConfig(w io.Writer, config *Config)
```
- `WriteConfig` 中的 `Config` 冗餘,應直接使用 `Write`
- 又參數中的 `config` 應使用 `conf` 或 `c` 即可,也避免長短字命名混用
## 2.4. Use a consistent naming style
- 為了讓你的程式碼命名具有可預測性的條件還有**自我命名風格必須一致**
- 若你有個物件`*sql.DB`會一直出現在程式碼中,若沒意義上的改變,不要每次命名變數時都不一樣
- e.g. `d *sql.DB`, `dbase *sql.DB`, `DB *sql.DB`, 或 `database *sql.DB`
- 應該保持一致為: **`db *sql.DB`**
- **慣用變數命名原則**
- loop induction variable
- **`i`**, **`j`**, **`k`**
> 如果你發現你的巢狀迴圈 i, j, k 不夠用,表示你的 function 必須要重構!
- counter or accumulator
- **`n`**
- value in generic encoding function(通用編碼函數,是啥?)
- **`v`**
- key of a map
- **`k`**
- shorthand for parameters of type `string`
- **`s`**
## 2.5. Use a consistent declaration style
**Go 中至少有以下五種方式可以宣告並賦值:**
```go=
var x int = 1
var x = 1
var x int; x = 1
var x = int(1)
x := 1
```
**該使用何種風格?**
> **TL;DR**
> - 宣告,但在未來才要賦值,使用 **`var`**
> - 宣告,同時已能明確給予初始值,使用 **`:=`**
**有例外嗎?**
> **When something is complicated, it should look complicated.**
```go=
var length uint32 = 0x80
```
- 可能在你的某個 package 中,透過此種違反慣例的方式提示讀者:在此 package 的 `length` 是 `unit32` 的型態唷!
## 2.6. ***Be a team player***
- 未來一定有非常多機會參與妳不是唯一作者的專案,讓自己能夠融入該團隊的風格吧!
- 不要輕易地去改變團隊既有的風格,**別添亂**。只要一樣滿足 `gofmt` 的要求就不要輕易地去改變團隊的風格
---
# 3. Comments
**註解**在撰寫 Go 時扮演舉足輕重的角色
> Good code has lots of comments, bad code requires lots of comments.
> [name=Dave Thomas and Andrew Hunt, The Pragmatic Programmer]
撰寫有意義的註解,需要回答以下問題之一:
1. **What** - 這段 code 打算做什麼?
2. **How** - 如何做這件事?
3. **Why** - 為何做這件事?
**What** 適合寫在 ***public symbols***:
```go
// Open opens the named file for reading.
// If successful, methods on the returned file can be used for reading.
```
**How** 適合在 method 內部註釋:
```go
// queue all dependant actions
var results []chan error
for _, dep := range a.Deps {
results = append(results, execute(seen, dep))
}
```
**Why** 用來解釋一些從程式碼上下文無法快速參透的「外部因素」。
例如下例的 `HealthyPanicThreshold` 被設成 `0`,沒有註解(`// Disable HealthyPanicThreshold`)就不知道 `0` 是什麼含意。
```go
return &v2.Cluster_CommonLbConfig{
// Disable HealthyPanicThreshold
HealthyPanicThreshold: &envoy_type.Percent{
Value: 0,
},
}
```
## 3.1 Comments on variables and constants should describe their ***contents*** not their ***purpose***
**舉例:**
```go
const randomNumber = 6 // determined from an unbiased die
```
```go
const (
StatusContinue = 100 // RFC 7231, 6.2.1
StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
StatusProcessing = 102 // RFC 2518, 10.1
StatusOK = 200 // RFC 7231, 6.3.1
```
**有例外嗎?**
當變數並未被明確指定初始值時,此時你應該提到誰負責維護此變數的狀態:
```go
// sizeCalculationDisabled indicates whether it is safe
// to calculate Types' widths and alignments. See dowidth.
var sizeCalculationDisabled bool
```
但如果有更好的命名,就可不用註解。**e.g.**
```go
// registry of SQL drivers
var registry = make(map[string]*sql.Driver)
```
A better way to describe register of what:
```go
var sqlDrivers = make(map[string]*sql.Driver)
```
## 3.2 **Always** document public symbols
根據 Google Style Guide
- 任何不夠簡短也不夠明顯的 **public function** 都應該註解
- 任何在 library/package 內的 functions 都應該註解(無論長度與複雜度)
> 呃,那不就是所有的 function/method 都註解?
以 `io` package 內的註解為例:
```go!
// LimitReader returns a Reader that reads from r
// but stops with EOF after n bytes.
// The underlying implementation is a *LimitedReader.
func LimitReader(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
}
func (l *LimitedReader) Read(p []byte) (n int, err error) {
if l.N <= 0 {
return 0, EOF
}
if int64(len(p)) > l.N {
p = p[0:l.N]
}
n, err = l.R.Read(p)
l.N -= int64(n)
return
}
```
> 為何 Read 不需要註解?因為在此模組中的其他地方已有此 interface method 的註解,且在理解 `LimitReader` 與 `LimitedReader` 中的註解時也能不言而喻。
> **Reminder-** `Reader` is an **interface***
> ```go!
> // Reader is the interface that wraps the basic Read method.
> ...
> // Implementations must not retain p.
> type Reader interface {
> Read(p []byte) (n int, err error)
> }
> ```
那麼,什麼時候反而**不要註解**呢?
### 3.2.0 Document methods that implement an interface
若你只是註解「A 實作了 B 的介面」,這等於沒寫。
別做出下面這種註解:
```go
// Read implements the io.Reader interface
func (r *FileReader) Read(buf []byte) (int, error)
```
### 3.2.1. Don’t comment bad code, rewrite it
> Don’t comment bad code — rewrite it
> [name=Brian Kernighan]
原則如上,但常常因時程壓力,必須註解一段具有技術債 (technical debt) 的程式碼,除此之外最好多加註 `TODO` 與 `username` 來提醒讀者注意。
```go!
// TODO(John) this is O(N^2), find a faster way to do this.
```
其中,`John` 不一定是實作者,至少是釐清上下文的最佳諮詢對象。
### 3.2.2. Rather than commenting a block of code, refactor it
> **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.
> [name=Steve McConnell]
根據 [SOLID](https://en.wikipedia.org/wiki/SOLID) 中的 [S、單一職責原則](https://en.wikipedia.org/wiki/Single-responsibility_principle),一個 function 只要求做好一件事。
當你打破此原則時,屆時就會發現會需要在 function 中許多處加註解。
若能夠確實要求,function 通常也都足夠小且獨立,並易於測試。
通常此時你的 function 可能也就不需要額外的註解就足夠說明意圖了。
---
# 4. Package Design
> **Write shy code** - modules that **don't reveal anything unnecessary** to other modules and that **don't rely on other modules' implementations.**
> [name=[Dave Thomas](https://twitter.com/codewisdom/status/1045305561317888000?s=12)]
一個好的 Go package 應在開發過程盡力降低與其他原始碼的耦合 (coupling)、也不要暴露過多對他人無用的程式碼。
## 4.1 A good package starts with its name
好的 package 的命名,應試著回答以下問題:
> **此 package 提供何種服務?**
>> *Name your package for **what it provides**, **not what it contains.***
### 4.1.1 Good package names should be unique.
若發現你有兩個 packages 命名太相似或相同,可能是因為:
1. 名稱太通用 (too generic),請重新命名
2. 重新檢視你的架構,功能可能重複,精簡它或合併它
## 4.2 Avoid package names like `base`, `common`, or `util`
免不了會有一些 utility 或 helper 之類的程式碼預期會被多個 package 共用。你可能會因此將這些功能集中到以 `utils` 或 `helpers` 命名的 packages。
**建議不要這麼做**,而是**允許這些程式碼重複**,在被呼叫的 package 中都各自擁有這些 function,讓這些 function 的用途、目的能夠顯而易見。
> [A little] duplication is far cheaper than the wrong abstraction.
> [name=Sandy Metz]
>
## 4.3 Return early rather than nesting deeply
**Golang 沒有 try/catch/exception 的功能**,所以無需為此提供一個階層式架構、只為了在最上層使用 try catch block。
建議的實作模式為:**出錯就 return**,此技巧稱 ***guard clauses***。
**舉例:**
在 `bytes` package 中使用了 guard clauses 的寫法:
```go!
func (b *Buffer) UnreadRune() error {
if b.lastRead <= opInvalid {
return errors.New("bytes.Buffer: UnreadRune: previous operation was not a successful ReadRune")
}
if b.off >= int(b.lastRead) {
b.off -= int(b.lastRead)
}
b.lastRead = opInvalid
return nil
}
```
同樣的 function 若***不使用 guard clause***:
```go!
func (b *Buffer) UnreadRune() error {
if b.lastRead > opInvalid {
if b.off >= int(b.lastRead) {
b.off -= int(b.lastRead)
}
b.lastRead = opInvalid
return nil
}
return errors.New("bytes.Buffer: UnreadRune: previous operation was not a successful ReadRune")
}
```
作為讀者你可以感覺到,造成你較多的認知負荷,且此種寫法相對來說真的較容易出 bug。
## 4.4 Make the zero value useful
Go 替每個 primitive data type 都設計***zero value*** 的機制、當你只**宣告**但**未明確初始化**變數時 (explicit initialisation)。
- 數值型態 (numeric types) 的 *zero value* 就是 `0`
- 指標型態 (pointer types) 的 *zero value* 則是 `nil`。例如 slices, maps 和 channels 等
為了程式的正確性,永遠記得替你的變數給一個**有用的初始值**、或**使 zero value 在你的 package/struct 設計中有用處**。
**舉例:**
**`sync.Mutex`**

**`bytes.Buffer`**

再觀察 slice 在 [runtime](https://golang.org/src/runtime/slice.go) 中的定義的話:
```go
type slice struct {
array *[...]T // pointer to the underlying array
len int
cap int
}
```
就可以知道僅宣告,裏頭的成員皆有自己的 *zero value*,使你可以僅宣告就開始使用也不會出錯:
```go=
var s []string // just declaration
s = append(s, "Hello")
s = append(s, "world")
fmt.Println(strings.Join(s, " "))
```
> 但你可以用以下程式碼觀察僅宣告以及有初始化的 slice 的不同:
> ```go=
> var s1 = []string{} // same as s1 := make([]string, 0)
> var s2 []string
> if s1 != nil {
> fmt.Println("s1 is not nil")
> }
> if s2 == nil {
> fmt.Println("s2 is nil")
> }
> fmt.Println(reflect.DeepEqual(s1, s2)) // false
> ```
>
另外,指標型態的 *zero value* 為 nil 還有個好處是,你一樣可以呼叫 `nil` 的方法,並且在該方法中處理當傳入的指標為`nil`時該怎麼辦:
```go=
type Config struct {
path string
}
func (c *Config) Path() string {
if c == nil {
return "/usr/home"
}
return c.path
}
func main() {
var c1 *Config
var c2 = &Config{
path: "/export",
}
fmt.Println(c1.Path(), c2.Path())
// will print: /usr/home /export
}
```
## 4.5 Avoid package level state
一個維護性佳的程式為盡可能地**低耦合 (loosely coupled)**,以此避免改動一個 package、影響了另一個 package。
**指導方針:**
1. 使用 `interface` 來描述你想要的 functions/methods
2. 避免使用 **global state**
當你在一個 go file 中宣告了一個**首字大寫的變數**,它將會成為**整個程式中的一個全域變數 (global variable)**,**任何時候都看得到**,且任何一處都能夠改動它。
這將造成你原本彼此獨立的程式之間出現了高度耦合 (tight coupling)。
若想要降低這種耦合性,建議的做法為:
1. 將此變數用 `struct` 封裝
2. 使用 `interface` 來定義可操作此變數的行為
---
# 5. Project Structure
將多個 **packages** 放在一起,以下會稱之為 **project** 或 **module**,並且使用單一的 git repository 儲存。
每個 project 的命名,一樣,必須讓讀者明確知道其目的 (purpose)。
## 5.1. Consider fewer, larger packages
Go 不像其他語言提供許多能見度 (visibility) 相關的語法
- Java: `public`, `protected`, `private`
- C++: `friend` class
Go 很單純,只有區分 **public** 與 **private**,且簡單地使用**首字是否大寫**來區別。只要是 public、首字大寫的,就能夠被其他程式碼存取。
> 或者有的人會說 ***exported*** 和 ***not exported***
>
### 5.1.1. Arrange code into files by *import* statements
那到底該遵循什麼樣的準則來整理這些程式碼檔案呢?用 **`import` 語句**當作提示。
**指導方針:**
1. 使用**名詞**作為 package 名稱
1. 每當開始寫一個 package 時,先創建一個同名資料夾,再將該同名 go file 放置其中
- e.g. 我要寫一個 `package http`,就創建 `http` 資料夾,並寫在資料夾裡的 `http.go`
2. 當 package 越寫越大,那就適度分離出不同職責的 package 吧!
e.g.
- 將 `Client` type 放在 `client.go` 中
- 將 `Server` type 放在 `server.go` 中
- 將 `Request` 與 `Respons` types 放在 `messages.go` 中
3. 隨著開發工作的進行,再觀察,是否有**多個 packages 的 `import` 語句相似**,表示可能需要合併、並將真正相異的部分抽離
### 5.1.2. Prefer internal tests to external tests
- [internal tests vs. external tests](https://dev.to/julianchu/go-internal-vs-external-testing-27hg)
- 做 unit tests 時,使用 internal test
- 因為這樣可以對包含 private function/method 在內的所有內容做測試
- 若有一些 `Example` function 的話,使用 external tests,將有利於未來在 [godoc](https://godoc.org/) 中被查看、複製取用
### 5.1.3. Use `internal` packages to reduce your public API surface
若你**不想讓 public APIs 太 public**、只想要給特定的 packages 看到,能怎麼做?
> 將這種 package 放在 `internal/` 內
舉個例子,如果你的 package 放在 `…/a/b/c/internal/d/e/f`,那麼就只有 directory tree 為 `…/a/b/c` 的程式碼可以存取得到;**`…/a/b/g` 與其他的 project 皆無法存取**。
## 5.2. Keep package main small as small as possible
`main` 通常扮演著 singleton 的角色,在整個程式裡**是唯一的存在**。
又 `main` 在整個程式中只會被執行一次,故若要替 `main` 寫測試會非常困難,故須將業務邏輯從 `main` 中移出。
**那麼通常 `main` 負責什麼事情?**
1. parse [flags](https://golang.org/pkg/flag/)
2. open connections to databases
3. open loggers
然後再將這些物件交給別人去處理。
---
# 6. API Design
此節為最重要的**程式設計建議**,前面都只是**軟性建議**,就算沒達成後果可能只影響你自己、不會有什麼向下相容的問題。
但此節討論的 (public) API 不一樣,若不在一開始就嚴謹看待,未來要做更改影響的就一定會影響使用者。
## 6.1. Design APIs that are hard to misuse.
> APIs should be easy to use and hard to misuse.
> [name=[Josh Bloch](https://www.infoq.com/articles/API-Design-Joshua-Bloch/)]
>
### 6.1.1. Be wary of functions which take several parameters of the same type
> ***TL;DR***
> APIs with multiple parameters of the same type are hard to use correctly.
>
一個造成他人容易誤用的特徵:function 要求**多個同型別的參數**。
你心想,怎麼會呢?
**舉例:**
```go=
func Max(a, b int) int
func CopyFile(to, from string) error
```
對 `Max` 來說,因具有交換律 (commutative),故參數前後顛倒都沒關係:
```go=
Max(8, 10) // 10
Max(10, 8) // 10
```
但對 `CopyFile` 來說,若不偷看定義或註解,你能總是從以下的例子中知道誰才是**來源**與**目的地**嗎?
```go
CopyFile("/tmp/backup", "presentation.md")
CopyFile("presentation.md", "/tmp/backup")
```
對於 `CopyFile` 的情境,在此拋轉引玉、給一種解法給大家思考:
```go=
type Source string
func (src Source) CopyTo(dest string) error {
return CopyFile(dest, string(src))
}
func main() {
var from Source = "presentation.md"
from.CopyTo("/tmp/backup")
}
```
此解法利用 `interface` 來限制 caller 可能的情境,使得 `CopyFile` 總是被正確呼叫。unit test 也好寫了、甚至能夠將 `CopyFile` 換成 private function 避免誤用。
## 6.2. Design APIs for their default use case
### 6.2.0 Functional options for friendly APIs
> References:
> - [functional options](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis)
> - [another chinese explaination](https://blog.csdn.net/liyunlong41/article/details/89048382)
Dave Cheney 認為,API 應該要非常容易使用,尤其要在 **default case** 時亦是如此。
換句話說,你不該強迫使用者提供他們不關心的參數給 function。
> Go 沒有 [default arguments](https://en.wikipedia.org/wiki/Default_argument) 的設計。2017 年的 [issue](https://github.com/golang/go/issues/21909) 也稍微討論過此事。
>> 例如像 Python:
>> ```python
>> def connect(host="127.0.0.1", port=5432):
>> # do something
>> ```
所以什麼是 functional options 呢?
需求是:
1. caller 如我不想要給一堆參數,想要預設時不想給 `nil`
2. 不想要寫一堆 `if conf = nil {}` 來處理預設
**舉例:**
```go=
package main
import (
"errors"
"fmt"
)
type dbConfig struct {
Host string
Port int
Table string
}
type client struct {
*dbConfig
}
func newConnect(conf *dbConfig) (*client, error) {
// do some necessary process
return &client{dbConfig: conf}, nil
}
type dbConfigOption func(c *dbConfig) error
func setHost(host string) dbConfigOption {
return func(c *dbConfig) error {
if c == nil {
return errors.New("given config is nil")
}
c.Host = host
return nil
}
}
func setPort(port int) dbConfigOption {
return func(c *dbConfig) error {
if c == nil {
return errors.New("given config is nil")
}
c.Port = port
return nil
}
}
func setTable(table string) dbConfigOption {
return func(c *dbConfig) error {
if c == nil {
return errors.New("given config is nil")
}
c.Table = table
return nil
}
}
func wrapConnect(options ...dbConfigOption) (*client, error) {
// default configuration
conf := &dbConfig{
Host: "127.0.0.1",
Port: 5432,
Table: "tx",
}
for _, f := range options {
err := f(conf)
if err != nil {
// do some error handling
fmt.Println(err)
}
}
return newConnect(conf)
}
func main() {
db, err := wrapConnect(setHost("192.168.1.130"), setPort(1234))
// db, err := wrapConnect(setHost("192.168.1.130"))
// db, err := wrapConnect()
fmt.Println(db.Host)
fmt.Println(db.Port)
fmt.Println(db.Table)
fmt.Println(err)
}
```
### 6.2.1. Discourage the use of nil as a parameter
> **TL;DL**
> 避免允許使用者傳入`nil`來作為「選擇預設值」的選擇,應該讓使用者創建一個 default 物件傳入。
### 6.2.2. Prefer var args to []T parameters
你一定會有需要此種設計的時候:
```go
func ShutdownVMs(ids []string) error
```
因為傳入參數是一個 slice,故表示允許傳入 **empty slice** 或 **`nil`**,因此造成了**額外的測試負擔**。
再舉個例子,以下的程式碼片段需要重構:
```go!
if svc.MaxConnections > 0 || svc.MaxPendingRequests > 0 || svc.MaxRequests > 0 || svc.MaxRetries > 0 {
// apply the non zero parameters
}
```
考慮到 `if` statement 越來越長,所以將此邏輯抽離出來寫一個 function 如下:
```go!
// anyPostive indicates if any value is greater than zero.
func anyPositive(values ...int) bool {
for _, v := range values {
if v > 0 {
return true
}
}
return false
}
if anyPositive(svc.MaxConnections, svc.MaxPendingRequests, svc.MaxRequests, svc.MaxRetries) {
// apply the non zero parameters
}
```
但有沒有可能此 `anyPositive` 被誤用了呢?
```go
if anyPositive() { ... }
```
雖然此例仍然回傳 `false`,不會造成太大的問題。**但如果回傳 `true` 該怎麼辦?**
建議的最佳實踐為,限制 caller 一定得給至少一個參數,來避免誤用或不如預期的輸出:
```go
// anyPostive indicates if any value is greater than zero.
func anyPositive(first int, rest ...int) bool {
if first > 0 {
return true
}
for _, v := range rest {
if v > 0 {
return true
}
}
return false
}
```
## 6.3. Let functions define the behaviour they requires
> **TL;DR**
> 只傳遞必要的資訊進去 function,能讓人更明確地知道如何使用它
來看一個需求例,此例也闡述如何一步步重構:我想要把一段 content 存到一個 file 去,可以這樣寫:
```go
// Save writes the contents of doc to the file f.
func Save(f *os.File, doc *Document) error
```
但問題是,`*os.File` 定義了許多與 `Save` 無關的功能,如果能夠在 signature 上就明確規範好行為會更好。所以可以進一步改寫成:
```go
// Save writes the contents of doc to the supplied
// ReadWriterCloser.
func Save(rwc io.ReadWriteCloser, doc *Document) error
```
又再考慮到單一職責原則,按照此 function 的命名,應該是不需要 Read 的功能,故再重構一次:
```go
// Save writes the contents of doc to the supplied
// WriteCloser.
func Save(wc io.WriteCloser, doc *Document) error
```
但接著,難道我們真的每次寫入完就立即將 file descriptor 關閉嗎?有可能我們還需要繼續寫入其他 content,故最佳實踐其實是:
```go
// Save writes the contents of doc to the supplied
// Writer.
func Save(w io.Writer, doc *Document) error
```
以上的做法遵循的法則又稱 **interface segregation principle**。
---
# 7. Error handling
作者寫了很多有關錯誤處理的文章,以下文章找時間瞄一下:
- [Inspecting errors](https://dave.cheney.net/2014/12/24/inspecting-errors)
- [Constant errors](https://dave.cheney.net/2016/04/07/constant-errors)
- [Don’t just check errors, handle them gracefully](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully)
以下內容將不會與上述的文章重複。
## 7.1. Eliminate error handling by eliminating errors
> **TL;DR**
> 不是要你**屏蔽**錯誤訊息,而是尋找能夠讓你**不用自己處理錯誤**的方法,來重構你的程式碼
>
### 7.1.1. Counting lines
**舉例:**
計算一個檔案有幾行,一開始可能會這樣寫
```go=
func CountLines(r io.Reader) (int, error) {
var (
br = bufio.NewReader(r)
lines int
err error
)
for {
_, err = br.ReadString('\n')
lines++
if err != nil {
break
}
}
if err != io.EOF {
return 0, err
}
return lines, nil
}
```
看似沒問題,也滿足前面的 interface segregation principal。
但 line 8 ~ 14 其實邏輯有點怪,為何是先 `lines++` 才檢查錯誤呢?
> 因為受限於 `ReadString` 的用法,它會在遇到 new line character 之前先遇到 EOF(end-of-file (`io.EOF`)) 的話,就回傳 error。
>
> 所以這樣的寫法是為了滿足整個 file 沒有 new line character、也能計算到一行。
>
你當然可以想辦法調整一下這個函式內的邏輯,充滿各式各樣的錯誤處理之後,避免上述提到的怪異點。但如此一般,你的函式就變得很難讓人理解原本是要幹麻的了。
若再進一步熟悉 golang 的話,會發現較佳的寫法是:
```go
func CountLines(r io.Reader) (int, error) {
sc := bufio.NewScanner(r)
lines := 0
for sc.Scan() {
lines++
}
return lines, sc.Err()
}
```
利用 `sc.Scan()` 回傳 `true`,讓我們知道遇到了換行符號且沒有錯誤,故可以正確地 `lines++`;`sc.Scan()` 回傳 `false` 時表示遇到錯誤或 EOF ,則會幫我們離開迴圈。
`sc.Err()` 則幫我們封裝了遇到的第一個 error、且如果 error 是 `io.EOF`,則幫我們轉成 `nil`。
> `bufio.Scanner` 可以掃描任意 pattern,預設值是掃描 newlines
>
### 7.1.2. WriteResponse
此例啟發於 [go blog - Errors are values](https://blog.golang.org/errors-are-values)。
如果你撰寫的是非常底層的邏輯,免不了就真的必須在你的實作中親手處理錯誤,讓你的程式碼有非常多重複的錯誤處理。那麼實務上你可以怎麼處理呢?
**舉例:**
HTTP server 要建構一塊 HTTP response:
```go=
type Header struct {
Key, Value string
}
type Status struct {
Code int
Reason string
}
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
_, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
if err != nil {
return err
}
for _, h := range headers {
_, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
if err != nil {
return err
}
}
if _, err := fmt.Fprint(w, "\r\n"); err != nil {
return err
}
_, err = io.Copy(w, body)
return err
}
```
函式依序處理要回傳的字串,若在任何一段 subroutine 出錯,就捨棄所有內容回傳 error。
問題在於**太多重複性的檢查錯誤程序**,最後的 line 27~28 也很醜。此時創建一個 **small wrapper type - `errWriter`** 將很有幫助。
重構後的程式碼如下:
```go
type errWriter struct {
io.Writer
err error
}
func (e *errWriter) Write(buf []byte) (int, error) {
if e.err != nil {
return 0, e.err
}
var n int
n, e.err = e.Writer.Write(buf)
return n, nil
}
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
ew := &errWriter{Writer: w}
fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
for _, h := range headers {
fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
}
fmt.Fprint(ew, "\r\n")
io.Copy(ew, body)
return ew.err
}
```
若不幸在某一次寫入出錯之後,下一次呼叫 `errWriter.Write` 都會捨棄掉要寫入的內容、直接回傳之前的 err。`WriteResponse` 的邏輯變得清晰許多,最後也只要直接回傳 `ew.err` 即可。
## 7.2. Only handle an error once
> **TL;DR**
> 使用 [github.com/pkg/errors](https://godoc.org/github.com/pkg/errors) 封裝錯誤訊息,使你的錯誤報告變成美妙的 [K&D](https://www.gopl.io/) style error
>> - 用 `go get github.com/pkg/errors` 取得上述 error wrapper
>> - [The Go Programming Language](https://github.com/KeKe-Li/book/blob/master/Go/The.Go.Programming.Language.pdf)
>> *Alan A. A. **D**onovan, Google Inc.*
>> *Brian W. **K**ernighan, Princeton University*
go 的程式碼很常這樣處理錯誤:
```go
if err != nil {
// print some error message
return err
}
```
但會有何困擾呢?當你的程式碼有以下的層次時:
```go=
func WriteAll(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
log.Println("unable to write:", err) // annotated error goes to log file
return err // unannotated error returned to caller
}
return nil
}
func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
log.Printf("could not marshal config: %v", err)
return err
}
if err := WriteAll(w, buf); err != nil {
log.Println("could not write config: %v", err)
return err
}
return nil
}
```
可以預期,出錯時會印出以下內容:
```shell
unable to write: io.EOF
could not write config: io.EOF
```
但在程式的最上層,你只拿到了原始的錯誤、而失去的 **error context**:
```go
err := WriteConfig(f, &conf)
fmt.Println(err) // io.EOF
```
所以我們需要一個封裝過的方法,除了原始錯誤訊息之外,也將我們在每一層寫下的 error context 一併回報。
這就是 `errors` 模組提供的服務,看以下改寫過的例子:
```go=
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
)
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "open failed")
}
defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
return nil, errors.Wrap(err, "read failed")
}
return buf, nil
}
func ReadConfig() ([]byte, error) {
home := os.Getenv("HOME")
config, err := ReadFile(filepath.Join(home, ".settings.xml"))
return config, errors.WithMessage(err, "could not read config")
}
func main() {
_, err := ReadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
```
出錯時,輸出會被包裝成以下:
```shell!
could not read config: open failed: open /home/maxcian/.settings.xml: no such file or directory
```
且此 error value 還保留了詳細的 call stack,透過 [`Cause(err)`](https://godoc.org/github.com/pkg/errors#Cause) 呼叫取得:
```go
func main() {
_, err := ReadConfig()
if err != nil {
fmt.Printf("Original error:\n%T %v\n", errors.Cause(err), errors.Cause(err))
fmt.Println()
fmt.Printf("Stack trace:\n%+v\n", err)
os.Exit(1)
}
}
```
```shell
Original error:
*os.PathError open /home/maxcian/.settings.xml: no such file or directory
Stack trace:
open /home/maxcian/.settings.xml: no such file or directory
open failed
main.ReadFile
/home/maxcian/go/src/github.com/hjcian/gophercises/main.go:15
main.ReadConfig
/home/maxcian/go/src/github.com/hjcian/gophercises/main.go:28
main.main
/home/maxcian/go/src/github.com/hjcian/gophercises/main.go:33
runtime.main
/usr/local/go/src/runtime/proc.go:203
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1373
could not read config
exit status 1
```
---
# 8. Concurrency
Go team 盡力開發出硬體資源消耗低、高效率、且容易使用的 concurrency 支援,但開發者若誤用反而容易寫出無效率且不可靠的程式碼。
在此章節討論一些在使用`chan`、`select`和 `go` 時,很常會誤踩的**坑**。
## 8.1. Keep yourself busy or do the work yourself
> **TL;DR**
> 別過度使用 `goroutine`,適度就好。
>
以下程式碼想要運行一個 web server,監聽 port 8080。有什麼潛在問題嗎?
```go=
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, GopherCon SG")
})
go func() {
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}()
for {
}
}
```
他確實如期運作,運行了一個 simple web server。
問題是 **line 19 會讓一顆 CPU 徒勞地空轉**。
> You can try it on your computer for the experiment by yourself.
可能還會看到有人這樣寫,但表示該人不了解到底問題是什麼:
```go=19
for {
runtime.Gosched()
}
```
使用 `runtime.Gosched()`,只是告訴 scheduler 先去做別的 goroutine,待會再回來找我。但你的 CPU 仍然在瞎忙。
> Demonstration on my computer:
> 
稍微有點經驗的開發者,可能會想到可以使用 empty select statement,來**永遠阻塞 main goroutine**:
```go=19
select {}
```
這樣的確不會再造成任何 CPU 空轉,但仍然是**治標不治本**。
其實此例,**不需多此一舉**將 `http.ListenAndServe` 放到 `goroutine`,就直接讓他運行在 `main goroutine` 就行了:
```go=10
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, GopherCon SG")
})
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
```
因為你根本也沒有要讓 main goroutine 做其他事,那就讓它自己來吧!(do the work yourself)
## 8.2. Leave concurrency to the caller
將使用 concurrency 的時機,留給 caller。
以下兩個 API 有何差異?
```go
// ListDirectory returns the contents of dir.
func ListDirectory(dir string) ([]string, error)
```
```go
// ListDirectory returns a channel over which
// directory entries will be published. When the list
// of entries is exhausted, the channel will be closed.
func ListDirectory(dir string) chan string
```
第一例的潛在問題:
1. caller 會被阻塞 (block),直到函式搜索完所有目錄
2. 搜尋可能會很久
3. 占用一定程度的記憶體
第二例的潛在問題:
1. 處理途中若有任何 **error 你不會知道**,你**只知道 channel 被關閉**
2. 就算你已經取得了你想要的目錄,關閉此 channel 的唯一方法就是讓它讀完。故其實沒從此種寫法中受惠
考慮上述的優缺點,折衷的解法是傳入 **callback function**,讓 caller 決定他想要做什麼事 (in fact, [`filepath.Walk`](https://golang.org/pkg/path/filepath/#Walk) 就是這麼實作)
```go
func ListDirectory(dir string, cb func(string))
```
## 8.3. Never start a goroutine without knowning when it will stop
> **TL;DR**
> 不要開啟一個 goroutine 之後,沒有設計任何關閉它的方法
>
今天有一個簡單的 Web 應用程式:
1. **0.0.0.0:8080** 用來服務 application traffic
2. **127.0.0.1:8001** 用提供 `/debug/pprof` endpoint 的存取
```go
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
go http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux) // debug
http.ListenAndServe("0.0.0.0:8080", mux) // app traffic
}
```
> `/debug/pprof` 是什麼?
>> 
考慮到未來應用程式的增長,先來小重構一下:
```go
func serveApp() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
http.ListenAndServe("0.0.0.0:8080", mux)
}
func serveDebug() {
http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
}
func main() {
go serveDebug()
serveApp()
}
```
將兩段程式碼從 `main.main` 中解耦,同時也遵循上述的建議-**將 concurrency 的部分留給 caller 去決定**。
但有個問題,各自的 goroutine 若因故死亡,沒有人知道阿!如果死亡的是 `serveDebug`,且當維運人員試圖從 `/debug/pprof` 獲取程式資訊卻失敗時,會森77的。
故在此引入一個需求:**需要其中一個 goroutine 死亡時,整個 application 也可以跟著關閉,省得誤會程式正常運作中。**
故程式再重構一次:
```go
func serveApp() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
if err := http.ListenAndServe("0.0.0.0:8080", mux); err != nil {
log.Fatal(err)
}
}
func serveDebug() {
if err := http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux); err != nil {
log.Fatal(err)
}
}
func main() {
go serveDebug()
go serveApp()
select {}
}
```
利用 `select {}` 阻塞 main goroutine,且當任何一個 goroutine 死亡時,會呼叫 `log.Fatal` 關閉程式。
但仍然有些問題沒考慮到:
1. 如果 `ListenAndServer` 回傳的是 `nil` error,那麼該 goroutine 就會在不停止程式的情況下停止服務
> 其實在 `server.go` 中有註解:`ListenAndServe always returns a non-nil error.` ,故在此只是提出一個假設性問題
2. `log.Fatal` 會呼叫 `os.Exit`,無條件結束程式,造成你程序中的 `defer` 不會被呼叫到、別的 goroutines 也無預警地死亡,**這會使得編寫測試時很彆扭**
> **TIP**: Only use `log.Fatal` from `main.main` or `init` functions.
故再引入新的需求:**在有 goroutine 死亡時,將錯誤回傳給 caller 以便知道停止原因**,以便可以乾淨地關閉程式。
再重構如下:
```go
func serveApp() error {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
return http.ListenAndServe("0.0.0.0:8080", mux)
}
func serveDebug() error {
return http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
}
func main() {
done := make(chan error, 2)
go func() {
done <- serveDebug()
}()
go func() {
done <- serveApp()
}()
for i := 0; i < cap(done); i++ {
if err := <-done; err != nil {
fmt.Printf("error: %v \n", err)
}
}
}
```
錯誤資訊是已經蒐集到了,但還不夠,接著我們**需要一個方法**能夠**將關閉訊號傳遞給別的 goroutine**。
先做一個 helper function `serve` 來實現此邏輯,其中借助一個 `stop` channel 來傳遞關閉的訊號,並在其中呼叫 `Shutdown()`。
再重構如下:
```go=
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.Printf("error: %v \n", err)
}
if !stopped {
stopped = true
close(stop)
}
}
}
```
main goroutine 會被阻塞在 line 39 (`err := <-done`)。現在,只要 `done` 收到值,被阻塞的 main goroutine 就會開始工作,觸發 line 42~44 關閉 `stop` channel。
接著其他的 goroutines 就會收到 `stop` 傳來的訊號(line 8),關閉他們的 `http.Server`。
這些 goroutines 關閉 `http.Server` 之後,就會造成該 goroutine 也跟著 return,最後 `main.main` 就如我們所願乾淨地停止、且也有明確的錯誤資訊了。
:::success
為何利用額外的 channel 來作為發送關閉訊號的管道?
這裡可能偷渡了一個使用原則:**The Channel Closing Principle**
1. 不關閉一個有多個 senders 的 channel
2. 不從接收端關閉 channel
原因是因為以下事實:
1. close a closed channel -> **panic**
2. send a value to closed channel -> **panic**
延伸閱讀
- [Channels in Go - **A Comprehensive Interpretation**](https://go101.org/article/channel.html)
- [**The Channel Closing Principle** - How to Gracefully Close Channels](https://go101.org/article/channel-closing.html)
:::