go語言學習總結(五十)Uber Go 語言編程規範

2020-09-03 coder人生

相信很多人前兩天都看到 Uber 在 github 上面開源的 Go 語言編程規範了,原文在這裡:https://github.com/uber-go/guide/blob/master/style.md 。我們今天就來簡單了解一下國外大廠都是如何來寫代碼的。行文倉促,錯誤之處,多多指正。另外如果覺得還不錯,也歡迎分享給更多的人。

1. 介紹

英文原文標題是 Uber Go Style Guide,這裡的 Style 是指在管理我們代碼的時候可以遵從的一些約定。

這篇編程指南的初衷是更好的管理我們的代碼,包括去編寫什麼樣的代碼,以及不要編寫什麼樣的代碼。我們希望通過這份編程指南,代碼可以具有更好的維護性,同時能夠讓我們的開發同學更高效地編寫 Go 語言代碼。

這份編程指南最初由 Prashant Varanasi 和 Simon Newton 編寫,旨在讓其他同事快速地熟悉和編寫 Go 程序。經過多年發展,現在的版本經過了多番修改和改進了。這是我們在 Uber 遵從的編程範式,但是很多都是可以通用的,如下是其他可以參考的連結:

所有的提交代碼都應該通過 golint 和 go vet 檢測,建議在代碼編輯器上面做如下設置:

保存的時候運行 goimports

使用 golint 和 go vet 去做錯誤檢測。

你可以通過下面連結發現更多的 Go 編輯器的插件: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

2. 編程指南

2.1 指向 Interface 的指針

在我們日常使用中,基本上不會需要使用指向 interface 的指針。當我們將 interface 作為值傳遞的時候,底層數據就是指針。Interface 包括兩方面:

一個包含 type 信息的指針

一個指向數據的指針

如果你想要修改底層的數據,那麼你只能使用 pointer。

2.2 Receiver 和 Interface

使用值作為 receiver 的時候 method 可以通過指針調用,也可以通過值來調用。

type S struct { data string}func (s S) Read() string { return s.data}func (s *S) Write(str string) { s.data = str}sVals := map[int]S{1: {&34;}}// You can only call Read using a valuesVals[1].Read()// This will not compile:// sVals[1].Write(&34;)sPtrs := map[int]*S{1: {&34;}}// You can call both Read and Write using a pointersPtrs[1].Read()sPtrs[1].Write(&34;)相似的,pointer 也可以滿足 interface 的要求,儘管 method 使用 value 作為 receiver。type F interface { f()}type S1 struct{}func (s S1) f() {}type S2 struct{}func (s *S2) f() {}s1Val := S1{}s1Ptr := &S1{}s2Val := S2{}s2Ptr := &S2{}var i Fi = s1Vali = s1Ptri = s2Ptr// The following doesn&34;pkg/errors&34;Could not open&34;could not open&34;could not open&34;unknown error&34;could not open&34;unknown error&34;file %q not found&34;not found&34;unknown error&34;file %q not found&34;unknown error&34;file %q not found&34;foo&34;unknown error&34;pkg/errors&34;pkg/errors&34;failed to create new store: %s&34;new store: %s&34;bar must not be empty&34;USAGE: foo <bar>&34;bar must not be empty&34;USAGE: foo <bar>&34;&34;test&34;failed to set up test&34;&34;test&34;failed to set up test&34;Hello world&34;Hello world&34;a&34;b&34;a&34;b&34;fmt&34;os&34;go.uber.org/atomic&34;golang.org/x/sync/errgroup&34;fmt&34;os&34;go.uber.org/atomic&34;golang.org/x/sync/errgroup&34;net/http&34;example.com/client-go&34;example.com/trace/v2&34;fmt&34;os&34;golang.net/x/trace&34;fmt&34;os&34;runtime/trace&34;golang.net/x/trace&34;Invalid v: %v&34;Invalid v: %v&34;A&39;t need to specify// the type again.func F() string { return &34; }

上面說的第二種兩邊數據類型不同的情況。

type myError struct{}func (myError) Error() string { return &34; }func F() myError { return myError{} }var _e error = F()// F returns an object of type myError but we want error.

4.9 對於不做 export 的全局變量使用前綴 _

對於同一個 package 下面的多個文件,一個文件中的全局變量可能會被其他文件誤用,所以建議使用 _ 來做前綴。(其實這條規則有待商榷)

Bad

// foo.goconst ( defaultPort = 8080 defaultUser = &34;)// bar.gofunc Bar() { defaultPort := 9090 ... fmt.Println(&34;, defaultPort) // We will not see a compile error if the first line of // Bar() is deleted.}

Good

// foo.goconst ( _defaultPort = 8080 _defaultUser = &34;)

4.10 struct 嵌套

struct 中的嵌套類型在 field 列表排在最前面,並且用空行分隔開。

Bad

type Client struct { version int http.Client}

Good

type Client struct { http.Client version int}

4.11 struct 初始化的時候帶上 Field

這樣會更清晰,也是 go vet 鼓勵的方式

Bad

k := User{&34;, &34;, true}

Good

k := User{ FirstName: &34;, LastName: &34;, Admin: true,}

4.12 局部變量聲明

變量聲明的時候可以使用 := 以表示這個變量被顯示的設置為某個值。

Bad

var s = &34;Goods := &34;

但是對於某些情況使用 var 反而表示的更清晰,比如聲明一個空的 slice: Declaring Empty Slices

Bad

func f(list []int) { filtered := []int{} for _, v := range list { if v > 10 { filtered = append(filtered, v) } }}

Good

func f(list []int) { var filtered []int for _, v := range list { if v > 10 { filtered = append(filtered, v) } }}

4.13 nil 是合法的 slice

在返回值是 slice 類型的時候,直接返回 nil 即可,不需要顯式地返回長度為 0 的 slice。

Bad

if x == &34; { return []int{}}

Good

if x == &34; { return nil}

判斷 slice 是不是空的時候,使用 len(s) == 0。

Bad

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

Good

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

使用 var 聲明的 slice 空值可以直接使用,不需要 make()。

Bad

nums := []int{}// or, nums := make([]int)if add1 { nums = append(nums, 1)}if add2 { nums = append(nums, 2)}

Good

var nums []intif add1 { nums = append(nums, 1)}if add2 { nums = append(nums, 2)}

4.14 避免 scope

Bad

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

Good

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

當然某些情況下,scope 是不可避免的,比如

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

4.15 避免參數語義不明確(Avoid Naked Parameters)

Naked Parameter 指的應該是意義不明確的參數,這種情況會破壞代碼的可讀性,可以使用 C 分格的注釋(/*...*/)進行注釋。

Bad

// func printInfo(name string, isLocal, done bool)printInfo(&34;, true, true)

Good

// func printInfo(name string, isLocal, done bool)printInfo(&34;, true /* isLocal */, true /* done */)

對於上面的示例代碼,還有一種更好的處理方式是將上面的 bool 類型換成自定義類型。

type Region intconst ( UnknownRegion Region = iota Local)type Status intconst ( StatusReady = iota + 1 StatusDone // Maybe we will have a StatusInProgress in the future.)func printInfo(name string, region Region, status Status)

4.16 使用原生字符串,避免轉義

Go 支持使用反引號,也就是 「`」 來表示原生字符串,在需要轉義的場景下,我們應該儘量使用這種方案來替換。

Bad

wantError := &34;test\&34;

Good

wantError := `unknown error:&34;`

4.17 Struct 引用初始化

使用 &T{} 而不是 new(T) 來聲明對 T 類型的引用,使用 &T{} 的方式我們可以和 struct 聲明方式 T{} 保持統一。

Bad

sval := T{Name: &34;}// inconsistentsptr := new(T)sptr.Name = &34;

Good

sval := T{Name: &34;}sptr := &T{Name: &34;}

4.18 字符串 string format

如果我們要在 Printf 外面聲明 format 字符串的話,使用 const,而不是變量,這樣 go vet 可以對 format 字符串做靜態分析。

Bad

msg := &34;fmt.Printf(msg, 1, 2)

Good

const msg = &34;fmt.Printf(msg, 1, 2)

4.19 Printf 風格函數命名

當聲明 Printf 風格的函數時,確保 go vet 可以對其進行檢測。可以參考:Printf family 。

另外也可以在函數名字的結尾使用 f 結尾,比如: WrapF,而不是 Wrap。然後使用 go vet

$ go vet -printfuncs=wrapf,statusf

更多參考: go vet: Printf family check.

5. 編程模式(Patterns)

5.1 Test Tables

當測試邏輯是重複的時候,通過 subtests 使用 table 驅動的方式編寫 case 代碼看上去會更簡潔。

Bad

// func TestSplitHostPort(t *testing.T)host, port, err := net.SplitHostPort(&34;)require.NoError(t, err)assert.Equal(t, &34;, host)assert.Equal(t, &34;, port)host, port, err = net.SplitHostPort(&34;)require.NoError(t, err)assert.Equal(t, &34;, host)assert.Equal(t, &34;, port)host, port, err = net.SplitHostPort(&34;)require.NoError(t, err)assert.Equal(t, &34;, host)assert.Equal(t, &34;, port)host, port, err = net.SplitHostPort(&34;)require.NoError(t, err)assert.Equal(t, &34;, host)assert.Equal(t, &34;, port)

Good

// func TestSplitHostPort(t *testing.T)tests := []struct{ give string wantHost string wantPort string}{ { give: &34;, wantHost: &34;, wantPort: &34;, }, { give: &34;, wantHost: &34;, wantPort: &34;, }, { give: &34;, wantHost: &34;, wantPort: &34;, }, { give: &34;, wantHost: &34;, wantPort: &34;, },}for _, tt := range tests { t.Run(tt.give, func(t *testing.T) { host, port, err := net.SplitHostPort(tt.give) require.NoError(t, err) assert.Equal(t, tt.wantHost, host) assert.Equal(t, tt.wantPort, port) })}

很明顯,使用 test table 的方式在代碼邏輯擴展的時候,比如新增 test case,都會顯得更加的清晰。

在命名方面,我們將 struct 的 slice 命名為 tests,同時每一個 test case 命名為 tt。而且,我們強烈建議通過 give 和 want 前綴來表示 test case 的 input 和 output 的值。

tests := []struct{ give string wantHost string wantPort string}{ // ...}for _, tt := range tests { // ...}

5.2 Functional Options

關於 functional options 簡單來說就是通過類似閉包的方式來進行函數傳參。

Bad

// package dbfunc Connect( addr string, timeout time.Duration, caching bool,) (*Connection, error) { // ...}// Timeout and caching must always be provided,// even if the user wants to use the default.db.Connect(addr, db.DefaultTimeout, db.DefaultCaching)db.Connect(addr, newTimeout, db.DefaultCaching)db.Connect(addr, db.DefaultTimeout, false /* caching */)db.Connect(addr, newTimeout, false /* caching */)Goodtype options struct { timeout time.Duration caching bool}// Option overrides behavior of Connect.type Option interface { apply(*options)}type optionFunc func(*options)func (f optionFunc) apply(o *options) { f(o)}func WithTimeout(t time.Duration) Option { return optionFunc(func(o *options) { o.timeout = t })}func WithCaching(cache bool) Option { return optionFunc(func(o *options) { o.caching = cache })}// Connect creates a connection.func Connect( addr string, opts ...Option,) (*Connection, error) { options := options{ timeout: defaultTimeout, caching: defaultCaching, } for _, o := range opts { o.apply(&options) } // ...}// Options must be provided only if needed.db.Connect(addr)db.Connect(addr, db.WithTimeout(newTimeout))db.Connect(addr, db.WithCaching(false))db.Connect( addr, db.WithCaching(false), db.WithTimeout(newTimeout),)

更多參考:

Self-referential functions and the design of optionsFunctional options for friendly APIs

註:關於 functional option 這種方式我本人也強烈推薦,我很久以前也寫過一篇類似的文章,感興趣的可以移步: 寫擴展性好的代碼:函數

6. 總結

Uber 開源的這個文檔,通篇讀下來給我印象最深的就是:保持代碼簡潔,並具有良好可讀性。不得不說,相比於國內很多 「代碼能跑就完事了」 這種寫代碼的態度,這篇文章或許可以給我們更多的啟示和參考。

相關焦點

  • [Go 語言教程] Go 語言簡介
    Go 語言教程1 Go 語言介紹Go 即Golang,是Google公司2009年11月正式對外公開的一門程式語言。Go是靜態強類型語言,是區別於解析型語言的編譯型語言。2 Go語言特性跨平臺的編譯型語言語法接近C語言管道(channel),切片(slice),並發(routine)有垃圾回收的機制支持面向對象和面向過程的編程模式3 Go 語言特色編程模式比較簡單,沒有複雜的設計模式全部源碼編譯到一個文件,編譯速度很快最新版本也有動態庫形式
  • 來自於美國矽谷 Uber 的語言編碼規範
    今天我給大家推薦的這個開源項目啊,是來自於 Uber 的 Go 語言編碼規範,我們可以看一看人家矽谷公司的語言編碼規範,向優秀的公司學習。放心,今天給大家推薦的這份規範,是中文翻譯版,所以你英語不好,也沒關係,開源作者已經幫大家翻譯好了。
  • go語言學習總結(四十八)深入理解 Go Channel
    channel 是 Go 語言中的一個非常重要的特性,這篇文章來深入了解一下 channel。1. CSP要想理解 channel 要先知道 CSP 模型。func main() { var x chan int go func() { x <- 1 }() <-x}$ go run channel1.gofatal error: all goroutines are asleep - deadlock!
  • 老王學習go語言——3.Go語言基礎 -第一個go程序
    Go出生的年代網際網路已經有了長足的發展,所以Go從出生起就面對的是海量,高並發的網際網路大潮,所以從胎教開始,Go語言就練就了一身本領,到2009年開源,2012年正式穩定版本發布,Go語言就已經在處理多核並發以及開發效率,挖掘計算機CPU資源,網絡編程和並發編程等方面體現出了很強的實力。
  • 未來後端語言的趨勢——go語言免費學習網站大推薦!
    導語: Go語言是谷歌2009發布的第二款開源程式語言,以其可以媲美C或C++代碼的速度,而且更加安全、支持並行進程得到人們的喜愛。而它高並發的特性我相信它將在以後的後端語言中越來越流行!一.易百教程易百教程網的go語言教程是我首先要推薦的,為什麼呢?它相比於其它go語言教程網站的內容,除了一樣詳實的教程外,還增加了go編程代碼實例,最適合初次學習go語言的人邊看教程,變根據實例敲代碼。
  • 「Go 語言教程」 Go語言結構
    Go 語言教程學習一門程式語言,除了學習語法,詞法,以及寫法等和編譯器有關的特性(就是什麼是語法錯誤)之外,需要對這個語言的結構有個清晰的認識,其中包括代碼目錄結構,源碼文件,以及代碼結構組織等。1 目錄和源碼首先我門看目錄和源碼,從之前的Go 語言教程我們知道,Go語言有工程目錄,和GOPATH環境變量對應,工程目錄結構有bin 存放編譯後的可執行文件src 存放實現源碼,go get工具獲取的web上的模塊包都會放到這個目錄下,並有對應的目錄結構pkg 存放編譯後的庫文件(分不同平臺)Go語言的源碼文件格式為.go格式。
  • Go語言憑什麼進到2017年程式語言排行榜的前十
    4、自由高效:組合的思想、無侵入式的接口Go語言可以說是開發效率和運行效率二者的完美融合,天生的並發編程支持。Go語言支持當前所有的編程範式,包括過程式編程、面向對象編程以及函數式編程。5、強大的標準庫這包括網際網路應用、系統編程和網絡編程。Go裡面的標準庫基本上已經是非常穩定了,特別是我這裡提到的三個,網絡層、系統層的庫非常實用。
  • 「Go 語言教程」Go語言函數說明
    go語言教程「工欲上其事,必先利器!」一個好的設計往往是效率的體現,所以設計乃高效之本。編程中很多時候函數作為模塊設計的最小單元。函數設計也有很多方法和規定,以及設計原則。那麼go語言的函數都是怎麼樣子的,都有些什麼原則和要求呢,那麼怎麼做好函數方法設計呢,就讓我們一起來學習學習。1 入口函數作為編譯型的程式語言,go語言和其他流行的語言(C/C++, JAVA等)都有程序入口,就是所謂的入口函數main。
  • Go語言編程,sqlite的用法
    概要:本文學習如何在go軟體中讀寫sqlite3。為何選擇sqlite?sqlite3 基本語法和mysql相近。更少也意味著更簡單的功能。僅支持int,real,text,blob 4個基本類型。模糊查找也僅僅支持like。因為我準備在記事本這個小工具中使用,故mysql 顯得太大。
  • Go語言環境安裝
    程式語言,以簡潔、快速、安全著稱。技多不壓身,在學習過程中記錄下來,以備後續參考,希望對有同樣需求的小夥伴有所幫助。選擇一個print.go文件,通過window命令行工具運行go run命令,看看輸出效果。
  • 程式語言那麼多,很多人選擇go語言,那麼Go有什麼優點?
    他們對工具集的不滿迫使他們重新考慮系統編程,創建了一個精益,精簡和編譯的解決方案,該解決方案允許在壓力下進行大規模多線程,並發和性能處理。在此代碼中,main函數觸發了goroutine(由go關鍵字表示)調用hello()。該程序需要sleep一下,然後繼續。如果`hello()`函數發生任何事情,`main`函數將不會引起注意。重要的是工作已經完成。
  • Go程式語言也可以用來編寫Web應用?
    Go程式語言也可以用來編寫Web應用? Go語言的語法接近C語言,但是對於變量的聲明是不同的,其他語法不同之處是for循環和if判斷式沒有括號圍繞。Go語言支持垃圾回收功能。
  • Go語言的魅力
    給周末無事,想充電的朋友分享一下我最近迷戀上了Go語言的理由[偷笑],起初讓我了解它,是它那幾位殿堂級的創始人,後來去年B站原始碼洩露,幾乎整站全是go語言架構,讓我映像深刻,而近年國內很多巨頭阿里,頭條,小米,360,美團,螞蟻,騰訊等的開源項目可見,go就是一門未來具有絕對魅力的語言,我很多朋友公司裡,無歷史包袱的新項目都在優先考慮golang架構。
  • 為什麼很多公司轉型 Go 語言開發?Go 語言能做什麼
    而GO語言不一樣,通過協程可以方便的實現API的並行處理,達到處理效率的最大化。依賴Golang的高性能HTTP Server,提升系統吞吐能力,由PHP的數百級別提升到數千裡甚至過萬級別。開發效率GO語言使用起來簡單、代碼描述效率高、編碼規範統一、上手快。
  • web開發我更喜歡使用GO語言
    go語言在2007年9月設計,然後於2009年11月正式向外宣布推出使用,而且是開放原始碼項目,首先在Linux系統與Mac OS X平臺實現運行,不久在Windows系統實現。go語言可能是Google開發的程式語言,迅速受到開發的關注並願意使用它,在2016年被TIOBE 選為「TIOBE 年最佳程式語言」,可想而知go能被開發者認可一定有它獨特的優勢,而我更喜歡使用GO語言web開發。
  • JAVA與GO語言哪個更容易學?
    一,GO語言的優劣勢Go開發中的痛點編譯慢,失控的依賴,個工程師只是用了一個語言裡面的一部分,程序難以維護(可讀性差、文檔不清晰等),更新的花費越來越長,交叉編譯困難Go語言的優勢學習曲線容易MGo語言語法簡單,包含了類C語法。
  • GO語言入門
    GO 命令源碼文件1)命令源碼文件定義:命令源碼文件是程序的運行入口,如果一個源碼文件聲明屬於main包,並且包含一個無參數聲明的main函數,那麼它就是命令源碼文件2)命令源碼文件接收參數的包:GO語言標準庫中有一個代碼包flag專門用於接收和解析程序參數A. flag.StringVar()flag.StringVar
  • 想學一門新的程式語言?考慮一下Go (Golang)吧
    但萬事開頭難,學習新東西亦如此。如果開發員想學一門新的程式語言,該選擇什麼呢?Go語言學起來簡單得令人驚訝當我第一次開始學習Go語言時,我正著手開發一個個人項目,為此我不得不掌握新的語法(我總是在學習一門新的程式語言時想出一個項目)。我決定創建一個命令行應用程式來枚舉子域,以輔助尋找資產中存在的漏洞獎金計劃。
  • go程式語言能做什麼
    鑑於Go語言的特點和設計的初衷,Go語言作為伺服器程式語言,很適合處理日誌、數據打包、虛擬機處理、文件系統、分布式系統、資料庫代理等;網絡編程方面,Go語言廣泛應用於Web 應用、API應用、下載應用等;除此之外,Go語言還適用於內存資料庫和雲平臺領域,目前國外很多雲平臺都是採用Go開發。
  • 老王學習go語言——3.go語言基礎語法和關鍵字 gdb調試
    慣例書名放在最上面《go語言高並發與微服務實戰》開始基礎語法和關鍵字之前,上一篇遺留了幾個問題:go語言debug之Gdb Version,之前沒用過C++和C,所以對於Gdb不是很熟悉,也不想特別麻煩去搞,於是想偷個懶,利用網上的方法,去下載liteidex,準備利用裡面的gdb64.exe,但下載了最新版,裡面沒有gdb的可執行文件。