目錄
- 統(tǒng)一規(guī)范篇
- 命名篇
- 開發(fā)篇
- 優(yōu)化篇
統(tǒng)一規(guī)范篇
本篇主要描述了公司內(nèi)部同事都必須遵守的一些開發(fā)規(guī)矩,如統(tǒng)一開發(fā)空間戒祠,既使用統(tǒng)一的開發(fā)工具來保證代碼最后的格式的統(tǒng)一,開發(fā)中對文件和代碼長度的控制穿肄,必須經(jīng)過go語言自帶的檢測機制等示弓。
1.1 合理規(guī)劃目錄
【原則1.1】合理規(guī)劃目錄供屉,一個目錄中只包含一個包(實現(xiàn)一個模塊的功能)行冰,如果模塊功能復雜考慮拆分子模塊,或者拆分目錄伶丐。
說明:在Go中對于模塊的劃分是基于package這個概念,可以在一個目錄中可以實現(xiàn)多個package疯特,但是并不建議這樣的實現(xiàn)方式哗魂。主要的缺點是模塊之間的關系不清晰,另外不利于模塊功能擴展漓雅。
錯誤示例:
project
│ config.go
│ controller.go
│ filter.go
│ flash.go
│ log.go
│ memzipfile.go
│ mime.go
│ namespace.go
│ parser.go
│ router.go
│ staticfile.go
│ template.go
│ templatefunc.go
│ tree.go
│ util.go
| validation.go
| validators.go
推薦做法:
project
├─cache
│ │ cache.go
│ │ conv.go
│ │
│ └─redis
│ redis.go
├─config
│ │ config.go
│ │ fake.go
│ │ ini.go
│ └─yaml
│ yaml.go
├─logs
│ conn.go
│ console.go
│ file.go
│ log.go
│ smtp.go
└─validation
util.go
validation.go
validators.go
1.2 GOPATH設置
【建議1.2】使用單一的 GOPATH
雖說Go語言支持擁有多個 GOPATH录别,但多個GOPATH的情況并不具有彈性。GOPATH本身就是高度自我完備的(通過導入路徑)邻吞。有多個 GOPATH 會導致某些副作用组题,例如可能使用了給定的庫的不同的版本。你可能在某個地方升級了它抱冷,但是其他地方卻沒有升級崔列。而且,我還沒遇到過任何一個需要使用多個 GOPATH 的情況。所以只使用單一的 GOPATH赵讯,這會提升你 Go 的開發(fā)進度盈咳。
許多人不同意這一觀點,接下來我會做一些澄清边翼。像 etcd 或 camlistore 這樣的大項目使用了像 godep 這樣的工具鱼响,將所有依賴保存到某個目錄中。也就是說组底,這些項目自身有一個單一的 GOPATH丈积。它們只能在這個目錄里找到對應的版本。除非你的項目很大并且極為重要债鸡,否則不要為每個項目使用不同的 GOPAHT桶癣。如果你認為項目需要一個自己的 GOPATH 目錄,那么就創(chuàng)建它娘锁,否則不要嘗試使用多個 GOPATH牙寞。它只會拖慢你的進度。
所有項目共用一個workspace莫秆,如下圖所示:
workspace/
├── bin
├── pkg
│ └── linux_amd64
│
└── src
├── project1
│
└── project2
│
└── project3
│
└── …
優(yōu)點: 方便發(fā)布到github.com, 讓第三方通過go get等工具獲取间雀。
內(nèi)部項目,建議采用第一種工程結構镊屎。公開項目惹挟、提供給第三方集成的項目采用第二種項目結構。
1.3 import 規(guī)范
import路徑是一個唯一標示的字符串
import在多行的情況下缝驳,goimports會自動幫你格式化连锯,但是我們這里還是規(guī)范一下import的一些規(guī)范,如果你在一個文件里面引入了一個package用狱,還是建議采用如下格式:
import (
"fmt"
)
如果你的包引入了三種類型的包运怖,標準庫包,程序內(nèi)部包夏伊,第三方包摇展,建議采用如下方式進行組織你的包:
import (
"encoding/json"
"strings"
"myproject/models"
"myproject/controller"
"myproject/utils"
"github.com/astaxie/beego"
"github.com/go-sql-driver/mysql"
)
有順序的引入包,不同的類型采用空格分離溺忧,第一種實標準庫咏连,第二是項目包,第三是第三方包鲁森。
【規(guī)則1.3.1】在非測試文件(*_test.go)中祟滴,禁止使用 . 來簡化導入包的對象調(diào)用。
錯誤示例:
// 這是不好的導入
import . " pubcode/api/broker"
這種寫法不利于閱讀歌溉,因而不提倡垄懂。
【規(guī)則1.3.2】禁止使用相對路徑導入(./subpackage),所有導入路徑必須符合 go get 標準。
錯誤示例:
// 這是不好的導入
import "../net"
正確做法:
// 這是正確的做法
import "github.com/repo/proj/src/net"
【建議1.3.3】建議使用goimports工具或者IDE工具來管理多行import
go默認已經(jīng)有了gofmt工具埠偿,但是我們強烈建議使用goimport工具透罢,這個在gofmt的基礎上增加了自動刪除和引入包.
go get golang.org/x/tools/cmd/goimports
不同的編輯器有不同的配置, sublime的配置教程:http://michaelwhatcott.com/gosublime-goimports/
LiteIDE和GoLand默認已經(jīng)支持了goimports,如果你的不支持請點擊屬性配置->golangfmt->勾選goimports
保存之前自動fmt你的代碼冠蒋。
好處:import在多行的情況下羽圃,goimports工具會自動幫你格式化,自動刪除和引入包抖剿。很多IDE工具也可以自動檢查并糾正import路徑
1.4 代碼風格
Go語言對代碼風格作了很多強制的要求朽寞,并提供了工具gofmt, golint, go tool vet等工具檢查。
【規(guī)則1.4.1】提交代碼時斩郎,必須使用gofmt對代碼進行格式化脑融。
大部分的格式問題可以通過 gofmt 來解決,gofmt 自動格式化代碼缩宜,保證所有的 go 代碼與官方推薦的格式保持一致肘迎,所有格式有關問題,都以gofmt的結果為準锻煌。所以妓布,建議在提交代碼庫之前先運行一下這個命令。
gofmt(也可以用go fmt宋梧,其操作于程序包的級別匣沼,而不是源文件級別),讀入Go的源代碼捂龄,然后輸出按照標準風格縮進和垂直對齊的源碼逸吵,并且保留了根據(jù)需要進行重新格式化的注釋岩瘦。如果你想知道如何處理某種新的布局情況锣枝,可以運行gofmt墩弯;如果結果看起來不正確,則需要重新組織你的程序刀脏,不要把問題繞過去局荚。標準程序包中的所有Go代碼,都已經(jīng)使用gofmt進行了格式化愈污。
不需要花費時間對結構體中每個域的注釋進行排列,如下面的代碼轮傍,
type T struct {
name string // name of the object
value int // its value
}
gofmt將會按列進行排列:
type T struct {
name string // name of the object
value int // its value
}
【規(guī)則1.4.2】提交代碼時暂雹,必須使用golint對代碼進行檢查。
golint 會檢測的方面:
- 變量名規(guī)范
- 變量的聲明创夜,像var str string = "test"杭跪,會有警告,應該var str = "test"
- 大小寫問題,大寫導出包的要有注釋
- x += 1 應該 x++
等等
詳細可以看官方庫示例涧尿,https://github.com/golang/lint/tree/master/testdata
想速成的可以看Golang lint簡易使用方法自行學習使用
【建議1.4.3】提交代碼前系奉,必須使用go vet對代碼進行檢查。
如果說golint是檢查我們的代碼規(guī)范的話姑廉,那么vet工具則是可以幫我們靜態(tài)分析我們的源碼存在的各種問題缺亮,例如多余的代碼,提前return的邏輯桥言,struct的tag是否符合標準等萌踱。
go get golang.org/x/tools/cmd/vet
使用如下:
go vet .
1.5 大小約定
【建議1.5.1】單個文件長度不超過500行。
對開源引入代碼可以降低約束号阿,新增代碼必須遵循并鸵。
【建議1.5.2】單個函數(shù)長度不超過50行。
函數(shù)兩個要求:單一職責扔涧、要短小
【規(guī)則1.5.3】單個函數(shù)圈復雜度最好不要超過10园担,禁止超過15。
說明:圈復雜度越高枯夜,代碼越復雜弯汰,就越難以測試和維護,同時也說明函數(shù)職責不單一卤档。
【規(guī)則1.5.4】單行語句不能過長蝙泼,如不能拆分需要分行寫。一行最多120個字符劝枣。
換行時有如下建議:
換行時要增加一級縮進汤踏,使代碼可讀性更好;
低優(yōu)先級操作符處劃分新行舔腾;換行時操作符應保留在行尾溪胶;
換行時建議一個完整的語句放在一行,不要根據(jù)字符數(shù)斷行
示例:
if ((tempFlag == TestFlag) &&
(((counterVar - constTestBegin) % constTestModules) >= constTestThreshold)) {
// process code
}
【建議1.5.5】函數(shù)中縮進嵌套必須小于等于3層稳诚。
舉例哗脖,禁止出現(xiàn)以下這種鋸齒形的函數(shù):
func testUpdateOpts PushUpdateOptions) (err error) {
isNewRef := opts.OldCommitID == git.EMPTY_SHA
isDelRef := opts.NewCommitID == git.EMPTY_SHA
if isNewRef && isDelRef {
if isDelRef {
repo, err := GetRepositoryByName(owner.ID, opts.RepoName)
if err != nil {
if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) {
if err := CommitRepoAction(CommitRepoActionOptions{
PusherName: opts.PusherName,
RepoOwnerID: owner.ID,
RepoName: repo.Name,
RefFullName: opts.RefFullName,
OldCommitID: opts.OldCommitID,
NewCommitID: opts.NewCommitID,
Commits: &PushCommits{},
}); err != nil {
return fmt.Errorf("CommitRepoAction (tag): %v", err)
}
return nil
}
}
else {
owner, err := GetUserByName(opts.RepoUserName)
if err != nil {
return fmt.Errorf("GetUserByName: %v", err)
}
return nil
}
}
}
// other code
}
提示:如果發(fā)現(xiàn)鋸齒狀函數(shù),應通過盡早通過return等方法重構扳还。
【原則1.5.6】保持函數(shù)內(nèi)部實現(xiàn)的組織粒度是相近的才避。
舉例,不應該出現(xiàn)如下函數(shù):
func main() {
initLog()
//這一段代碼的組織粒度氨距,明顯與其他的不均衡
orm.DefaultTimeLoc = time.UTC
sqlDriver := beego.AppConfig.String("sqldriver")
dataSource := beego.AppConfig.String("datasource")
modelregister.InitDataBase(sqlDriver, dataSource)
Run()
}
應該改為:
func main() {
initLog()
initORM() //修改后桑逝,函數(shù)的組織粒度保持一致
Run()
}
參考鏈接
https://studygolang.com/articles/2059
https://studygolang.com/articles/12033
https://blog.csdn.net/shuanger_/article/details/48241767
https://blog.csdn.net/tanzhe2017/article/list
命名篇
本篇以開發(fā)時從上往下的順序既:開發(fā)前約定的基本命名規(guī)范、包俏让、常量楞遏、變量茬暇、結構體、參數(shù)寡喝、返回值的順序講解了開發(fā)中各個環(huán)節(jié)的命名規(guī)范糙俗。
2.1 基本命令規(guī)范
【規(guī)則2.1.1】需要注釋來補充的命名就不算是好命名。
說明:命名應該做到讓人見名知意预鬓,好的命名可以讓人節(jié)省關注注釋的時間巧骚。
【規(guī)則2.1.2】使用可搜索的名稱
說明:單字母名稱和數(shù)字常量很難從一大堆文字中搜索出來。單字母名稱僅適用于短方法中的本地變量珊皿,名稱長短應與其作用域相對應网缝。若變量或常量可能在代碼中多處使用,則應賦其以便于搜索的名稱蟋定。
【規(guī)則2.1.3】做有意義的區(qū)分
說明:要區(qū)分名稱粉臊,就要以讀者能鑒別不同之處的方式來區(qū)分
比如說Product 和 ProductInfo 和 ProductData 沒有區(qū)別,NameString 和 Name 沒有區(qū)別驶兜。
錯誤示例:
type Reader interface {
Read(p []byte) (n int, err error)
}
// 多個函數(shù)接口
type WriteFlusher interface {
Write([]byte) (int, error)
Flush() error
}
2.2 項目目錄名
【規(guī)則2.2.1】目錄名必須為全小寫單詞扼仲,允許加中劃線‘-’組合方式,但是頭尾不能為中劃線抄淑。
例如:
go-sql-driver
hsa-microservice
service-mgr
【建議2.2.2】雖然允許出現(xiàn)中劃線屠凶,但是盡量避免或少加中劃線。
2.3 包名
【原則2.3.1】取名盡量采取有意義的包名肆资,簡單和可閱讀矗愧。
【規(guī)則2.3.2】包名必須全部為小寫單詞,無下劃線郑原,越短越好唉韭。盡量不要與標準庫重名。
錯誤示例:
import (
"MyUtil" //包名大寫
"suffix_array" //有下劃線
"io/util suffixarray" //不僅長犯犁,還是個帶空格的包名
"io/ioutil" //與標準庫重名
)
說明:包名在被導入后属愤,會以 package.Func()方式使用,任何人使用你的包都得敲一遍該包名,因為包名也是類型和函數(shù)的一部分
例如buf := new(bytes.Buffer)
就不要取名為 bytes.BytesBuffer
酸役,這樣過于累贅住诸。
【規(guī)則2.3.3】禁止通過中劃線連接多個單詞的方式來命名包名。
package go-oci8 //編譯錯誤
【建議2.3.4】包名盡量與所在目錄名一致涣澡,引用時比較方便贱呐。
說明:這是因為在import導入的包是按目錄名來命名的,如果不一致入桂,代碼閱讀者就很困惑吼句。
2.4 文件名
和其它語言一樣,名字在Go中是非常重要的事格。它們甚至還具有語義的效果:一個名字在程序包之外的可見性是由它的首字符是否為大寫來確定的惕艳。因此,值得花費一些時間來討論Go程序中的命名約定驹愚。
【規(guī)則2.4.1】文件名必須為小寫單詞远搪,允許加下劃線‘_’組合方式,但是頭尾不能為下劃線逢捺。
例如: port_allocator.go
【建議2.4.2】雖然允許出現(xiàn)下劃線谁鳍,但是盡量避免。
說明:如果采用下劃線的方式劫瞳,注意避免跟下面保留特定用法的后綴沖突:
1)測試文件:_test.go
2)系統(tǒng)相關的文件:
_386.go倘潜、_amd64.go、_arm.go志于、_arm64.go涮因、_android.go、_darwin.go伺绽、_dragonfly.go养泡、_freebsd.go、_linux.go奈应、_nacl.go澜掩、_netbsd.go、_openbsd.go杖挣、_plan9.go肩榕、_solaris.go、_windows.go惩妇、_android_386.go株汉、_android_amd64.go、_android_arm.go屿附、_android_arm64.go郎逃、_darwin_386.go、_darwin_amd64.go挺份、_darwin_arm.go褒翰、_darwin_arm64.go、_dragonfly_amd64.go匀泊、_freebsd_386.go优训、_freebsd_amd64.go、_freebsd_arm.go各聘、_linux_386.go揣非、_linux_amd64.go、_linux_arm.go躲因、_linux_arm64.go早敬、_linux_mips64.go忌傻、_linux_mips64le.go、_linux_ppc64.go搞监、_linux_ppc64le.go水孩、_linux_s390x.go、_nacl_386.go琐驴、_nacl_amd64p32.go俘种、_nacl_arm.go、_netbsd_386.go绝淡、_netbsd_amd64.go宙刘、_netbsd_arm.go、_openbsd_386.go牢酵、_openbsd_amd64.go悬包、_openbsd_arm.go、_plan9_386.go茁帽、_plan9_amd64.go玉罐、_plan9_arm.go、_solaris_amd64.go潘拨、_windows_386.go
_windows_amd64.go
【建議2.4.3】文件名以功能為指引吊输,名字中不需再出現(xiàn)模塊名或者組件名。
說明:因為Go包的導入是與路徑有關的铁追,本身已經(jīng)隱含了模塊/組件信息季蚂。
2.5 常量
【規(guī)則2.5.1】常量&枚舉名采用大小寫混排的駝峰模式(Golang官方要求),不允許出現(xiàn)下劃線
示例:
const (
CategoryBooks = iota // 0
CategoryHealth // 1
CategoryClothing // 2
)
【建議2.5.2】按照功能來區(qū)分琅束,而不是將所有類型都分在一組扭屁,并建議將公共常量置于私有常量之前
示例:
const (
KindPage = "page"
// The rest are node types; home page, sections etc.
KindHome = "home"
KindSection = "section"
KindTaxonomy = "taxonomy"
KindTaxonomyTerm = "taxonomyTerm"
// Temporary state.
kindUnknown = "unknown"
// The following are (currently) temporary nodes,
// i.e. nodes we create just to render in isolation.
kindRSS = "RSS"
kindSitemap = "sitemap"
kindRobotsTXT = "robotsTXT"
kind404 = "404"
)
【規(guī)則2.2.3】如果是枚舉類型的常量,需要先創(chuàng)建相應類型
示例:
type tstCompareType int
const (
tstEq tstCompareType = iota
tstNe
tstGt
tstGe
tstLt
tstLe
)
【建議2.5.4】如果模塊的功能較為復雜涩禀、常量名稱容易混淆的情況下料滥,為了更好地區(qū)分枚舉類型,可以使用完整的前綴
示例:
type PullRequestStatus int
const (
PullRequestStatusConflict PullRequestStatus = iota
PullRequestStatusChecking
PullRequestStatusMergeable
)
2.6 變量
變量申明
【規(guī)則2.6.1】變量命名基本上遵循相應的英文表達或簡寫艾船,在相對簡單的環(huán)境(對象數(shù)量少葵腹、針對性強)中,可以將一些名稱由完整單詞簡寫為單個字母
例如:
- user 可以簡寫為 u
- userID 可以簡寫 uid
- 若變量類型為 bool 類型屿岂,則名稱應以 Has, Is, Can 或 Allow 開頭:
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
只有從其他標準移植過來的常量才和原來保持一致践宴,比如:
- 自定義的 http.StatusOK
- 移植過來的 tls.TLS_RSA_WITH_AES_128_CBC_SHA
變量命名慣例
【規(guī)則2.6.2】變量名稱一般遵循駝峰法,并且不允許出現(xiàn)下劃線爷怀,當遇到特有名詞時阻肩,需要遵循以下規(guī)則:
- 如果變量為私有,且特有名詞為首個單詞运授,則使用小寫烤惊,如:apiClient
- 其它情況都應當使用該名詞原有的寫法乔煞,如 APIClient、repoID撕氧、UserID
錯誤示例:UrlArray瘤缩,應該寫成 urlArray 或者 URLArray
下面列舉了一些常見的特有名詞:
"API","ASCII"伦泥,"CPU","CSS"锦溪,"DNS"不脯,"EOF",GUID"刻诊,"HTML"防楷,"HTTP","HTTPS"则涯,"ID","IP"复局,"JSON","LHS"粟判,"QPS"亿昏,"RAM","RHS"
"RPC", "SLA"档礁,"SMTP"角钩,"SSH","TLS"呻澜,"TTL","UI"递礼,"UID","UUID"羹幸,"URI"脊髓,"URL", "UTF8"栅受,"VM"将硝,"XML","XSRF"窘疮,"XSS"
【規(guī)則2.6.3】不要使用_來命名變量名袋哼,多個變量申明放在一起
正確示例:
var (
Found bool
count int
)
【規(guī)則2.6.4】在函數(shù)外部申明必須使用var,不要采用:=,容易踩到變量的作用域的問題闸衫。
全局變量名
【規(guī)則2.6.5】全局變量必須為大小寫混排的駝峰模式涛贯,不允許出現(xiàn)下劃線。首字母根據(jù)作為范圍確定大小寫蔚出。
例如:
var Global int //包外
var global int //包內(nèi)
【建議2.6.6】盡量避免跨package使用全局變量弟翘,盡量減少全局變量的使用虫腋。
局部變量名
【規(guī)則2.6.7】局部變量名必須為大小寫混排,且首字母小寫稀余,不能有下劃線悦冀。
例如:
result, err := MakeRegexpArray(str)
循環(huán)變量
【建議2.6.8】for循環(huán)變量可以使用單字母。
2.7 結構體(struct)
【規(guī)則2.7.1】struct申明和初始化格式采用多行
定義如下:
type User struct{
Username string
Email string
}
初始化如下:
u := User{
Username: "astaxie",
Email: "astaxie@gmail.com",
}
【規(guī)則2.7.2】結構體名必須為大小寫混排的駝峰模式睛琳,不允許出現(xiàn)下劃線盒蟆,可被包外部引用則首字母大寫;如僅包內(nèi)使用师骗,則首字母小寫历等。
例如:
type ServicePlan struct
type internalBroker struct
【建議2.7.3】結構名建議采用名詞、動名詞為好辟癌。
結構體名應該是名詞或名詞短語寒屯,如Custome、WikiPage黍少、Account寡夹、AddressParser,避免使用 Manager厂置、Processor菩掏、Data、Info农渊、這樣的類名患蹂,類名不應當是動詞。
2.8 接口名
接口命名規(guī)則:單個函數(shù)的接口名以”er”作為后綴砸紊,
【規(guī)則2.8.1】接口名必須為大小寫混排传于,支持包外引用則首字母大寫,僅包內(nèi)使用則首字母小寫醉顽。不能有下劃線沼溜,整體必須為名詞。
【建議2.8.2】單個函數(shù)的接口名以”er”作為后綴游添。
單個函數(shù)的接口名以”er”作為后綴系草,如Reader,Writer。接口的實現(xiàn)則去掉“er”唆涝。除非有更合適的單詞找都。
例如:
type Reader interface {...}
2.9 函數(shù)和方法名
函數(shù)
【規(guī)則2.9.1】函數(shù)名必須為大小寫混排的駝峰模式
函數(shù)名必須為大小寫混排的駝峰模式属韧,名字可以長但是得把功能怨酝,必要的參數(shù)描述清楚苍苞,不允許出現(xiàn)下劃線诅妹。
示例:
func MakeRegexpArrayOrDie // 暴露給包外部函數(shù)
func matchesRegexp // 包內(nèi)部函數(shù)
【建議2.9.2】函數(shù)名力求精簡準確顾瞻,并采用用動詞或動詞短
如 postPayment毒返、deletePage链快、save险领。并依 Javabean 標準加上 get、set戒职、is前綴栗恩。
例如:xxx + With + 需要的參數(shù)名 + And + 需要的參數(shù)名 + …..
方法
【規(guī)則2.9.3】方法接收名必須為大小寫混排,首字母小寫洪燥。方法接收者命名要能夠體現(xiàn)接收者對象磕秤。
【建議2.9.4】接收者名通常1個或者2個字母就夠,最長不能超過4個字母蚓曼。
【建議2.9.5】接收者名不要使用me亲澡,this 或者 self 這種泛指的名字。
例如:
func (c *Controller) Run(stopCh <-chan struct{})
參考:https://github.com/golang/go/wiki/CodeReviewComments#receiver-names
【建議2.9.6】定義方法時纫版,如果方法內(nèi)不會直接引用接收者,則省略掉接收者名客情。
舉例:
func (T) sayHi() {
// do things without T
}
func (*T) sayHello() {
// do things without *T
}
2.10 參數(shù)名
【規(guī)則2.10】參數(shù)名必須為大小寫混排其弊,且首字母小寫,不能有下劃線膀斋。
例如:
func MakeRegexpArray(str string)
2.11 返回值
【規(guī)則2.11.1】返回值如果是命名的梭伐,則必須大小寫混排,首字母小寫仰担。
【建議2.11.2】 函數(shù)的返回值應避免使用命名的參數(shù)糊识。
舉例:
func (n *Node) Bad() (node *Node, err error)
func (n *Node) Good() (*Node, error)
因為如果使用命名變量很容易導致臨時變量覆蓋而引起隱藏的bug。
例外情況:多個返回值類型相同的情況下摔蓝,使用命名返回值來區(qū)分不同的返回參數(shù)赂苗。
說明:命名返回值使代碼更清晰,同時更加容易讀懂贮尉。
舉例:
func getName()(firstName, lastName, nickName string){
firstName = "May"
lastName = "Chen"
nickName = "Babe"
return
}
參考:
https://github.com/golang/go/wiki/CodeReviewComments#named-result-parameters
https://golang.org/doc/effective_go.html#named-results
開發(fā)篇
說明:本篇主要是講解開發(fā)中各個環(huán)節(jié)的開發(fā)規(guī)范和對一些代碼的優(yōu)化寫法拌滋。在本文中有一些特別標黃的建議,我真的建議你好好看看那些代碼猜谚,因為那可能對你提高代碼開發(fā)會很有幫助败砂。
3.1 包
第三方包管理
【建議3.1.1】項目倉庫中包含全量的代碼
說明:將依賴源碼都放到當前工程的vendor目錄下,將全量的代碼保存到項目倉庫中魏铅,這樣做有利于避免受第三方變動的影響昌犹。
【建議3.1.2】建議采用 Glide 來管理第三方包
第三方包應該盡量獲取release版本,而非master分支的版本览芳。master上的版本通常是正在開發(fā)的非穩(wěn)定版本斜姥。
3.2 魔鬼數(shù)字
【規(guī)則3.2】代碼中禁止使用魔鬼數(shù)字。
說明:直接使用數(shù)字,造成代碼難以理解疾渴,也難以維護千贯。應采用有意義的靜態(tài)變量或枚舉來代替。
例外情況:有些特殊情況下搞坝,如循環(huán)或比較時采用數(shù)字0搔谴,-1,1桩撮,這些情況可采用數(shù)字敦第。
3.3 常量 & 枚舉
【==建議3.3.1==】 為整數(shù)常量添加 String() 方法
如果你利用 iota 來使用自定義的整數(shù)枚舉類型,務必要為其添加 String() 方法店量。例如芜果,像這樣:
type State int
const
(
Running State = iota
Stopped
Rebooting
Terminated
)
如果你創(chuàng)建了這個類型的一個變量,然后輸出融师,會得到一個整數(shù)(http://play.golang.org/p/V5VVFB05HB):
func main() {
state := Running
// print: "state 0"
fmt.Println("state ", state)
}
除非你回顧常量定義右钾,否則這里的0看起來毫無意義。只需要為State類型添加String()方法就可以修復這個問題(http://play.golang.org/p/ewMKl6K302):
func (s State) String() string {
switchs {
caseRunning:
return"Running"
caseStopped:
return"Stopped"
caseRebooting:
return"Rebooting"
caseTerminated:
return"Terminated"
default:
return"Unknown"
}
}
新的輸出是:state: Running旱爆。顯然現(xiàn)在看起來可讀性好了很多舀射。在你調(diào)試程序的時候,這會帶來更多的便利怀伦。同時還可以在實現(xiàn) MarshalJSON()脆烟、UnmarshalJSON() 這類方法的時候使用同樣的手段。
【==建議3.3.2==】讓 iota 從 a +1 開始增量
在前面的例子中同時也產(chǎn)生了一個我已經(jīng)遇到過許多次的 bug房待。假設你有一個新的結構體邢羔,有一個State字段:
type T struct{
Name string
Port int
State State
}
現(xiàn)在如果基于 T 創(chuàng)建一個新的變量,然后輸出桑孩,你會得到奇怪的結果(http://play.golang.org/p/LPG2RF3y39):
func main() {
t := T{Name:"example", Port: 6666}
// prints: "t {Name:example Port:6666 State:Running}"
fmt.Printf("t %+v\n", t)
}
看到 bug 了嗎拜鹤?State
字段沒有初始化,Go 默認使用對應類型的零值進行填充洼怔。由于State
是一個整數(shù)署惯,零值也就是0
,但在我們的例子中它表示Running
镣隶。
那么如何知道 State 被初始化了极谊?還是它真得是在Running
模式?沒有辦法區(qū)分它們安岂,那么這就會產(chǎn)生未知的轻猖、不可預測的 bug。不過域那,修復這個很容易咙边,只要讓 iota 從 +1 開始(http://play.golang.org/p/VyAq-3OItv):
const
(
Running State = iota + 1
Stopped
Rebooting
Terminated
)
現(xiàn)在t變量將默認輸出Unknown猜煮,不是嗎? :) :
func main() {
t := T{Name:"example", Port: 6666}
// 輸出: "t {Name:example Port:6666 State:Unknown}"
fmt.Printf("t %+v\n", t)
}
不過讓 iota 從零值開始也是一種解決辦法败许。例如王带,你可以引入一個新的狀態(tài)叫做Unknown
,將其修改為:
const
(
Unknown State = iota
Running
Stopped
Rebooting
Terminated
)
3.4 結構體
【規(guī)則3.4.1】對于要使用json轉換的結構體代碼市殷,變量名必須為大寫愕撰,否則你只會得到一個為空的對象
例如:
BaiduNewsItem struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
PubDate string `xml:"pubDate"`
Author string `xml:"author"`
}
【==建議3.4.2==】 在初始化結構體時使用帶有標簽的語法
這是一個無標簽語法的例子:
type T struct{
Foo string
Bar int
}
func main() {
t := T{"example", 123}// 無標簽語法
fmt.Printf("t %+v\n", t)
}
那么如果你添加一個新的字段到T結構體,代碼會編譯失敶浊蕖:
type T struct{
Foo string
Bar int
Qux string
}
func main() {
t := T{"example", 123}// 無法編譯
fmt.Printf("t %+v\n", t)
}
如果使用了標簽語法搞挣,Go的兼容性規(guī)則(http://golang.org/doc/go1compat)會處理代碼。例如在向net包的類型添加叫做Zone的字段音羞,參見:http://golang.org/doc/go1.1#library囱桨。回到我們的例子嗅绰,使用標簽語法:
type T struct{
Foo string
Bar int
Qux string
}
func main() {
t := T{Foo:"example", Bar: 123}
fmt.Printf("t %+v\n", t)
}
這個編譯起來沒問題舍肠,而且彈性也好。不論你如何添加其他字段到T結構體窘面。你的代碼總是能編譯貌夕,并且在以后的 Go 的版本也可以保證這一點。只要在代碼集中執(zhí)行go vet民镜,就可以發(fā)現(xiàn)所有的無標簽的語法。
【==建議3.4.3==】將結構體的初始化拆分到多行
如果有兩個以上的字段险毁,那么就用多行制圈。它會讓你的代碼更加容易閱讀,也就是說不要:
T{Foo: "example", Bar:someLongVariable, Qux:anotherLongVariable, B: forgetToAddThisToo}
而是:
T{
Foo: "example",
Bar: someLongVariable,
Qux: anotherLongVariable,
B: forgetToAddThisToo,
}
這有許多好處畔况,首先它容易閱讀鲸鹦,其次它使得允許或屏蔽字段初始化變得容易(只要注釋或刪除它們),最后添加其他字段也更容易(只要添加一行)跷跪。
3.5 運算符
【規(guī)則3.5】運算符前后馋嗜、逗號后面、if后面等需有單空格隔開吵瞻。
if err != nil {…}
c := a + b
return {}, err
例外情況:
go fmt
認為應該刪除空格的場景葛菇。例如,在傳參時橡羞,字符串拼接的”+”號眯停。
3.6 函數(shù)
【原則3.6.1】保持函數(shù)內(nèi)部實現(xiàn)的組織粒度是相近的。
舉例卿泽,不應該出現(xiàn)如下函數(shù):
func main() {
initLog()
//這一段代碼的組織粒度莺债,明顯與其他的不均衡
orm.DefaultTimeLoc = time.UTC
sqlDriver := beego.AppConfig.String("sqldriver")
dataSource := beego.AppConfig.String("datasource")
modelregister.InitDataBase(sqlDriver, dataSource)
Run()
}
應該改為:
func main() {
initLog()
initORM() //修改后,函數(shù)的組織粒度保持一致
Run()
}
【==建議3.6.2==】 返回函數(shù)調(diào)用
我已經(jīng)看過很多代碼例如(http://play.golang.org/p/8Rz1EJwFTZ):
func bar() (string, error) {
v, err := foo()
iferr != nil {
return"", err
}
returnv, nil
}
然而,你只需要:
func bar() (string, error) {
return foo()
}
更簡單也更容易閱讀(當然齐邦,除非你要對某些內(nèi)部的值做一些記錄)椎侠。
【==建議3.6.3==】 withContext 封裝函數(shù)
有時對于函數(shù)會有一些重復勞動,例如鎖/解鎖措拇,初始化一個新的局部上下文我纪,準備初始化變量等等……這里有一個例子:
func foo() {
mu.Lock()
defer mu.Unlock()
// foo 相關的工作
}
func bar() {
mu.Lock()
defer mu.Unlock()
// bar 相關的工作
}
func qux() {
mu.Lock()
defer mu.Unlock()
// qux 相關的工作
}
如果你想要修改某個內(nèi)容,你需要對所有的都進行修改儡羔。如果它是一個常見的任務宣羊,那么最好創(chuàng)建一個叫做withContext的函數(shù)。這個函數(shù)的輸入?yún)?shù)是另一個函數(shù)汰蜘,并用調(diào)用者提供的上下文來調(diào)用它:
func withLockContext(fn func()) {
mu.Lock
defer mu.Unlock()
fn()
}
只需要將之前的函數(shù)用這個進行封裝:
func foo() {
withLockContext(func() {
// foo 相關工作
})
}
func bar() {
withLockContext(func() {
// bar 相關工作
})
}
func qux() {
withLockContext(func() {
// qux 相關工作
})
}
不要光想著加鎖的情形仇冯。對此來說最好的用例是數(shù)據(jù)庫鏈接。現(xiàn)在對 withContext 函數(shù)作一些小小的改動:
func withDBContext(fn func(db DB)) error {
// 從連接池獲取一個數(shù)據(jù)庫連接
dbConn := NewDB()
returnfn(dbConn)
}
如你所見族操,它獲取一個連接苛坚,然后傳遞給提供的參數(shù),并且在調(diào)用函數(shù)的時候返回錯誤色难。你需要做的只是:
func foo() {
withDBContext(func(db *DB) error {
// foo 相關工作
})
}
func bar() {
withDBContext(func(db *DB) error {
// bar 相關工作
})
}
func qux() {
withDBContext(func(db *DB) error {
// qux 相關工作
})
}
你在考慮一個不同的場景泼舱,例如作一些預初始化?沒問題枷莉,只需要將它們加到withDBContext就可以了娇昙。這對于測試也同樣有效。
這個方法有個缺陷笤妙,它增加了縮進并且更難閱讀冒掌。再次提示,永遠尋找最簡單的解決方案蹲盘。
3.7 參數(shù)
【建議3.7.1】參數(shù)按邏輯緊密程度安排位置, 同種類型的參數(shù)放在相鄰位置股毫。
舉例:
func(m1, m2 *MenuEntry) bool
func (c *Client) Delete(key string, recursive bool, dir bool) (*RawResponse, error)
【建議3.7.2】避免使用標識參數(shù)來控制函數(shù)的執(zhí)行邏輯。
舉例:
func doAorB(flag int) {
if flag == flagA {
processA1()
return
}
if flag == flagB {
processB1()
return
}
}
特別是標識為布爾值時召衔,通過標識參數(shù)控制函數(shù)內(nèi)的邏輯铃诬,true執(zhí)行這部分邏輯,false執(zhí)行另外一部分邏輯苍凛,說明了函數(shù)職責不單一趣席。
【建議3.7.3】參數(shù)個數(shù)不要超過5個
參數(shù)過多通常意味著缺少封裝,不易維護毫深,容易出錯.
3.8 返回值
【規(guī)則3.8.1】函數(shù)返回值個數(shù)不要超過3個吩坝。
【建議3.8.2】如果函數(shù)的返回值超過3個,建議將其中關系密切的返回值參數(shù)封裝成一個結構體哑蔫。
3.9 注釋
Go提供了C風格的塊注釋/* */和C++風格的行注釋//钉寝。通常為行注釋弧呐;塊注釋大多數(shù)作為程序包的注釋,但也可以用于一個表達式中嵌纲,或者用來注釋掉一大片代碼俘枫。
godoc用來處理Go源文件,抽取有關程序包內(nèi)容的文檔逮走。在頂層聲明之前出現(xiàn)鸠蚪,并且中間沒有換行的注釋,會隨著聲明一起被抽取师溅,作為該項的解釋性文本茅信。這些注釋的本質和風格決定了godoc所產(chǎn)生文檔的質量。
Go代碼的注釋可以被godocs工具轉化為文檔發(fā)布墓臭。所以準確的代碼注釋除了能夠幫助閱讀代碼還有助于代碼手冊的生成蘸鲸。
Godoc工具說明可參考如下鏈接:
https://godoc.org/golang.org/x/tools/cmd/godoc
通用注釋要求
【原則3.9.1】編寫代碼首先考慮如何代碼自我解釋,然后才是添加注釋進行補充說明
說明:優(yōu)秀的代碼不寫注釋也可輕易讀懂窿锉,注釋無法把糟糕的代碼變好酌摇,需要很多注釋來解釋的代碼往往存在壞味道,需要重構嗡载。
示例:注釋不能消除代碼的壞味道:
// 判斷m是否為素數(shù)
// 返回值:: 1是素數(shù)窑多,0不是素數(shù)
func p(m int) int {
var i, k int
k = sqrt(m)
for i = 2; i <= k; i++ {
if m%i == 0 {
break // 發(fā)現(xiàn)整除,表示m不為素數(shù)洼滚,結束遍歷
}
}
// 遍歷中沒有發(fā)現(xiàn)整除的情況埂息,返回
if i > k {
return 1
}
// 遍歷中沒有發(fā)現(xiàn)整除的情況,返回
return 0
}
重構代碼后遥巴,不需要注釋:
// IsPrimeNumber return true if num is prime
func IsPrimeNumber(num int) bool {
var i int
sqrtOfNum := sqrt(num)
for i = 2; i <= sqrtOfNum; i++ {
if num%i == 0 {
return false
}
}
return true
}
【原則3.9.2】注釋的內(nèi)容要清楚耿芹、明了,含義準確挪哄,防止注釋二義性。
說明:有歧義的注釋反而會導致維護者更難看懂代碼琉闪,正如帶兩塊表反而不知道準確時間迹炼。
示例:注釋與代碼相矛盾,注釋內(nèi)容也不清楚颠毙,前后矛盾斯入。
// 上報網(wǎng)管時要求故障ID與恢復ID相一致
// 因此在此由告警級別獲知是不是恢復ID
// 若是恢復ID則設置為ClearId,否則設置為AlarmId
if ClearAlarmLevel != rcData.level {
SetAlarmID(rcData.AlarmId);
} else {
SetAlarmID(rcData.ClearId);
}
正確做法:修改注釋描述如下:
// 網(wǎng)管達成協(xié)議:上報故障ID與恢復ID由告警級別確定蛀蜜,若是清除級別刻两,ID設置為ClearId,否則設為AlarmId
...
【原則3.9.3】在代碼的功能滴某、意圖層次上進行注釋磅摹,即注釋用于解釋代碼難以直接表達的意圖滋迈,而不是重復描述代碼。
說明:注釋的目的是解釋代碼的目的户誓、功能和采用的方法饼灿,提供代碼以外的信息,幫助讀者理解代碼帝美,防止沒必要的重復注釋信息碍彭。
對于實現(xiàn)代碼中巧妙的、晦澀的悼潭、有趣的庇忌、重要的地方加以注釋。
注釋不是為了名詞解釋(what)舰褪,而是說明用途(why)皆疹。
示例:如下注釋純屬多余。
i++ // increment i
if receiveFlag { // if receiveFlag is TRUE
...
如下這種無價值的注釋不應出現(xiàn)(空洞的笑話抵知,無關緊要的注釋)墙基。
// 時間有限,現(xiàn)在是:04刷喜,根本來不及想為什么残制,也沒人能幫我說清楚
...
而如下的注釋則給出了有用的信息:
//由于xx編號網(wǎng)上問題,在xx情況下掖疮,芯片可能存在寫錯誤初茶,此芯片進行寫操作后,必須進行回讀校驗浊闪,如果回讀不正確恼布,需要再重復寫-回讀操作,最多重復三次搁宾,這樣可以解決絕大多數(shù)網(wǎng)上應用時的寫錯誤問題
time := 0
for (readReg(someAddr) != value) && (time < 3) {
writeReg(someAddr, value)
time++
}
對于實現(xiàn)代碼中巧妙的折汞、晦澀的、有趣的盖腿、重要的地方加以注釋爽待,出彩的或復雜的代碼塊前要加注釋,如:
// Divide result by two, taking into account that x contains the carry from the add.
for i := 0; i < len(result); i++ {
x = (x << 8) + result[i]
result[i] = x >> 1
x &= 1
}
【規(guī)則3.9.4】所有導出對象都需要注釋說明其用途翩腐;非導出對象根據(jù)情況進行注釋鸟款。必須時,應該說明值的取值范圍茂卦,及默認值何什。
【規(guī)則3.9.5】注釋的單行長度不能超過 80 個字符。
【規(guī)則3.9.6】注釋需要緊貼對應的包聲明和函數(shù)之前等龙,不能有空行处渣、
【規(guī)則3.9.7】非跨度很長的注釋伶贰,盡量使用 // 方式。
/*
* 1. 確保 template 存在
*/
改成:
// 1. 確保 template 存在
【規(guī)則3.9.8】避免多余的空格霍比,兩句注釋之間保持一個空格幕袱。
示例:
// 采用這樣的方式
// Sentence one. Sentence two.
// 而不是如下的方式
// Sentence one. Sentence two.
保持和Go的風格一樣,參考https://golang.org/cl/20022
【原則3.9.9】注釋第一條語句應該為一條概括語句悠瞬,并且使用被聲明的名字作為開頭们豌。
例如:
// Compile parses a regular expression and returns, if successful, a Regexp
// object that can be used to match against text.
func Compile(str string) (regexp *Regexp, err error) {
【建議3.9.10】//與注釋的文檔之間空一格。
示例:
// 采用如下方式
// This is a comment
// for humans.
//而不要采用如下方式:
//This is a comment
//for humans.
對于Go保留的語法浅妆,就不需要空一格
//go:generate go run gen.go
詳細的語法可以參考:https://golang.org/cmd/compile/#hdr-Compiler_Directives.
包注釋要求
【規(guī)則3.9.11】每個程序包都應該有一個包注釋望迎,一個位于package子句之前的塊注釋。
對于有多個文件的程序包凌外,包注釋只需要出現(xiàn)在一個文件中辩尊,任何一個文件都可以。包注釋應該用來介紹該程序包康辑,并且提供與整個程序包相關的信息摄欲。它將會首先出現(xiàn)在godoc頁面上,并會建立后續(xù)的詳細文檔疮薇。
/*
Package regexp implements a simple library for regular expressions.
The syntax of the regular expressions accepted is:
regexp:
concatenation { '|' concatenation }
concatenation:
{ closure }
closure:
term [ '*' | '+' | '?' ]
term:
'^'
'$'
'.'
character
'[' [ '^' ] character-ranges ']'
'(' regexp ')'
*/
package regexp
如果程序包很簡單胸墙,則包注釋可以非常簡短。
// Package path implements utility routines for
// manipulating slash-separated filename paths.
【規(guī)則3.9.12】不要依靠用空格進行對齊按咒。
注釋不需要額外的格式迟隅,例如星號橫幅喇辽。生成的輸出甚至可能會不按照固定寬度的字體進行展現(xiàn)浆劲,所以不要依靠用空格進行對齊—godoc,就像gofmt础拨,會處理這些事情掠抬。注釋是不作解析的普通文本吼野,所以HTML和其它注解,例如this两波,將會逐字的被復制箫锤。對于縮進的文本,godoc確實會進行調(diào)整雨女,來按照固定寬度的字體進行顯示,這適合于程序片段阳准。fmt package的包注釋使用了這種方式來獲得良好的效果氛堕。
根據(jù)上下文,godoc甚至可能不會重新格式化注釋野蝇,所以要確保它們看起來非常直接:使用正確的拼寫讼稚,標點括儒,以及語句結構,將較長的行進行折疊锐想,等等帮寻。
3.3.3 結構、接口及其他類型注釋要求
【建議3.27】類型定義一般都以單數(shù)信息描述赠摇。
示例:
// 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 { ...
函數(shù)與方法注釋要求
【建議3.9.13】函數(shù)聲明處注釋描述函數(shù)功能藕帜、性能及用法烫罩,包括輸入和輸出參數(shù)、函數(shù)返回值洽故、可重入的要求等贝攒;定義處詳細描述函數(shù)功能和實現(xiàn)要點,如實現(xiàn)的簡要步驟时甚、實現(xiàn)的理由隘弊、設計約束等
說明:重要的、復雜的函數(shù)荒适,提供外部使用的接口函數(shù)應編寫詳細的注釋梨熙。
【建議3.9.14】如果函數(shù)或者方法為判斷類型(返回值主要為bool類型),則以 returns true if 開頭吻贿。
如下例所示:
// HasPrefix returns true if name has any string in given slice as // prefix.
func HasPrefix(name string, prefixes []string) bool { ...
變量和常量的注釋要求
Go的聲明語法允許對聲明進行組合串结。單個的文檔注釋可以用來介紹一組相關的常量或者變量。由于展現(xiàn)的是整個聲明舅列,這樣的注釋通常非常簡單的肌割。
// Error codes returned by failures to parse an expression.
var (
ErrInternal = errors.New("regexp: internal error")
ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
...
)
一般建議采用這樣的方式:
var (
// BConfig is the default config for Application
BConfig *Config
// AppConfig is the instance of Config, store the config information from file
AppConfig *beegoAppConfig
// AppPath is the absolute path to the app
AppPath string
// GlobalSessions is the instance for the session manager
GlobalSessions *session.Manager
)
編碼注釋
在編碼階段應該同步寫好 變量、函數(shù)帐要、包 的注釋把敞,最后可以利用 godoc 命令導出文檔。注釋必須是完整的句子榨惠,句子的結尾應該用句號作為結尾(英文句號)奋早。注釋推薦用英文,可以在寫代碼過程中鍛煉英文的閱讀和書寫能力赠橙。而且用英文不會出現(xiàn)各種編碼的問題耽装。
每個包都應該有一個包注釋,一個位于 package 子句之前的塊注釋或行注釋期揪。包如果有多個 go 文件掉奄,只需要出現(xiàn)在一個 go 文件中即可。
// ping包實現(xiàn)了常用的ping相關的函數(shù)
package ping
Bug的注釋
針對代碼中出現(xiàn)的bug凤薛,可以采用如下教程使用特殊的注釋姓建,在godocs中可以做到注釋高亮:
// BUG(astaxie):This divides by zero.
var i float = 1/0
http://blog.golang.org/2011/03/godoc-documenting-go-code.html
帶mutex的struct必須是指針receivers
如果你定義的struct中帶有mutex,那么你的receivers必須是指針
recieved是值類型還是指針類型
到底是采用值類型還是指針類型主要參考如下原則:
func(w Win) Tally(playerPlayer)int //w不會有任何改變
func(w *Win) Tally(playerPlayer)int //w會改變數(shù)據(jù)
更多的請參考:https://code.google.com/p/go-wiki/wiki/CodeReviewComments#Receiver_Type
其他注釋要求
- 當某個部分等待完成時诞仓,可用 TODO: 開頭的注釋來提醒維護人員。
- 當某個部分存在已知問題進行需要修復或改進時速兔,可用 FIXME: 開頭的注釋來提醒維護人員墅拭。
- 當需要特別說明某個問題時,可用 NOTE: 開頭的注釋:
- 針對代碼中出現(xiàn)的bug涣狗,可以采用BUG(who):注釋谍婉,這些注釋將被識別為已知的bug,并包含在文檔的BUGS區(qū)屑柔。而其中的who應該是那些可以提供關于這個BUG更多信息的用戶名屡萤。
比如,下面就是一個bytes包中已知的問題:
// BUG(r): The rule Title uses for word boundaries does not handle Unicode punctuation properly.
3.10 錯誤
【原則3.10.1】錯誤處理的原則就是不能丟棄任何有返回err的調(diào)用掸宛,不要采用_丟棄死陆,必須全部處理。接收到錯誤唧瘾,要么返回err措译,要么實在不行就panic,或者使用log記錄下來
【規(guī)則3.10.2】error的信息不要采用大寫字母饰序,盡量保持你的錯誤簡短领虹,但是要足夠表達你的錯誤的意思。
【規(guī)則3.10.3】導出的錯誤變量的命名求豫,以Err開始塌衰,如ErrSomething,無需導出的錯誤變量命名蝠嘉,以Error作為后綴最疆,如specificError
舉例:
// 包級別的導出error.
var ErrSomething = errors.New("something went wrong")
func main() {
// 通常情況下我們只需要使用"err"
result, err := doSomething()
// 但是你也可以申明一個新的長名字變量,例如 "somethingError".
// Error作為后綴
var specificError error
result, specificError = doSpecificThing()
// ... 后面就使用specificError.
}
不好的例子:
var ErrorSomething = errors.New("something went wrong")
var SomethingErr = errors.New("something went wrong")
func main() {
var specificErr error
result, specificErr = doSpecificThing()
var errSpecific error
result, errSpecific = doSpecificThing()
var errorSpecific error
result, errorSpecific = doSpecificThing()
}
【規(guī)則3.10.4】公共包內(nèi)禁止使用panic蚤告,如果有panic需要內(nèi)部recover并返回error努酸。
說明:只有當實在不可運行的情況采用panic,例如文件無法打開杜恰,數(shù)據(jù)庫無法連接導致程序無法正常運行获诈,但是對于其他的package對外的接口不能有panic。
3.11 其他
【建議3.11.1】在代碼中編寫字符串形式的json時心褐,使用反單引號舔涎,而不是雙引號。
例如:
"{"key":"value"}"
改為格式更清晰的:
`
{
"key":"value"
}
`
【規(guī)則3.11.2】相對獨立的程序塊之間逗爹、變量說明之后必須加空行亡嫌,而邏輯緊密相關的代碼則放在一起。
不好的例子:
func formatResponseBody(res *http.Response, httpreq *httplib.BeegoHttpRequest, pretty bool) string {
body, err := httpreq.Bytes()
if err != nil {
log.Fatalln("can't get the url", err)
}
match, err := regexp.MatchString(contentJsonRegex, res.Header.Get("Content-Type"))
if err != nil {
log.Fatalln("failed to compile regex", err)
}
if pretty && match {
var output bytes.Buffer
err := json.Indent(&output, body, "", " ")
if err != nil {
log.Fatal("Response Json Indent: ", err)
}
return output.String()
}
return string(body)
}
應該改為:
func formatResponseBody(res *http.Response, httpreq *httplib.BeegoHttpRequest, pretty bool) string {
body, err := httpreq.Bytes()
if err != nil {
log.Fatalln("can't get the url", err)
}
match, err := regexp.MatchString(contentJsonRegex, res.Header.Get("Content-Type"))
if err != nil {
log.Fatalln("failed to compile regex", err)
}
if pretty && match {
var output bytes.Buffer
err := json.Indent(&output, body, "", " ")
if err != nil {
log.Fatal("Response Json Indent: ", err)
}
return output.String()
}
return string(body)
}
提示:當你需要為接下來的代碼增加注釋的時候,說明該考慮加一行空行了昼伴。
【規(guī)則3.11.3】盡早return:一旦有錯誤發(fā)生,馬上返回镣屹。
舉例:不要使用
if err != nil {
// error handling
} else {
// normal code
}
而推薦使用:
if err != nil {
// error handling
return // or continue, etc.
}
// normal code
這樣可以減少嵌套深度圃郊,代碼更加美觀。
【建議3.11.4】禁止出現(xiàn)2處及以上的重復代碼女蜈。
如果出現(xiàn)持舆,必須抽取為獨立小函數(shù)。不要擔心性能問題伪窖,編譯器會幫你搞定大部分的內(nèi)聯(lián)優(yōu)化逸寓。同時認真閱讀第四章節(jié)的“代碼質量保證優(yōu)先原則”
【建議3.11.5】if條件判斷, 同時使用超過3個表達式以上的時候, 使用switch替代。
例如:
if a == 0 || a == 1 || a == 2 || a == 3 {
// ...
}
建議改寫為:
switch a {
case 0, 1, 2, 3:
// ....
}
【建議3.11.6】定義bool變量時覆山,要避免判斷時出現(xiàn)雙重否定竹伸,應使用肯定形式的表達式。
舉例:
if !notFailed && !isReported { // 晦澀簇宽,不容易理解
notifyUser()
} else {
process()
}
應改為:
if isSuccess || isReported {
process()
} else {
notifyUser()
}
【建議3.11.7】for循環(huán)初始值從0開始勋篓,判斷條件使用<無等號的方式。
舉例:
for i := 1; i <= 10; i++ {
doSomeThing()
}
應改為:
for i := 0; i < 10; i++ {
doSomeThing()
}
這樣子可以迅速準確得出循環(huán)次數(shù)魏割。
【建議3.11.8】長句子打印或者調(diào)用譬嚣,使用參數(shù)進行格式化分行
我們在調(diào)用fmt.Sprint
或者log.Sprint
之類的函數(shù)時,有時候會遇到很長的句子钞它,我們需要在參數(shù)調(diào)用處進行多行分割:
下面是錯誤的方式:
log.Printf(“A long format string: %s %d %d %s”, myStringParameter, len(a),
expected.Size, defrobnicate(“Anotherlongstringparameter”,
expected.Growth.Nanoseconds() /1e6))
應該是如下的方式:
log.Printf(
“A long format string: %s %d %d %s”,
myStringParameter,
len(a),
expected.Size,
defrobnicate(
“Anotherlongstringparameter”,
expected.Growth.Nanoseconds()/1e6,
),
)
【==建議3.11.9==】 將 for-select 封裝到函數(shù)中
如果在某個條件下拜银,你需要從 for-select 中退出,就需要使用標簽遭垛。例如:
func main() {
L:
for{
select {
case<-time.After(time.Second):
fmt.Println("hello")
default:
break L
}
}
fmt.Println("ending")
}
如你所見尼桶,需要聯(lián)合break使用標簽。這有其用途耻卡,不過我不喜歡疯汁。這個例子中的 for 循環(huán)看起來很小,但是通常它們會更大卵酪,而判斷break的條件也更為冗長幌蚊。
如果需要退出循環(huán),我會將 for-select 封裝到函數(shù)中:
func main() {
foo()
fmt.Println("ending")
}
func foo() {
for{
select {
case<-time.After(time.Second):
fmt.Println("hello")
default:
return
}
}
}
你還可以返回一個錯誤(或任何其他值)溃卡,也是同樣漂亮的溢豆,只需要:
// 阻塞
if err := foo(); err != nil {
// 處理 err
}
【==建議3.11.10==】把 slice、map 等定義為自定義類型
將 slice 或 map 定義成自定義類型可以讓代碼維護起來更加容易瘸羡。假設有一個Server類型和一個返回服務器列表的函數(shù):
type Server struct{
Name string
}
func ListServers() []Server {
return[]Server{
{Name:"Server1"},
{Name:"Server2"},
{Name:"Foo1"},
{Name:"Foo2"},
}
}
現(xiàn)在假設需要獲取某些特定名字的服務器漩仙。需要對 ListServers() 做一些改動,增加篩選條件:
// ListServers 返回服務器列表。只會返回包含 name 的服務器队他【砺兀空的 name 將會返回所有服務器。
func ListServers(name string) []Server {
servers := []Server{
{Name:"Server1"},
{Name:"Server2"},
{Name:"Foo1"},
{Name:"Foo2"},
}
// 返回所有服務器
if name == ""{
return servers
}
// 返回過濾后的結果
filtered := make([]Server, 0)
for_, server := range servers {
if strings.Contains(server.Name, name) {
filtered = append(filtered, server)
}
}
return filtered
}
現(xiàn)在可以用這個來篩選有字符串Foo的服務器:
func main() {
servers := ListServers("Foo")
// 輸出:“servers [{Name:Foo1} {Name:Foo2}]”
fmt.Printf("servers %+v\n", servers)
}
顯然這個函數(shù)能夠正常工作麸折。不過它的彈性并不好锡凝。如果你想對服務器集合引入其他邏輯的話會如何呢?例如檢查所有服務器的狀態(tài)垢啼,為每個服務器創(chuàng)建一個數(shù)據(jù)庫記錄窜锯,用其他字段進行篩選等等……
現(xiàn)在引入一個叫做Servers的新類型,并且修改原始版本的 ListServers() 返回這個新類型:
type Servers []Server
// ListServers 返回服務器列表
func ListServers() Servers {
return[]Server{
{Name:"Server1"},
{Name:"Server2"},
{Name:"Foo1"},
{Name:"Foo2"},
}
}
現(xiàn)在需要做的是只要為Servers類型添加一個新的Filter()方法:
// Filter 返回包含 name 的服務器芭析∶空的 name 將會返回所有服務器。
func (s Servers) Filter(name string) Servers {
filtered := make(Servers, 0)
for_, server := range s {
ifstrings.Contains(server.Name, name) {
filtered = append(filtered, server)
}
}
return filtered
}
現(xiàn)在可以針對字符串Foo篩選服務器:
func main() {
servers := ListServers()
servers = servers.Filter("Foo")
fmt.Printf("servers %+v\n", servers)
}
哈馁启!看到你的代碼是多么的簡單了嗎驾孔?還想對服務器的狀態(tài)進行檢查?或者為每個服務器添加一條數(shù)據(jù)庫記錄进统?沒問題助币,添加以下新方法即可:
func (s Servers) Check()
func (s Servers) AddRecord()
func (s Servers) Len()
...
【==建議3.11.11==】 為訪問 map 增加 setter,getters
如果你重度使用 map 讀寫數(shù)據(jù)螟碎,那么就為其添加 getter 和 setter 吧眉菱。通過 getter 和 setter 你可以將邏輯封分別裝到函數(shù)里。這里最常見的錯誤就是并發(fā)訪問掉分。如果你在某個 goroutein 里有這樣的代碼:
m["foo"] = bar
還有這個:
delete(m,"foo")
會發(fā)生什么俭缓?你們中的大多數(shù)應當已經(jīng)非常熟悉這樣的競態(tài)了。簡單來說這個競態(tài)是由于 map 默認并非線程安全酥郭。不過你可以用互斥量來保護它們:
mu.Lock()
m["foo"] ="bar"
mu.Unlock()
以及:
mu.Lock()
delete(m,"foo")
mu.Unlock()
假設你在其他地方也使用這個 map华坦。你必須把互斥量放得到處都是!然而通過 getter 和 setter 函數(shù)就可以很容易的避免這個問題:
func Put(key, value string) {
mu.Lock()
m[key] = value
mu.Unlock()
}
func Delete(key string) {
mu.Lock()
delete(m, key)
mu.Unlock()
}
使用接口可以對這一過程做進一步的改進不从。你可以將實現(xiàn)完全隱藏起來惜姐。只使用一個簡單的、設計良好的接口椿息,然后讓包的用戶使用它們:
type Storage interface {
Delete(key string)
Get(key string) string
Put(key, value string)
}
這只是個例子歹袁,不過你應該能體會到。對于底層的實現(xiàn)使用什么都沒關系寝优。不光是使用接口本身很簡單条舔,而且還解決了暴露內(nèi)部數(shù)據(jù)結構帶來的大量的問題。
但是得承認乏矾,有時只是為了同時對若干個變量加鎖就使用接口會有些過分孟抗。理解你的程序迁杨,并且在你需要的時候使用這些改進。
參數(shù)傳遞
【建議3.11.12】 對于少量數(shù)據(jù)凄硼,不要傳遞指針
【建議3.11.13】 對于大量數(shù)據(jù)的 struct 可以考慮使用指針
【建議3.11.14】 傳入的參數(shù)是 map铅协,slice,chan 不要傳遞指針摊沉,因為 map警医,slice,chan 是引用類型坯钦,不需要傳遞指針的指針
自定義類型的string循環(huán)問題
如果自定義的類型定義了String方法,那么在打印的時候會產(chǎn)生隱藏的一些bug
type MyInt int
func (m MyInt) String() string {
return fmt.Sprint(m) //BUG:死循環(huán)
}
func(m MyInt) String() string {
return fmt.Sprint(int(m)) //這是安全的,因為我們內(nèi)部進行了類型轉換
}
panic
盡量不要使用panic侈玄,除非你知道你在做什么
在邏輯處理中禁用panic
在main包中只有當實在不可運行的情況采用panic婉刀,例如文件無法打開,數(shù)據(jù)庫無法連接導致程序無法
正常運行序仙,但是對于其他的package對外的接口不能有panic突颊,只能在包內(nèi)采用。
強烈建議在main包中使用log.Fatal來記錄錯誤潘悼,這樣就可以由log來結束程序律秃。
注意閉包的調(diào)用
【原則3.11.15】在循環(huán)中調(diào)用函數(shù)或者goroutine方法,一定要采用顯示的變量調(diào)用治唤,不要再閉包函數(shù)里面調(diào)用循環(huán)的參數(shù)
fori:=0;i<limit;i++{
go func(){ DoSomething(i) }() //錯誤的做法
go func(i int){ DoSomething(i) }(i)//正確的做法
}
參考:
http://golang.org/doc/articles/race_detector.html#Race_on_loop_counter
優(yōu)化篇
說明:本篇的意義是為開發(fā)提供一些經(jīng)過驗證的開發(fā)規(guī)則和建議棒动,讓開發(fā)在開發(fā)過程中避免低級錯誤,從而提高代碼的質量保證和性能效率
4.1 質量保證
4.1.1 代碼質量保證優(yōu)先原則
【原則4.1.1】代碼質量保證優(yōu)先原則:
(1)正確性宾添,指程序要實現(xiàn)設計要求的功能船惨。
(2)簡潔性,指程序易于理解并且易于實現(xiàn)缕陕。
(3)可維護性粱锐,指程序被修改的能力,包括糾錯扛邑、改進怜浅、新需求或功能規(guī)格變化的適應能力。
(4)可靠性蔬崩,指程序在給定時間間隔和環(huán)境條件下恶座,按設計要求成功運行程序的概率。
(5)代碼可測試性舱殿,指軟件發(fā)現(xiàn)故障并隔離奥裸、定位故障的能力,以及在一定的時間和成本前提下沪袭,進行測試設計湾宙、測試執(zhí)行的能力樟氢。
(6)代碼性能高效,指是盡可能少地占用系統(tǒng)資源侠鳄,包括內(nèi)存和執(zhí)行時間埠啃。
(7)可移植性,指為了在原來設計的特定環(huán)境之外運行伟恶,對系統(tǒng)進行修改的能力碴开。
4.1.2 對外接口原則
【原則4.1.2】對于主要功能模塊抽象模塊接口,通過interface提供對外功能博秫。
說明:Go語言其中一個特殊的功能就是interface潦牛,它讓面向對象,內(nèi)容組織實現(xiàn)非常的方便挡育。正確的使用這個特性可以使模塊的可測試性和可維護性得到很大的提升巴碗。對于主要功能包(模塊),在package包主文件中通過interface對外提供功能即寒。
示例:在buffer包的buffer.go中定義如下內(nèi)容
package buffer
import (
"policy_engine/models"
)
//other code …
type MetricsBuffer interface {
Store(metric *DataPoint) error
Get(dataRange models.MatchPolicyDataRange) (*MetricDataBuf, error)
Clear(redisKey string) error
Stop()
Stats() []MetrisBufferStat
GetByKey(metricKey string) []DataPoint
}
使用buffer package的代碼示例橡淆,通過interface定義,可以在不影響調(diào)用者使用的情況下替換package母赵∫菥簦基于這個特性,在測試過程中凹嘲,也可以通過實現(xiàn)符合interface要求的類來打樁實現(xiàn)測試目的师倔。
package metrics
import (
...//other import
"policy_engine/worker/metrics/buffer"
)
type MetricsClient struct {
logger lager.Logger
redisClient *store.RedisClient
conf *config.Config
metricsBuffer buffer.MetricsBuffer //interface類型定義的成員
metricsStatClient *metricstat.MetricsStatClient
stopSignal chan struct{}
}
func New(workerId string, redisClient *store.RedisClient, logger lager.Logger, conf *config.Config) *MetricsClient {
var metricsBuffer MetricsBuffer
if conf.MetricsBufferConfig.StoreType == config.METRICS_MEM_STORE {
//具有interface定義函數(shù)的package實現(xiàn),通過內(nèi)存保存數(shù)據(jù)
metricsBuffer = NewMemBuffer(logger, conf)
} else if conf.MetricsBufferConfig.StoreType == config.METRICS_REDIS_STORE {
//具有interface定義函數(shù)的package實現(xiàn)周蹭,通過redis保存數(shù)據(jù)
metricsBuffer = NewRedisBuffer(redisClient, logger, conf)
} else {
... //other code
}
... //other code
}
4.1.3 值與指針(T/*T)的使用原則
關于接收者對指針和值的規(guī)則是這樣的溯革,值方法可以在指針和值上進行調(diào)用,而指針方法只能在指針上調(diào)用谷醉。這是因為指針方法可以修改接收者致稀;使用拷貝的值來調(diào)用它們,將會導致那些修改會被丟棄俱尼。
對于使用T還是*T作為接收者抖单,下面是一些建議:
【建議4.1.3.1】基本類型傳遞時,盡量使用值傳遞遇八。
【建議4.1.3.2】如果傳遞字符串或者接口對象時矛绘,建議直接實例傳遞而不是指針傳遞。
【建議4.1.3.3】如果是map刃永、func货矮、chan,那么直接用T斯够。
【建議4.1.3.4】如果是slice囚玫,method里面不重新reslice之類的就用T喧锦。
【建議4.1.3.5】如果想通過method改變里面的屬性,那么請使用*T抓督。
【建議4.1.3.6】如果是struct燃少,并且里面包含了sync.Mutex之類的同步原語,那么請使用*T铃在,避免copy阵具。
【建議4.1.3.7】如果是一個大型的struct或者array,那么使用*T會比較輕量定铜,效率更高阳液。
【建議4.1.3.8】如果是struct、slice揣炕、array里面的元素是一個指針類型趁舀,然后調(diào)用函數(shù)又會改變這個數(shù)據(jù),那么對于讀者來說采用*T比較容易懂祝沸。
【建議4.1.3.9】其它情況下,建議采用*T越庇。
參考:https://github.com/golang/go/wiki/CodeReviewComments#pass-values
4.1.4 init的使用原則
每個源文件可以定義自己的不帶參數(shù)的init函數(shù)罩锐,來設置它所需的狀態(tài)。init是在程序包中所有變量聲明都被初始化卤唉,以及所有被導入的程序包中的變量初始化之后才被調(diào)用涩惑。
除了用于無法通過聲明來表示的初始化以外,init函數(shù)的一個常用法是在真正執(zhí)行之前進行驗證或者修復程序狀態(tài)的正確性桑驱。
【規(guī)則4.1.4.1】一個文件只定義一個init函數(shù)竭恬。
【規(guī)則4.1.4.2】一個包內(nèi)的如果存在多個init函數(shù),不能有任何的依賴關系熬的。
注意如果包內(nèi)有多個init痊硕,每個init的執(zhí)行順序是不確定的。
4.1.5 defer的使用原則
【建議4.1.5.1】如果函數(shù)存在多個返回的地方押框,則采用defer來完成如關閉資源岔绸、解鎖等清理操作。
說明:Go的defer語句用來調(diào)度一個函數(shù)調(diào)用(被延期的函數(shù))橡伞,在函數(shù)即將返回之前defer才被運行盒揉。這是一種不尋常但又很有效的方法,用于處理類似于不管函數(shù)通過哪個執(zhí)行路徑返回兑徘,資源都必須要被釋放的情況刚盈。典型的例子是對一個互斥解鎖,或者關閉一個文件挂脑。
【建議4.1.5.2】defer會消耗更多的系統(tǒng)資源藕漱,不建議用于頻繁調(diào)用的方法中欲侮。
【建議4.1.5.3】避免在for循環(huán)中使用defer。
說明:一個完整defer過程要處理緩存對象、參數(shù)拷貝苞冯,以及多次函數(shù)調(diào)用手形,要比直接函數(shù)調(diào)用慢得多。
錯誤示例:實現(xiàn)一個加解鎖函數(shù)忘伞,解鎖過程使用defer處理。這是一個非常小的函數(shù)沙兰,并且能夠預知解鎖的位置氓奈,使用defer編譯后會使處理產(chǎn)生很多無用的過程導致性能下降。
var lock sync.Mutex
func testdefer() {
lock.Lock()
defer lock.Unlock()
}
func BenchmarkTestDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
testdefer()
}
}
// 耗時結果
BenchmarkTestDefer 10000000 211 ns/op
推薦做法:如果能夠明確函數(shù)退出的位置鼎天,可以選擇不使用defer處理舀奶。保證功能不變的情況下,性能明顯提升斋射,是耗時是使用defer的1/3育勺。
var lock sync.Mutex
func testdefer() {
lock.Lock()
lock.Unlock() // ## 【修改】去除defer
}
func BenchmarkTestDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
testdefer()
}
}
// 耗時結果
BenchmarkTest" 30000000 43.5 ns/op
4.1.6 Goroutine使用原則
【規(guī)則4.1.6.1】確保每個goroutine都能退出。
說明:Goroutine是Go并行設計的核心罗岖,在實現(xiàn)功能時不可避免會使用到涧至,執(zhí)行goroutine時會占用一定的棧內(nèi)存。
啟動goroutine就相當于啟動了一個線程桑包,如果不設置線程退出的條件就相當于這個線程失去了控制南蓬,占用的資源將無法回收,導致內(nèi)存泄露哑了。
錯誤示例:示例中ready()啟動了一個goroutine循環(huán)打印信息到屏幕上赘方,這個goroutine無法終止退出。
package main
import (
"fmt"
"time"
)
func ready(w string, sec int) {
go func() { // ## 【錯誤】goroutine啟動之后無法終止
for {
time.Sleep(time.Duration(sec) * time.Second)
fmt.Println(w, "is ready! ")
}
}()
}
func main() {
ready("Tea", 2)
ready("Coffee", 1)
fmt.Println("I'm waiting")
time.Sleep(5 * time.Second)
}
推薦做法:對于每個goroutine都需要有退出機制弱左,能夠通過控制goroutine的退出窄陡,從而回收資源。通常退出的方式有:
- 使用標志位的方式拆火;
- 信號量泳梆;
- 通過channel通道通知;
注意:channel是一個消息隊列榜掌,一個goroutine獲取signal后优妙,另一個goroutine將無法獲取signal,以下場景下每個channel對應一個goroutine
package main
import (
"fmt"
"time"
)
func ready(w string, sec int, signal chan struct{}) {
go func() {
for {
select {
case <-time.Tick(time.Duration(sec) * time.Second):
fmt.Println(w, "is ready! ")
case <-signal: // 對每個goroutie增加一個退出選項
fmt.Println(w, "is close goroutine!")
return
}
}
}()
}
func main() {
signal1 := make(chan struct{}) // 增加一個signal
ready("Tea", 2, signal1)
signal2 := make(chan struct{}) // 增加一個signal
ready("Coffee", 1, signal2)
fmt.Println("I'm waiting")
time.Sleep(4 * time.Second)
signal1 <- struct{}{}
signal2 <- struct{}{}
time.Sleep(4 * time.Second)
}
【規(guī)則4.1.6.2】禁止在閉包中直接引用閉包外部的循環(huán)變量憎账。
說明:Go語言的特性決定了它會出現(xiàn)其它語言不存在的一些問題套硼,比如在循環(huán)中啟動協(xié)程,當協(xié)程中使用到了循環(huán)的索引值胞皱,往往會出現(xiàn)意想不到的問題邪意,通常需要程序員顯式地進行變量調(diào)用九妈。
for i := 0; i < limit; i++ {
go func() { DoSomething(i) }() //錯誤做法
go func(i int) { DoSomething(i)}(i) //正確做法
}
參考:http://golang.org/doc/articles/race_detector.html#Race_on_loop_counter
4.1.7 Channel使用原則
【規(guī)則4.1.7.1】傳遞channel類型的參數(shù)時應該區(qū)分其職責。
在只發(fā)送的功能中,傳遞channel類型限定為: c chan<- int
在只接收的功能中,傳遞channel類型限定為: c <-chan int
【規(guī)則4.1.7.2】確保對channel是否關閉做檢查雾鬼。
說明:在調(diào)用方法時不能想當然地認為它們都會執(zhí)行成功萌朱,當錯誤發(fā)生時往往會出現(xiàn)意想不到的行為,因此必須嚴格校驗并合適處理函數(shù)的返回值策菜。例如:channel在關閉后仍然支持讀操作晶疼,如果channel中的數(shù)據(jù)已經(jīng)被讀取,再次讀取時會立即返回0值與一個channel關閉指示又憨。如果不對channel關閉指示進行判斷翠霍,可能會誤認為收到一個合法的值。因此在使用channel時蠢莺,需要判斷channel是否已經(jīng)關閉寒匙。
錯誤示例:下面代碼中若cc已被關閉,如果不對cc是否關閉做檢查躏将,則會產(chǎn)生死循環(huán)锄弱。
package main
import (
"errors"
"fmt"
"time"
)
func main() {
var cc = make(chan int)
go client(cc)
for {
select {
case <-cc: //## 【錯誤】當channel cc被關閉后如果不做檢查則造成死循環(huán)
fmt.Println("continue")
case <-time.After(5 * time.Second):
fmt.Println("timeout")
}
}
}
func client(c chan int) {
defer close(c)
for {
err := processBusiness()
if err != nil {
c <- 0
return
}
c <- 1
}
}
func processBusiness() error {
return errors.New("domo")
}
推薦做法:對通道增加關閉判斷。
// 前面代碼略……
for {
select {
case _, ok := <-cc:
// 增加對chnnel關閉的判斷祸憋,防止死循環(huán)
if ok == false {
fmt.Println("channel closed")
return
}
fmt.Println("continue")
case <-time.After(5 * time.Second):
fmt.Println("timeout")
}
}
// 后面代碼略……
【規(guī)則4.1.7.3】禁止重復釋放channel会宪。
說明:重復釋放channel會觸發(fā)run-time panic,導致程序異常退出夺衍。重復釋放一般存在于異常流程判斷中,如果惡意攻擊者能夠構造成異常條件喜命,則會利用程序的重復釋放漏洞實施DoS攻擊沟沙。
錯誤示例:
func client(c chan int) {
defer close(c)
for {
err := processBusiness()
if err != nil {
c <- 0
close(c) // ## 【錯誤】可能會產(chǎn)生雙重釋放
return
}
c <- 1
}
}
推薦做法:確保創(chuàng)建的channel只釋放一次。
func client(c chan int) {
defer close(c)
for {
err := processBusiness()
if err != nil {
c <- 0 // ## 【修改】使用defer延遲close后壁榕,不再單獨進行close
return
}
c <- 1
}
}
4.1.8 其它
【建議4.1.8.1】使用go vet --shadow檢查變量覆蓋矛紫,以避免無意的變量覆蓋。
GO的變量賦值和聲明可以通過”:=”同時完成牌里,但是由于Go可以初始化多個變量颊咬,所以這個語法容易引發(fā)錯誤。下面的例子是一個典型的變量覆蓋引起的錯誤牡辽,第二個val的作用域只限于for循環(huán)內(nèi)部喳篇,賦值沒有影響到之前的val。
package main
import "fmt"
import "strconv"
func main() {
var val int64
if val, err := strconv.ParseInt("FF", 16, 64); nil != err {
fmt.Printf("parse int failed with error %v\n", err)
} else {
fmt.Printf("inside : val is %d\n", val)
}
fmt.Printf("outside : val is %d \n", val)
}
執(zhí)行結果:
inside : val is 255
outside : val is 0
正確的做法:
package main
import "fmt"
import "strconv"
func main() {
var val int64
var err error
if val, err = strconv.ParseInt("FF", 16, 64); nil != err {
fmt.Printf("parse int failed with error %v\n", err)
} else {
fmt.Printf("inside : val is %d\n", val)
}
fmt.Printf("outside : val is %d \n", val)
}
執(zhí)行結果:
inside : val is 255
outside : val is 255
【建議4.1.8.2】GO的結構體中控制使用Slice和Map态辛。
GO的slice和map等變量在賦值時麸澜,傳遞的是引用。從結果上看奏黑,是淺拷貝炊邦,會導致復制前后的兩個變量指向同一片數(shù)據(jù)编矾。這一點和Go的數(shù)組、C/C++的數(shù)組行為不同馁害,很容易出錯窄俏。
package main
import "fmt"
type Student struct {
Name string
Subjects []string
}
func main() {
sam := Student{
Name: "Sam", Subjects: []string{"Math", "Music"},
}
clark := sam //clark.Subject和sam.Subject是同一個Slice的引用!
clark.Name = "Clark"
clark.Subjects[1] = "Philosophy" //sam.Subject[1]也變了碘菜!
fmt.Printf("Sam : %v\n", sam)
fmt.Printf("Clark : %v\n", clark)
}
執(zhí)行結果:
Sam : {Sam [Math Philosophy]}
Clark : {Clark [Math Philosophy]}
作為對比凹蜈,請看作為Array定義的Subjects的行為:
package main
import "fmt"
type Student struct {
Name string
Subjects [2]string
}
func main() {
var clark Student
sam := Student{
Name: "Sam", Subjects: [2]string{"Math", "Music"},
}
clark = sam //clark.Subject和sam.Subject不同的Array
clark.Name = "Clark"
clark.Subjects[1] = "Philosophy" //sam.Subject不受影響!
fmt.Printf("Sam : %v\n", sam)
fmt.Printf("Clark : %v\n", clark)
}
執(zhí)行結果:
Sam : {Sam [Math Music]}
Clark : {Clark [Math Philosophy]}
編寫代碼時炉媒,建議這樣規(guī)避上述問題:
- 結構體內(nèi)盡可能不定義Slice踪区、Maps成員;
- 如果結構體有Slice吊骤、Maps成員缎岗,盡可能以小寫開頭、控制其訪問白粉;
- 結構體的賦值和復制传泊,盡可能通過自定義的深度拷貝函數(shù)進行;
【規(guī)則4.1.8.3】避免在循環(huán)引用調(diào)用 runtime.SetFinalizer鸭巴。
說明:指針構成的 "循環(huán)引用" 加上 runtime.SetFinalizer 會導致內(nèi)存泄露眷细。
runtime.SetFinalizer用于在一個對象 obj 被從內(nèi)存移除前執(zhí)行一些特殊操作,比如寫到日志文件中鹃祖。在對象被 GC 進程選中并從內(nèi)存中移除以前溪椎,SetFinalizer 都不會執(zhí)行,即使程序正常結束或者發(fā)生錯誤恬口。
錯誤示例:垃圾回收器能正確處理 "指針循環(huán)引用"校读,但無法確定 Finalizer 依賴次序,也就無法調(diào)用Finalizer 函數(shù)祖能,這會導致目標對象無法變成不可達狀態(tài)歉秫,其所占用內(nèi)存無法被回收。
package main
import (
"fmt"
"runtime"
"time"
)
type Data struct {
d [1024 * 100]byte
o *Data
}
func test() {
var a, b Data
a.o = &b
b.o = &a
// ## 【錯誤】循環(huán)和SetFinalize同時使用
runtime.SetFinalizer(&a, func(d *Data) { fmt.Printf("a %p final.\n", d) })
runtime.SetFinalizer(&b, func(d *Data) { fmt.Printf("b %p final.\n", d) })
}
func main() {
for { // ## 【錯誤】循環(huán)和SetFinalize同時使用
test()
time.Sleep(time.Millisecond)
}
}
通過跟蹤GC的處理過程养铸,可以看到如上代碼內(nèi)存在不斷的泄露:
go build -gcflags "-N -l" && GODEBUG="gctrace=1" ./test
gc11(1): 2+0+0 ms, 104 -> 104 MB 1127 -> 1127 (1180-53) objects
gc12(1): 4+0+0 ms, 208 -> 208 MB 2151 -> 2151 (2226-75) objects
gc13(1): 8+0+1 ms, 416 -> 416 MB 4198 -> 4198 (4307-109) objects
以上結果標紅的部分代表對象數(shù)量雁芙,我們在代碼中申請的對象都是局部變量,在正常處理過程中GC會持續(xù)的回收局部變量占用的內(nèi)存钞螟。但是在當前的處理過程中兔甘,內(nèi)存無法被GC回收,目標對象無法變成不可達狀態(tài)鳞滨。
推薦做法:需要避免內(nèi)存指針的循環(huán)引用以及runtime.SetFinalizer同時使用裂明。
【規(guī)則4.1.8.4】避免在for循環(huán)中使用time.Tick()函數(shù)。
如果在for循環(huán)中使用time.Tick(),它會每次創(chuàng)建一個新的對象返回闽晦,應該在for循環(huán)之外初始化一個ticker后扳碍,再在循環(huán)中使用:
ticker := time.Tick(time.Second)
for {
select {
case <-ticker:
// …
}
}
4.2 性能效率
4.2.1 Memory優(yōu)化
【建議4.2.1.1】將多次分配小對象組合為一次分配大對象。
比如, 將 *bytes.Buffer 結構體成員替換為bytes仙蛉。緩沖區(qū) (你可以預分配然后通過調(diào)用bytes.Buffer.Grow為寫做準備) 笋敞。這將減少很多內(nèi)存分配(更快)并且減緩垃圾回收器的壓力(更快的垃圾回收) 。
【建議4.2.1.2】將多個不同的小對象綁成一個大結構荠瘪,可以減少內(nèi)存分配的次數(shù)夯巷。
比如:將
for k, v := range m {
k, v := k, v // copy for capturing by the goroutine
go func() {
// use k and v
}()
}
替換為:
for k, v := range m {
x := struct{ k, v string }{k, v} // copy for capturing by the goroutine
go func() {
// use x.k and x.v
}()
}
這就將多次內(nèi)存分配(分別為k、v分配內(nèi)存)替換為了一次(為x分配內(nèi)存)哀墓。然而趁餐,這樣的優(yōu)化方式會影響代碼的可讀性,因此要合理地使用它篮绰。
【建議4.2.1.3】組合內(nèi)存分配的一個特殊情形是對分片數(shù)組進行預分配后雷。
如果清楚一個特定的分片的大小,可以對數(shù)組進行預分配:
type X struct {
buf []byte
bufArray [16]byte // Buf usually does not grow beyond 16 bytes.
}
func MakeX() *X {
x := &X{}
// Preinitialize buf with the backing array.
x.buf = x.bufArray[:0]
return x
}
【建議4.2.1.4】盡可能使用小數(shù)據(jù)類型吠各,并盡可能滿足硬件流水線(Pipeline)的操作臀突,如對齊數(shù)據(jù)預取邊界。
說明:不包含任何指針的對象(注意 strings,slices,maps 和 chans 包含隱含指針)不會被垃圾回收器掃描到贾漏。
比如候学,1GB 的分片實際上不會影響垃圾回收時間。因此如果你刪除被頻繁使用的對象指針纵散,它會對垃圾回收時間造成影響梳码。一些建議:使用索引替換指針,將對象分割為其中之一不含指針的兩部分伍掀。
【建議4.2.1.5】使用對象池來重用臨時對象掰茶,減少內(nèi)存分配。
標準庫包含的sync.Pool類型可以實現(xiàn)垃圾回收期間多次重用同一個對象硕盹。然而需要注意的是符匾,對于任何手動內(nèi)存管理的方案來說叨咖,不正確地使用sync.Pool會導致 use-after-free bug瘩例。
4.2.2 GC 優(yōu)化
【建議4.2.2.1】設置GOMAXPROCS為CPU的核心數(shù)目,或者稍高的數(shù)值甸各。
GC是并行的垛贤,而且一般在并行硬件上具有良好可擴展性。所以給 GOMAXPROCS 設置較高的值是有意義的趣倾,就算是對連續(xù)的程序來說也能夠提高垃圾回收速度聘惦。但是,要注意儒恋,目前垃圾回收器線程的數(shù)量被限制在 8 個以內(nèi)善绎。
【建議4.2.2.2】避免頻繁創(chuàng)建對象導致GC處理性能問題黔漂。
說明:盡可能少的申請內(nèi)存,減少內(nèi)存增量禀酱,可以減少甚至避免GC的性能沖擊炬守,提升性能。
Go語言申請的臨時局部變量(對象)內(nèi)存剂跟,都會受GC(垃圾回收)控制內(nèi)存的回收减途,其實我們在編程實現(xiàn)功能時申請的大部分內(nèi)存都屬于局部變量,所以與GC有很大的關系曹洽。
Go在GC的時候會發(fā)生Stop the world鳍置,整個程序會暫停,然后去標記整個內(nèi)存里面可以被回收的變量送淆,標記完成之后再恢復程序執(zhí)行税产,最后異步地去回收內(nèi)存。(暫停的時間主要取決于需要標記的臨時變量個數(shù)坊夫,臨時變量數(shù)量越多砖第,時間越長。Go 1.7以上的版本大幅優(yōu)化了GC的停頓時間环凿, Go 1.8下梧兼,通常的GC停頓的時間<100μs)
目前GC的優(yōu)化方式原則就是盡可能少的聲明臨時變量:
- 局部變量盡量利用
- 如果局部變量過多,可以把這些變量放到一個大結構體內(nèi)智听,這樣掃描的時候可以只掃描一個變量羽杰,回收掉它包含的很多內(nèi)存
本規(guī)則所說的創(chuàng)建對象包含:
- &obj{}
- new(abc{})
- make()
我們在編程實現(xiàn)功能時申請的大部分內(nèi)存都屬于局部變量,下面這個例子說明的是我們實現(xiàn)功能時需要注意的一個問題到推,適當?shù)恼{(diào)整可以減少GC的性能消耗考赛。
錯誤示例:
代碼中定義了一個tables對象,每個tables對象里面有一堆類似tableA和tableC這樣的一對一的數(shù)據(jù)莉测,也有一堆類似tableB這樣的一對多的數(shù)據(jù)颜骤。假設有1萬個玩家,每個玩家都有一條tableA和一條tableC的數(shù)據(jù)捣卤,又各有10條tableB的數(shù)據(jù)忍抽,那么將總的產(chǎn)生1w (tables) + 1w (tableA) + 1w (tableC) + 10w (tableB)的對象。
不好的例子:
// 對象數(shù)據(jù)表的集合
type tables struct {
tableA *tableA
tableB *tableB
tableC *tableC
// 此處省略一些表
}
// 每個對象只會有一條tableA記錄
type tableA struct {
fieldA int
fieldB string
}
// 每個對象有多條tableB記錄
type tableB struct {
city string
code int
next *tableB // 指向下一條記錄
}
// 每個對象只有一條tableC記錄
type tableC struct {
id int
value int64
}
建議一對一表用結構體董朝,一對多表用slice鸠项,每個表都加一個_is_nil的字段,用來表示當前的數(shù)據(jù)是否是有用的數(shù)據(jù)子姜,這樣修改的結果是祟绊,一萬個玩家,產(chǎn)生的對象總量是1w(tables)+1w([]tablesB),跟前面的差別很明顯:
// 對象數(shù)據(jù)表的集合
type tables struct {
tableA tableA
tableB []tableB
tableC tableC
// 此處省略一些表
}
// 每個對象只會有一條tableA記錄
type tableA struct {
_is_nil bool
fieldA int
fieldB string
}
// 每個對象有多條tableB記錄
type tableB struct {
_is_nil bool
city string
code int
next *tableB // 指向下一條記錄
}
// 每個對象只有一條tableC記錄
type tableC struct {
_is_nil bool
id int
value int64
}
4.2.3 其它優(yōu)化建議
【建議4.2.3.1】減少[]byte和string之間的轉換牧抽,盡量使用[]byte來處理字符嘉熊。
說明:Go里面string類型是immutable類型,而[]byte是切片類型扬舒,是可以修改的记舆,所以Go為了保證語法上面沒有二義性,在string和[]byte之間進行轉換的時候是一個實實在在的值copy呼巴,所以我們要盡量的減少不必要的這個轉變泽腮。
下面這個例子展示了傳遞slice但是進行了string的轉化,
func PrefixForBytes(b []byte) string {
return "Hello" + string(b)
}
所以我們可以有兩種方式衣赶,一種是保持全部的都是slice的操作诊赊,如下:
func PrefixForBytes(b []byte) []byte {
return append([]byte(“Hello”,b…))
}
還有一種就是全部是string的操作方式
func PrefixForBytes(str string) string {
return "Hello" + str
}
推薦閱讀:https://blog.golang.org/strings
【建議4.2.3.2】make申請slice/map時,根據(jù)預估大小來申請合適內(nèi)存府瞄。
說明:map和數(shù)組不同碧磅,可以根據(jù)新增的<key,value>對動態(tài)的伸縮,因此它不存在固定長度或者最大限制遵馆。
map的空間擴展是一個相對復雜的過程鲸郊,每次擴容會增加到上次大小的兩倍。它的結構體中有一個buckets和oldbuckets货邓,用來實現(xiàn)增量擴容秆撮,正常情況下直接使用buckets,oldbuckets為空换况,如果當前哈希表正在擴容职辨,則oldbuckets不為空,且buckets大小是oldbuckets大小的兩倍戈二。對于大的map或者會快速擴張的map舒裤,即便只是大概知道容量,也最好先標明觉吭。
slice是一個C語言動態(tài)數(shù)組的實現(xiàn)腾供,在對slice進行append等操作時,可能會造成slice的自動擴容鲜滩,其擴容規(guī)則:
- 如果新的大小是當前大小2倍以上伴鳖,則大小增長為新大小
- 否則循環(huán)以下操作:如果當前大小小于1024,按每次2倍增長绒北,否則每次按當前大小1/4增長黎侈,直到增長的大小超過或者等于新大小
推薦做法:在初始化map時指明map的容量察署。
- map := make(map[string]float, 100)
【建議4.2.3.3】字符串拼接優(yōu)先考慮bytes.Buffer闷游。
Golang字符串拼接常見有如下方式:
- fmt.Sprintf
- strings.Join
- string +
- bytes.Buffer
fmt.Sprintf會動態(tài)解析參數(shù),效率通常是最差的,而string是只讀的脐往,string+會導致多次對象分配與值拷貝休吠,而bytes.Buffer在預設大小情況下,通常只會有一次拷貝和分配业簿,不會重復拷貝和復制瘤礁,故效率是最佳的。
推薦做法:優(yōu)先使用bytes.Buffer梅尤,非關鍵路徑柜思,若考慮簡潔,可考慮其它方式巷燥,比如錯誤日志拼接使用fmt.Sprintf赡盘,但接口日志使用就不合適。
【建議4.2.3.4】避免使用CGO或者減少跨CGO調(diào)用次數(shù)缰揪。
說明:Go可以調(diào)用C庫函數(shù)陨享,但是Go帶有垃圾收集器且Go的棧是可變長,跟C實際是不能直接對接的钝腺,Go的環(huán)境轉入C代碼執(zhí)行前抛姑,必須為C新創(chuàng)建一個新的調(diào)用棧,把棧變量賦值給C調(diào)用棧艳狐,調(diào)用結束后再拷貝回來定硝,這個調(diào)用開銷非常大,相比直接GO語言調(diào)用毫目,單純的調(diào)用開銷喷斋,可能有2個甚至3個數(shù)量級以上,且Go目前還存在版本兼容性問題蒜茴。
推薦做法:盡量避免使用CGO星爪,無法避免時,要減少跨CGO調(diào)用次數(shù)粉私。
【建議4.2.3.5】避免高并發(fā)調(diào)用同步系統(tǒng)接口顽腾。
說明:編程世界同步場景更普遍,GO提供了輕量級的routine诺核,用同步來模擬異步操作抄肖,故在高并發(fā)下的,相比線程窖杀,同步模擬代價比較小漓摩,可以輕易創(chuàng)建數(shù)萬個并發(fā)調(diào)用。然而有些API是系統(tǒng)函數(shù)入客,而這些系統(tǒng)函數(shù)未提供異步實現(xiàn)管毙,程序中最常見的posix規(guī)范的文件讀寫都是同步腿椎,epoll異步可解決網(wǎng)絡IO,而對regular file是無法工作的夭咬。Go的運行時環(huán)境不可能提供超越操作系統(tǒng)API的能力啃炸,它依賴于系統(tǒng)syscall文件中暴露的api能力,而1.6版本還是多線程模擬卓舵,線程創(chuàng)建切換的代價也非常巨大南用,開源庫中有filepoller來模擬異步其實也基于這兩種思路,效率上也會大打折扣掏湾。
推薦做法:把諸如寫文件這樣的同步系統(tǒng)調(diào)用裹虫,要隔離到可控的routine中,而不是直接高并發(fā)調(diào)用融击。
【建議4.2.3.6】高并發(fā)時避免共享對象互斥恒界。
說明:在Go中,可以輕易創(chuàng)建10000個routine而對系統(tǒng)資源通常就是100M的內(nèi)存要求砚嘴,但是并發(fā)數(shù)多了十酣,在多線程中,當并發(fā)沖突在4個到8個線程間時际长,性能可能就開始出現(xiàn)拐點耸采,急劇下降,這同樣適應于Go工育,Go可以輕易創(chuàng)建routine虾宇,但對并發(fā)沖突的風險必須要做實現(xiàn)的處理。
推薦做法:routine需要是獨立的如绸,無沖突的執(zhí)行嘱朽,若routine間有并發(fā)沖突,則必須控制可能發(fā)生沖突的并發(fā)routine個數(shù)怔接,避免出現(xiàn)性能惡化拐點搪泳。
【建議4.2.3.7】長調(diào)用鏈或在函數(shù)中避免申明較多較大臨時變量。
routine的調(diào)用棧默認大小1.7版本已修改為2K扼脐,當棧大小不夠時岸军,Go運行時環(huán)境會做擴棧處理,創(chuàng)建10000個routine占用空間才20M瓦侮,所以routine非常輕量級艰赞,可以創(chuàng)建大量的并發(fā)執(zhí)行邏輯。而線程棧默認大小是1M肚吏,當然也可以設置到8K(有些系統(tǒng)可以設置4K)方妖,一般不會這么做,因為線程棧大小是固定的罚攀,不能隨需而變大党觅,不過實際CPU核一般都在100以內(nèi)雌澄,線程數(shù)是足夠的。
routine是怎么實現(xiàn)可變長棧呢仔役?當棧大小不夠時,它會新創(chuàng)建一個棧是己,通常是2倍大小增長又兵,然后把棧賦值過來,而棧中的指針變量需要搜索出來重新指向新的棧地址卒废,好處不是隨便有的沛厨,這里就明顯有性能開銷,而且這個開銷不小摔认。
說明:頻繁創(chuàng)建的routine逆皮,要注意棧生長帶來的性能風險,比如棧最終是2M大小参袱,極端情況下就會有數(shù)10次擴棧操作电谣,從而讓性能急劇下降。所以必須控制調(diào)用棧和函數(shù)的復雜度抹蚀,routine就意味著輕量級剿牺。
對于比較穩(wěn)定的routine,也要注意它的棧生長后會導致內(nèi)存飆升环壤。
【建議4.2.3.8】為高并發(fā)的輕量級任務處理創(chuàng)建routine池晒来。
說明:Routine是輕量級的,但對于高并發(fā)的輕量級任務處理郑现,頻繁創(chuàng)建routine來執(zhí)行湃崩,執(zhí)行效率也是非常低效率的。
推薦做法:高并發(fā)的輕量級任務處理接箫,需要使用routine池攒读,避免對調(diào)度和GC帶來沖擊。
【建議4.2.3.9】建議版本提供性能/內(nèi)存監(jiān)控的功能辛友,并動態(tài)開啟關閉整陌,但不要長期開啟pprof提供的CPU與MEM profile功能。
Go提供了pprof工具包瞎领,可以運行時開啟CPU與內(nèi)存的profile信息泌辫,便于定位熱點函數(shù)的性能問題,而MEM的profile可以定位內(nèi)存分配和泄漏相關問題九默。開啟相關統(tǒng)計震放,跟GC一樣,也會嚴重干擾性能驼修,因而不要長期開啟殿遂。
推薦做法:做測試和問題定位時短暫開啟诈铛,現(xiàn)網(wǎng)運行驻民,可以開啟短暫時間收集相關信息荠呐,同時要確保能夠自動關閉掉厘唾,避免長期打開携兵。