用Golang撰寫Package的Best Practice

Posted by Kubeguts on 2021-02-15

以下收錄撰寫golang package可以遵循的內容

Package 概覽

一個Go Pakcage的基本元素包含了如下:

  • Package declaration: 基本定義,告訴developer package的功能是什麼
  • Documentation: 提供Package內所包含的function是什麼,要如何使用
  • Imports: 如何引入pakcage內的function
  • var & const blocks: 使用的變數宣告
  • Types & interfaces: 定義package內會使用的型態以及介面
  • Functions: functions的實作

Package的範圍:一個資料夾內的所有.go檔案,其他除外

基本上一個Go Package只會包含單一目錄內所有的Go Source.

舉個例子

1
2
3
4
5
---services
|--- events (Package)
|-- data (資料夾)
|-- handlers.go
|-- serve.go

上面events這個package,只會包含 handlers.go 以及 serve.go 這兩隻檔案的內容,data資料夾會被排除

Package種類

Library Packages

定義Golang Package的內容,提供相同功能性的function。

1
2
3
4
// .../services
package services

...
  • Library Pakcage通常被其他的package所引入進來
  • Library Package的名稱必須要跟自己的資料夾名稱一樣
  • pakcage內的最好是提供相似的功能

Main Package

定義了Application的進入點,程式要啟動就會是從main package開始,注重在app的設置以及初始化的邏輯。

1
2
3
package main

func main() {}
  • 包含 main() function
  • 可以在任何的資料夾

範例

http package: https://golang.org/pkg/net/http/

  • Constants: 定義常數內容
1
2
3
4
5
6
7
8
9
10
11
12
const (
StatusContinue = 100 // RFC 7231, 6.2.1
StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
StatusProcessing = 102 // RFC 2518, 10.1
StatusEarlyHints = 103 // RFC 8297

StatusOK = 200 // RFC 7231, 6.3.1
StatusCreated = 201 // RFC 7231, 6.3.2
StatusAccepted = 202 // RFC 7231, 6.3.3
StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4
...
)
  • Variables: 定義變數內容,且給予說明
1
2
3
4
5
6
7
8
9
10
11
12
var (
// ErrNotSupported is returned by the Push method of Pusher
// implementations to indicate that HTTP/2 Push support is not
// available.
ErrNotSupported = &ProtocolError{"feature not supported"}

// Deprecated: ErrUnexpectedTrailer is no longer returned by
// anything in the net/http package. Callers should not
// compare errors against this variable.
ErrUnexpectedTrailer = &ProtocolError{"trailer header without chunked transfer encoding"}
...
)
  • Types: 定義客製化型態,並給予說明
1
2
3
4
5
6
7
// A Client is an HTTP client. Its zero value (DefaultClient) is a usable client that uses DefaultTransport. ...
type Client struct {
// Transport specifies the mechanism by which individual
// HTTP requests are made.
// If nil, DefaultTransport is used.
Transport RoundTripper
...

也提供了Clinet形態的初始化變數設置,不用一直自行初始化

1
2
// DefaultClient is the default Client and is used by Get, Head, and Post.
var DefaultClient = &Client{}
  • Function: 定義function內容,並添加說明
1
2
3
// CanonicalHeaderKey returns the canonical format of the header key s. The canonicalization converts the first letter and any letter following a hyphen to upper case; the rest are converted to lowercase. For example, the canonical key for "accept-encoding" is "Accept-Encoding". If s contains a space or invalid header field bytes, it is returned without modifications.

func CanonicalHeaderKey {...}

Pakcage 的生命週期

  1. Import 所需要的package
  2. 將initial value設置在variables
  3. 呼叫 init() 方法,執行初始化該package的動作

注意,無法直接呼叫 init() ,該方法只能在golang的compiler時期被golang呼叫執行。

範例

假如有個目錄如下

1
2
3
4
5
6
7
8
9
--- cmd
|--- startAll
| |- main.go
|--- services
|- handlers.go
|- serve.go
|--- internal
|--- ports
|--- ports.go

main.go import services 這個package

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
Package events provides a webservice that manages the libary's special events.
*/
package events

import (
"fmt"
"net/http"
"strconv"

"github.com/pluralsight/libmanager/services/internal/ports"
)

var port = 42

// StartServer registers the handlers and initiates the web service.
// The service is started on the local machine with the port specified by
// .../lm/services/internal/ports#EventService
func StartServer() error {
sm := http.NewServeMux()
sm.Handle("/", new(eventHandler))
return http.ListenAndServe(":"+strconv.Itoa(port), sm)
}

func init() {
fmt.Println("serve.go 1", port)
port = ports.EventService
fmt.Println("serve.go 2", port)
}

func init() {
fmt.Println("second init in serve.go")
}

可以看到init() 可以多次宣告

此時main.go呼叫services pakcage內的startServer function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Package main is typically used to define a single, executable command and its associated logic.
//
// Functions in the main package should normally avoid containing business logic; they
// should focus on initializing the application and handling any errors that are returned by
// any library calls.
package main

import (
"fmt"
"log"
"sync"

"github.com/pluralsight/libmanager/services/events"
)

func init() {
fmt.Println("init in main.go")
}

// The main function, when defined in a main package, defines
// the entry and exit point for an executable command.
func main() {
log.Println("Starting all services")

// start the services
wg := new(sync.WaitGroup)
wg.Add(1)
// 執行 events package內的StartServer方法
go startServer(wg, events.StartServer)
wg.Wait()
log.Println("All services stopped")
}

// startServer is a function in the main package that supports the command.
func startServer(wg *sync.WaitGroup, startFunc func() error) {
err := startFunc()
wg.Done()
if err != nil {
log.Fatal(err)
}
}

要注意,services import了internal內ports package的變數,故若呼叫了main.go 中的startServer,會先印出初始化的port = 42, 再來是透過init()接收到ports package的 port = 3000

1
2
3
4
5
func init() {
fmt.Println("serve.go 1", port)
port = ports.EventService
fmt.Println("serve.go 2", port)
}

印出如下

1
2
3
serve.go 1 42
serve.go 2 3000
second init in serve.go

Pakcage 存取範疇

  • Public Scope
    • 以大寫宣告
    • 可被專案內所有成員存取
  • Package Scope
    • 以小寫宣告
    • 只能被同一個package內存取
  • Internal Package
    • 可同時使用public-level, package-level 的成員
    • 只限於父層與子層的成員存取

Pakcage設計的Best Practice

Pakcage 名稱

  • 儘量簡短,使用名詞
  • 小寫開頭,不使用 _, 等特殊字元
  • 別使用一般使用者會用到的變數名稱 event, temp 等名詞來命名.

Package 說明

一個公開的pakcage通常會包含兩個主要說明內容

  • licensing: 使用的開放協議
  • package comment: package的功能說明
  • 說明開頭應包含 package名稱,方法的話開頭會function的名稱

helloworld/doc.go

1
2
3
4
5
6
7
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by sa BSD-style
// license that can be found in the LICENSE file

// Package helloworld implements ....

package helloworld

這時使用 go doc helloworld,就會顯示 helloworld package內doc.go的說明

使用 doc.go 做更複雜的說明

若有太多的package敘述要呈現,通常會在doc.go 說明

pic01

Pakcage內容的描述撰寫指引

  • 避免冗余

例如,下面的package名稱與function名稱都重複了,所以可以將function名稱與package重複的部分給去掉

1
2
http.HTTPServer -> http.server
json.JSONEncoder -> json.encoder
  • 盡量簡化

例如,下面可以簡化成如下

1
2
time.NewTime -> time.Time
db.ConnectToDatabase -> db.Connect

Package內容準則

  • 提供直覺的功能
    • single reponsibility: 單一功能
    • cohesive API: 功能相依的Application Programming Interface
  • 提供友善的使用方法
    • simple to use
    • minimize API: 將功能重點化
    • encapsulate changes: 將變動封裝起來,不影響原本使用者的input, output的使用方式
  • 最大化可重複利用性
    • reduce dependencies: 減少依賴其他dependencies的狀況
    • minimize scope: 縮小範疇,避免到處都是public,造成package內部的變數被其他地方所影響。

結構設計

Package Input Package Output
對於configuration使用 concrete types 對於configuration & behavior 都使用concrete types
對於behavior 使用 interface 對於errors 請避免不發生 panics: 因為錯誤即使發生了,使用者也無法對其做處理

透過 http package來看看其結構設計

Package Input Package Output
net/http.Request 對於http.Reqeust所要使用的configuration,例如http body要用的參數格式是什麼,使用 concrete types,明確規範使用者要放置什麼給package net/http.Response 對於其回傳結果,將configuration & behavior 都使用concrete types定義好,避免使用者混淆
net/http.Handler 對於http.Handler, 這個處理http的behavior 使用 interface,方便使用者可以針對他們自己的http Request進來的行為定義自己的型態 net/http.Get 正常狀況下 (用戶正確使用下),用戶使用其方法是不會發生panic的

Import Package方法

1. Typical Imports

使用fmt pakcage的 Println function

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("hello, gophers!")
}

2. Alternative Imports

  • Aliases 將pakcege 別名
1
2
3
4
5
6
7
8
9
10
11
12
package main

// 這時會引入了兩個叫做json的pakcage, compiler會出現混淆
import (
"encoding/json"
"pluralsight.com/libmanager/json"
)

func main() {
...
data, err := json.Marshal(...)
}

可透過 alias ,將其中一個package名稱進行別名

1
2
3
4
import (
"encode/json"
lmjson "pluralsight.com/libmanager/json"
)

3. Import for side effect: 為引入的package,在引入其他的package使其作用

這有點難理解,直接看範例

例如以下有引入兩個packages, 一個是sql package, 另外一個是 pq postgres這個資料庫的package.

1
2
3
4
import (
"database/sql"
_ "github.com/lib/pq"
)

可以看到 pq 是用 _ 代表他是一個write only的package, 本身使用者不會去使用到它,而是提供給 sql 做初始化sql內要採用哪個database做使用

可以看到 pg package內 有這一段程式碼,提供init()給sql做注入使用其postgres dirver做使用

1
2
3
func init() {
sql.Register("postgres", &Driver{})
}

4. Internal packages

主要提供父層與子層的pakcage做存取,目的是要提供跳脫自己之外的存取限制,又不會洩漏自己內部訊息給外部使用者。

只要是宣告為 internal 的package,就可以被父層或子層存取

以下範例為,services 下有宣告 internal package, 可提供其他package services/events/serve.go 做存取使用

pic04

若在超出internal定義的範疇 (上一個層級與下一個層級),則會compiler編譯時會發生錯誤訊息

pic02

pic03

4. Relative Import

透過 ../../package 的方式import,不過在Production環境下不建議使用; 或者得透過golang最新的 go mod 管理方式。

參考資源