Go 語言規範 - 編碼風格篇

當前版本: v1.0.20201106

GitHub: shockerli/go-code-guidehtml

命名規則

  • 站在調用者的角度,包不是給你本身用的
  • 簡潔、且見名知義
  • 採用通用、大衆熟知的縮寫命名。好比buf而不是bufio
  • 若是縮寫的名字會產生歧義,則放棄或換個

文件名

整個應用或包的主入口文件應當是 main.go,或與應用名稱簡寫相同。git

好比:spiker 包的主入口文件是 spiker.go,應用的主入口文件是 main.gogithub

包名

  • 包名與目錄名一致golang

    若是一個目錄下同時出現多個 package,則編譯失敗:express

    found packages pkg (a.go) and pb (b.go) in XXX
  • 大多數使用命名導入的狀況下,不須要重命名

    少讓調用者去起別名,除非名字太爛json

  • 所有小寫,沒有下劃線、大寫。錯誤示例MyPackagemy_packagemyPackage
  • 不用複數。例如net/url,而不是net/urls
  • 不用信息量不足的名字。錯誤示例commonlibutil

導入包

  • 若是程序包名稱與導入路徑的最後一個元素不匹配,則必須使用導入別名
import (
    client "example.com/client-go"
    trace "example.com/trace/v2"
)
  • 在全部其餘狀況下,除非導入之間有直接衝突,不然應避免導入別名
import (
    "net/http/pprof"
    gpprof "github.com/google/pprof"
)
  • 如遇重名,請保留標準包而別名自定義或第三方包
  • 在非測試文件(*_test.go)中,禁止使用 . 來簡化導入包的對象調用
  • 禁止使用相對路徑導入(./subpackage),全部導入路徑必須符合 go get 標準

駝峯命名法

常量、變量、類型、結構體、接口、函數、方法、屬性等,所有使用駝峯法 MixedCaps 或 mixedCaps。segmentfault

下劃線開頭的命名更不容許,Go 語言的公私有統一用大小寫開頭來區分。服務器

但有個例外,爲了對相關的測試用例進行分組,函數名可能包含下劃線,如:TestMyFunction_WhatIsBeingTested。架構

Bad:app

const ROLE_NAME = 10

Good:

const RoleName = 10

常量

  • 若是是枚舉類型的常量,須要先建立相應類型
type Scheme string

const (
    Http  Scheme = "http"
    Https Scheme = "https"
)
  • 若是模塊的功能較爲複雜、常量名稱容易混淆的狀況下,爲了更好地區分枚舉類型,可使用完整的前綴
type Symbol string

const (
    SymbolAdd Symbol = "+"
    SymbolSub Symbol = "-"
)

變量

  • 在相對簡單的環境(對象數量少、針對性強)中,能夠將一些名稱由完整單詞簡寫爲單個字母

    • user 能夠簡寫爲 u
    • userId 能夠簡寫 uid
  • 若變量類型爲 bool 類型,則名稱應以 HasIsCanAllow 開頭
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool

URL

  • URL 命名所有小寫
  • 用正斜槓 / 代表層級關係
  • 使用連字符 - 來提升長路徑中名稱的可讀性
  • 不得在 URL 中使用下劃線 _
  • URL 結尾不該包含正斜槓 /
  • 文件擴展名不該包含在 URL 中
  • URL 需見名知意,但不可暴露服務器架構

Bad:

/GetUserInfo
/photos_path
/My-Folder/my-doc/
/user/user-list

Good:

/user/list
/user/operator-logs

函數/方法名

  • 不要多此一舉
  • 長命名並不會使其更具可讀性,一份有用的說明文檔一般比額外的長名更有價值

Bad:

once.DoOrWaitUntilDone(f)

Good:

once.Do(f)
  • 在 pkg 包中名爲 New 的函數會返回一個 pkg.Pkg 類型的值
q := list.New()  // q is a *list.List
  • 當 pkg 包中某個函數的返回值類型爲 pkg.Pkg (或 *pkg.Pkg )時,函數名應省略類型名
start := time.Now()                                  // start is a time.Time
t, err := time.Parse(time.Kitchen, "6:06PM")         // t is a time.Time
  • 當函數返回的值類型爲 pkg.T 且 T 不爲 Pkg 時,函數名應包含 T 以便讓用戶代碼更易理解
ticker := time.NewTicker(d)          // ticker is a *time.Ticker
timer := time.NewTimer(d)            // timer is a *time.Timer
  • 獲取器/設置器

    Go 並不對獲取器(getter)和設置器(setter)提供自動支持。針對某個變量或字段,獲取器名字無需攜帶 Get,設置器名字以 Set 開頭。

    若你有個名爲 owner (小寫,未導出)的字段,其獲取器應當名爲 Owner(大寫,可導出)而非 GetOwner。

Bad:

owner := obj.GetOwner()
if owner != user {
    obj.SettingOwner(user)
}

Good:

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}
  • 若函數或方法爲判斷類型(返回值主要爲 bool 類型),則名稱應以 HasIsCanAllow 等判斷性動詞開頭
func HasPrefix(name string, prefixes []string) bool { ... }
func IsEntry(name string, entries []string) bool { ... }
func CanManage(name string) bool { ... }
func AllowGitHook() bool { ... }

接口名

按照約定,只包含一個方法的接口應當以該方法的名稱加上 -er 後綴來命名,如 Reader、Writer、Formatter/CloseNotifier 等。

名詞用於接口名,動詞用於接口的方法名。

type Reader interface {
    Read(p []byte) (n int, err error)
}

Error

  • Error 類型的命名以 Error 結尾
type ParseError struct {
    Line, Col int
}
  • Error 類型的變量,以 Err開頭
var ErrBadAction = errors.New("somepkg: a bad action was performed")
  • 返回類型爲 Error 的變量縮寫採用 err
func foo() {
    res, err := somepkgAction()
    if err != nil {
        if err == somepkg.ErrBadAction {
        }
        if pe, ok := err.(*somepkg.ParseError); ok {
             line, col := pe.Line, pe.Col
             // ....
        }
    }
}

其餘

包內容的名字不能夠包名開頭,由於無需重複包名

http 包提供的 HTTP 服務名爲 http.Server ,而非 HTTPServer 。用戶代碼經過 http.Server 引用該類型,所以沒有歧義。

不一樣包中的類型名能夠相同,由於客戶端可經過包名區分它們

例如,標準庫中含有多個名爲 Reader 的類型,包括 jpeg.Readerbufio.Readercsv.Reader。每一個包名搭配 Reader 都是個不錯的類型名。

名詞縮寫表

縮寫名 說明
ctx Context 或相關,好比 gin.Context

分號

Go 其實也是用分號(;)來結束語句,但 Go 與 JavaScript 同樣不建議給單一語句末尾加分號,由於編譯器會自動加分號。

像以下語句是徹底能夠的:

go func() { for { dst <- <-src } }()

一般 Go 程序只在諸如 for 循環子句這樣的地方使用分號,以此來將初始化器、條件及增量元素分開。若是你在一行中寫多個語句,也須要用分號隔開。

if err := f(); err != nil {
    g()
}

也是由於這個緣由,函數或控制語句的左大括號毫不能放在下一行。

if i < f()  // 報錯
{           // 報錯
    g()
}

圓括號

控制結構(if、for 和 switch)不須要圓括號,語法上就不須要

文檔

README、項目文檔、接口文檔等,中文文檔的排版參考:中文文案排版指北

註釋

  • 全部導出對象必須註釋說明其用途,非導出對象根據狀況進行註釋
  • 若是對象可數且無明確指定數量的狀況下,一概使用單數形式和通常進行時描述,不然使用複數形式
  • 包、函數、方法和類型的註釋說明都是一個完整的句子
  • 句子類型的註釋首字母均需大寫,短語類型的註釋首字母需小寫
  • 註釋的單行長度不能超過 80 個字符,超過請強制換行
  • 可導出對象的註釋,必須以對象的名稱做爲開頭
// FileInfo is the interface that describes a file and is returned by Stat and Lstat
type FileInfo interface { ...

// HasPrefix returns true if name has any string in given slice as prefix
func HasPrefix(name string, prefixes []string) bool { ...

單行註釋&多行註釋

  • 兩種註釋風格,單行註釋 //,多行註釋 /* ... */
  • 多行註釋僅用於包級別的文檔註釋,除此以外請用單行註釋。包註釋通常放置到 doc.go 文件,且該文件僅包含文檔註釋內容
  • 單行註釋符號與內容之間,請用一個空格隔開

Bad:

//Comments

Good:

// Comments

GoLand 可設置自動格式化:

Preferences > Editor > Code Style > Go > Other 勾選上 Add leading space to comments

包註釋

  • 包級別的註釋就是對包的介紹,只需在同個包的任一源文件中說明便可有效
  • 對於 main 包,通常只有一行簡短的註釋用以說明包的用途,且以項目名稱開頭
// Write project description
package main
  • 對於一個複雜項目的子包,通常狀況下不須要包級別註釋,除非是表明某個特定功能的模塊
  • 對於簡單的非 main 包,也可用一行註釋歸納
  • 對於相對功能複雜的非 main 包,通常都會增長一些使用示例或基本說明,且以 Package <name> 開頭
/*
Package http provides HTTP client and server implementations.
...
*/
package http
  • 特別複雜的包說明,可單首創建 doc.go 文件來加以說明

函數與方法

  • 若是一句話不足以說明所有問題,則可換行繼續進行更加細緻的描述
// Copy copies file from source to target path.
// It returns false and error when error occurs in underlying function calls.
  • 若函數或方法爲判斷類型(返回值主要爲 bool 類型),則以 <name> returns true if 開頭
// HasPrefix returns true if name has any string in given slice as prefix.
func HasPrefix(name string, prefixes []string) bool { ...

結構、接口及其它類型

  • 類型的定義通常都以單數形式描述:
// Request represents a request to run a command.
type Request struct { ...
  • 若是爲接口,則通常以如下形式描述:
// FileInfo is the interface that describes a file and is returned by Stat and Lstat.
type FileInfo interface { ...
  • 若是結構體屬性較多,需對屬性添加註釋
// Var variable for expression
type Var struct {
    Key   string      `json:"key"`   // variable key
    Value interface{} `json:"value"` // value
    Desc  string      `json:"desc"`  // variable description
}

其餘說明

  • 當某個部分等待完成時,可用 TODO: 開頭的註釋來提醒維護人員。
  • 當某個部分存在已知問題進行須要修復或改進時,可用 FIXME: 開頭的註釋來提醒維護人員。
  • 當須要特別說明某個問題時,可用 NOTE: 開頭的註釋:
// NOTE: os.Chmod and os.Chtimes don't recognize symbolic link,
// which will lead "no such file or directory" error.
return os.Symlink(target, dest)

格式化

咱們沒有太多可選的餘地,由於 Go 已經規範好了,在 Go 世界沒有此類戰爭。

縮進

縮進統一採用4個空格,禁用製表符。

EditorConfig 設置:

[{Makefile,go.mod,go.sum,*.go}]
indent_style = tab
indent_size = 4

GoLand 設置:

Preferences > Editor > Code Style > Go > Tabs and Indents

goland-tab-indent-w600

空行

  • 適當增長空行以保持代碼段落清晰

其餘

函數分組與順序

  • 函數應按粗略的調用順序排序
  • 同一文件中的函數應按接收者分組
  • 導出的函數應先出如今文件中,放在 structconstvar 定義的後面。
  • 在定義類型以後,但在接收者的其他方法以前,可能會出現一個 newXYZ() / NewXYZ()
  • 因爲函數是按接收者分組的,所以普通工具函數應在文件末尾出現。
  • 所以,通常一個 struct 及相關方法組織爲一個文件。

Bad:

func (s *something) Cost() {
    return calcCost(s.weights)
}

type something struct{ ... }

func calcCost(n int[]) int {...}

func (s *something) Stop() {...}

func newSomething() *something {
    return &something{}
}

Good:

type something struct{ ... }

func newSomething() *something {
    return &something{}
}

func (s *something) Cost() {
    return calcCost(s.weights)
}

func (s *something) Stop() {...}

func calcCost(n int[]) int {...}

減小嵌套

Bad:

for _, v := range data {
    if v.F1 == 1 {
        v = process(v)
        if err := v.Call(); err == nil {
            v.Send()
        } else {
            return err
        }
    } else {
        log.Printf("Invalid v: %v", v)
    }
}

Good:

for _, v := range data {
    if v.F1 != 1 {
        log.Printf("Invalid v: %v", v)
        continue
    }

    v = process(v)
    if err := v.Call(); err != nil {
        return err
    }
    v.Send()
}

沒必要要的 else

  • 多數時候,咱們能夠把 else 分支裏的代碼提取爲初始化。

Bad:

var a int
if b {
    a = 100
} else {
    a = 10
}

Good:

a := 10
if b {
    a = 100
}

全局變量聲明

  • 全局變量,必須使用 var 關鍵字
  • 請勿指定類型,除非它與表達式的類型不一樣

Bad:

var a string = "abc"
var s string = F()
    
func F() string { return "A" }

Good:

var a = "abc"
// 因爲 F() 已經明確了返回一個字符串類型,所以咱們沒有必要顯式指定 s 的類型
var s = F()
    
func F() string { return "A" }
  • 若是表達式的類型與所需的類型不徹底匹配,請指定類型
type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var err error = F()
// F() 返回一個 myError 類型的實例,可是咱們要 error 類型

局部變量聲明

  • 若是將變量明確設置爲某個值,則應使用短變量聲明形式(:=

Bad:

var s string = "abc"

Good:

s := "abc"
  • 若是變量專用於引用,則使用 var 關鍵字更合適
func s() {
    var s string
    f(&s)
}
  • 若是變量是返回值,則定義在函數返回類型中
func f(list []int) (filtered []int) {
    for _, v := range list {
        if v > 10 {
            filtered = append(filtered, v)
        }
    }
    return
}

import 包導入分組與排序

  • 同一文件,若是導入多個包,對其進行分組
  • 標準包第三方包自定義包,分別分組、空行分隔、排序

Bad:

import "a"
import "golang.org/x/sys"
import "runtime"
import "github.com/gin-gonic/gin"
import "b"
import "fmt"

Good:

import (
    "fmt"
    "runtime"

    "a"
    "b"

    "github.com/gin-gonic/gin"
    "golang.org/x/sys"
)

GoLand 設置以下:

goland-import-w600

類似的聲明進行分組

對於 varconsttype 等聲明語句:

  • 將類似的聲明放在一個組內

Bad:

const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

Good:

const (
    a = 1
    b = 2
)

var (
    a = 1
    b = 2
)

type (
    Area float64
    Volume float64
)
  • 僅將相關的聲明放在一組,不要將不相關的聲明放在一組

Bad:

type Operation int

const (
    Add Operation = iota + 1
    Subtract
    Multiply
    RoleName = "Role Name"
)

Good:

type Operation int

const (
    Add Operation = iota + 1
    Subtract
    Multiply
)

const RoleName = "Role Name"
  • 分組使用的位置沒有限制,函數內也可以使用分組

Bad:

func f() string {
    var red = color.New(0xff0000)
    var green = color.New(0x00ff00)
    var blue = color.New(0x0000ff)

    // ...
}

Good:

func f() string {
    var (
        red   = color.New(0xff0000)
        green = color.New(0x00ff00)
        blue  = color.New(0x0000ff)
    )

    // ...
}

函數/方法的參數/返回類型順序

  • 簡單類型優先於複雜類型

Bad:

func F(u User, n int) {}

Good:

func F(n int, u User) {}
  • 儘量將同種類型的參數放在相鄰位置,則只需寫一次類型

Bad:

func F(a int, c string, b int) {}

Good:

func F(a, b int, c string) {}
  • error 永遠在最後一個返回類型

Bad:

func F() (error, int) {}

Good:

func F() (int, error) {}

結構體屬性順序

咱們先看下示例:

結構體A - 定義:

struct {
    a string
    c string
    b bool
    d bool
}

結構體A - 大小爲40,內存佈局圖:

-w333

對比,結構體B - 定義:

struct {
    a string
    b bool
    c string
    d bool
}

結構體B - 大小爲48,內存佈局圖:

-w329

咱們發現,結構體的屬性順序不一樣,佔用的內存大小和佈局是徹底不一樣的

那咱們所以約定:將相同類型的屬性儘可能放置在一塊兒。即,推薦結構體A中的定義順序。

結構體中的嵌入

嵌入式類型(例如mutex)應位於結構體內的字段列表的頂部,而且必須有一個空行將嵌入式字段與常規字段分隔開。

Bad:

type Client struct {
    version int
    http.Client
}

Good:

type Client struct {
    http.Client

    version int
}

初始化結構體時必須指定字段名

必須在初始化結構體時指定字段名,不然相關工具和 Review 都不給過。若是不指定,會對代碼重構形成不可預期的後果。

Bad:

k := User{"John", "Doe", true}

Good:

k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}
惟一例外:若是有3個或更少的字段,則能夠在測試表中省略字段名
tests := []struct{
}{
    op Operation
    want string
}{
    {Add, "add"},
    {Subtract, "subtract"},
}

縮小變量做用域

  • 若是有可能,儘可能縮小變量做用範圍,除非它與減小嵌套的規則衝突

Bad:

err := ioutil.WriteFile(name, data, 0644)
if err != nil {
    return err
}

Good:

if err := ioutil.WriteFile(name, data, 0644); err != nil {
    return err
}
  • 若是須要在 if 以外使用函數調用的結果,則不該嘗試縮小範圍

Bad:

if data, err := ioutil.ReadFile(name); err == nil {
    err = cfg.Decode(data)
    if err != nil {
    return err
    }

    fmt.Println(cfg)
    return nil
} else {
    return err
}

Good:

data, err := ioutil.ReadFile(name)
if err != nil {
    return err
}

if err := cfg.Decode(data); err != nil {
    return err
}

fmt.Println(cfg)
return nil

Error 信息不該大寫或標點符號結束

Bad:

fmt.Errorf("Something bad.")

Good:

fmt.Errorf("something bad")

Error 描述信息是須要被包裹或引用描述的,那麼下面的代碼將告訴咱們爲什麼不該如此:

log.Printf("Reading %s: %v", filename, err)

slice

nil 是一個有效長度爲 0 的 slice
  • 零值切片可當即使用,無需調用make建立

Bad:

nums := []int{}
// or, nums := make([]int, 0)

if add1 {
    nums = append(nums, 1)
}

Good:

var nums []int

if add1 {
    nums = append(nums, 1)
}
  • 要檢查切片是否爲空,請始終使用 len(s) == 0,不要檢查 nil

Bad:

func isEmpty(s []string) bool {
    return s == nil
}

Good:

func isEmpty(s []string) bool {
    return len(s) == 0
}
  • 對於須要序列化的切片,則必須使用make初始化

Bad:

var v []int
s, _ := json.Marshal(v)
println(string(s))
// output: null

Good:

v := make([]int, 0)
s, _ := json.Marshal(v)
println(string(s))
// output: []

參考資料


感謝您的閱讀,以爲內容不錯,點個贊吧 😆

原文地址: https://shockerli.net/post/go-code-guide-style

相關文章
相關標籤/搜索