---
# System prepended metadata

title: 'Practical Go: Real world advice for writing maintainable Go programs'
tags: [golang]

---

---
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`**
![sync.Mutex example](https://i.imgur.com/IpL44ie.png)

**`bytes.Buffer`**
![bytes.Buffer example](https://i.imgur.com/SqoV3IL.png)

再觀察 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:
> ![](https://i.imgur.com/KPjpQP1.png)


稍微有點經驗的開發者，可能會想到可以使用 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` 是什麼？
>> ![/debug/pprof](https://i.imgur.com/lraPKVk.png)

考慮到未來應用程式的增長，先來小重構一下：
```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)
:::