Go 語言實踐(一)

本文由Austin發表git

指導原則

咱們要談論在一個編程語言中的最佳實踐,那麼咱們首先應該明確什麼是「最佳」。若是您們聽了我昨天那場講演的話,您必定看到了來自 Go 團隊的 Russ Cox 講的一句話:程序員

軟件工程,是您在編程過程當中增長了工期或者開發人員以後發生的那些事。 — Russ Coxsql

Russ 是在闡述軟件「編程」和軟件「工程」之間的區別,前者是您寫的程序,然後者是一個讓更多的人長期使用的產品。軟件工程師會來來去去地更換,團隊也會成長或者萎縮,需求也會發生變化,新的特性也會增長,bug 也會被修復,這就是軟件「工程」的本質。數據庫

我多是現場最先的 Go 語言用戶,但與其說個人主張來自個人資歷,不如說我今天講的是真實來自於 Go 語言自己的指導原則,那就是:編程

  1. 簡單性
  2. 可讀性
  3. 生產率

您可能已經注意到,我並無提性能或者併發性。實際上有很多的語言執行效率比 Go 還要高,但它們必定沒有 Go 這麼簡單。有些語言也以併發性爲最高目標,但它們的可讀性和生產率都很差。 性能併發性都很重要,但它們不如簡單性可讀性生產率那麼重要。json

簡單性

爲何咱們要力求簡單,爲何簡單對 Go 語言編程如此重要?數組

咱們有太多的時候感嘆「這段代碼我看不懂」,是吧?咱們懼怕修改一丁點代碼,生怕這一點修改就致使其餘您不懂的部分出問題,而您又沒辦法修復它。併發

這就是複雜性。複雜性把可讀的程序變得不可讀,複雜性終結了不少軟件項目。app

簡單性是 Go 的最高目標。不管咱們寫什麼程序,咱們都應該能一致認爲它應當簡單。dom

可讀性

Readability is essential for maintainability. — Mark Reinhold, JVM language summit 2018 可讀性對於可維護性相當重要。

爲何 Go 代碼的可讀性如此重要?爲何咱們應該力求可讀性?

Programs must be written for people to read, and only incidentally for machines to execute. — Hal Abelson and Gerald Sussman, Structure and Interpretation of Computer Programs 程序應該是寫來被人閱讀的,而只是順帶能夠被機器執行。

可閱讀性對全部的程序——不只僅是 Go 程序,都是如此之重要,是由於程序是人寫的而且給其餘人閱讀的,事實上被機器所執行只是其次。

代碼被閱讀的次數,遠遠大於被編寫的次數。一段小的代碼,在它的整個生命週期,可能被閱讀成百上千次。

The most important skill for a programmer is the ability to effectively communicate ideas. — Gastón Jorquera ^1 程序員最重要的技能是有效溝通想法的能力。

可讀性是弄清楚一個程序是在作什麼事的關鍵。若是您都不知道這個程序在作什麼,您如何去維護這個程序?若是一個軟件不可用被維護,那就可能被重寫,而且這也多是您公司最後一次在 GO 上面投入了。

若是您僅僅是爲本身我的寫一個程序,可能這個程序是一次性的,或者使用這個程序的人也只有您一個,那您想怎樣寫就怎樣寫。但若是是多人合做貢獻的程序,或者由於它解決人們的需求、知足某些特性、運行它的環境會變化,而在一個很長的時間內被不少人使用,那麼程序的可維護性則必須成爲目標。

編寫可維護的程序的第一步,那就是確保代碼是可讀的。

生產率

Design is the art of arranging code to work today, and be changeable forever. — Sandi Metz 設計是一門藝術,要求編寫的代碼當前可用,而且之後仍能被改動。

我想重點闡述的最後一個基本原則是生產率。開發者的生產率是一個複雜的話題,但歸結起來就是:爲了有效的工做,您由於一些工具、外部代碼庫而浪費了多少時間。Go 程序員應該感覺獲得,他們在工做中能夠從不少東西中受益了。(Austin Luo:言下之意是,Go 的工具集和基礎庫完備,不少東西觸手可得。)

有一個笑話是說,Go 是在 C++ 程序編譯過程當中被設計出來的。快速的編譯是 Go 語言用以吸引新開發者的關鍵特性。編譯速度仍然是一個不變的戰場,很公平地說,其餘語言須要幾分鐘才能編譯,而 Go 只須要幾秒便可完成。這有助於 Go 開發者擁有動態語言開發者同樣的高效,但卻不會面臨那些動態語言自己可靠性的問題。

Go 開發者意識到代碼是寫來被閱讀的,而且把閱讀放在編寫之上。Go 致力於從工具集、習慣等方面強制要求代碼必須編寫爲一種特定樣式,這消除了學習項目特定術語的障礙,同時也能夠僅僅從「看起來」不正確便可幫助開發者發現潛在的錯誤。

Go 開發者不會整日去調試那些莫名其妙的編譯錯誤。他們也不會整日浪費時間在複雜的構建腳本或將代碼部署到生產中這事上。更重要的是他們不會花時間在嘗試搞懂同事們寫的代碼是什麼意思這事上。

當 Go 語言團隊在談論一個語言必須擴展時,他們談論的就是生產率。

標識符

咱們要討論的第一個議題是標識符。標識符是一個名稱的描述詞,這個名稱能夠是一個變量的名稱、一個函數的名稱、一個方法的名稱、一個類型的名稱或者一個包的名稱等等。

Poor naming is symptomatic of poor design. — Dave Cheney 拙劣的名稱是拙劣的設計的表徵。

鑑於 Go 的語法限制,咱們爲程序中的事物選擇的名稱對咱們程序的可讀性產生了過大的影響。良好的可讀性是評判代碼質量的關鍵,所以選擇好名稱對於 Go 代碼的可讀性相當重要。

選擇清晰的名稱,而不是簡潔的名稱

Obvious code is important. What you can do in one line you should do in three. — Ukiah Smith 代碼要明確這很重要,您在一行中能作的事,應該拆到三行裏作。

Go 不是專一於將代碼精巧優化爲一行的那種語言,Go 也不是致力於將代碼精煉到最小行數的語言。咱們並不追求源碼在磁盤上佔用的空間更少,也不關心錄入代碼須要多長時間。

Good naming is like a good joke. If you have to explain it, it’s not funny. — Dave Cheney 好的名稱就如同一個好的笑話,若是您須要去解釋它,那它就不搞笑了。

這個清晰度的關鍵就是咱們爲 Go 程序選擇的標識符。讓咱們來看看一個好的名稱應當具有什麼吧:

  • **好的名稱是簡潔的。**一個好的名稱未必是儘量短的,但它確定不會浪費任何無關的東西在上面,好名字具備高信噪比。
  • **好的名稱是描述性的。**一個好的名稱應該描述一個變量或常量的使用,而非其內容。一個好的命名應該描述函數的結果或一個方法的行爲,而不是這個函數或方法自己的操做。一個好的名稱應該描述一個包的目的,而不是包的內容。名稱描述的東西越準確,名稱越好。
  • 好的名稱是可預測的。您應該可以從名稱中推斷出它的使用方式,這是選擇描述性名稱帶來的做用,同時也遵循了傳統。Go 開發者在談論慣用語時,便是說的這個。

接下來讓咱們深刻地討論一下。

標識符長度

有時候人們批評 Go 風格推薦短變量名。正如 Rob Pike 所說,「Go 開發者想要的是合適長度的標識符」。^1

Andrew Gerrand 建議經過使用更長的標識符向讀者暗示它們具備更高的重要性。

The greater the distance between a name’s declaration and its uses, the longer the name should be. — Andrew Gerrand ^2 標識符的聲明和使用間隔越遠,名稱的長度就應當越長。

據此,咱們能夠概括一些指導意見:

  • 短變量名稱在聲明和上次使用之間的距離很短時效果很好。
  • 長變量名須要證實其不一樣的合理性:越長的變量名,越須要更多的理由來證實其合理。冗長、繁瑣的名稱與他們在頁面上的權重相比,攜帶的信息很低。
  • 不要在變量名中包含其類型的名稱。
  • 常量須要描述其存儲的值的含義,而不是怎麼使用它。
  • 單字母變量可用於循環或邏輯分支,單詞變量可用於參數或返回值,多詞短語可用於函數和包這一級的聲明。
  • 單詞可用於方法、接口和包
  • 請記住,包的命名將成爲用戶引用它時採用的名稱,確保這個名稱更有意義。

讓咱們來看一個示例:

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
}
複製代碼

在這個示例中,範圍變量p在定義以後只在接下來的一行使用。p在整頁源碼和函數執行過程當中都只生存一小段時間。對p感興趣的讀者只須要查看兩行代碼便可。

與之造成對比的是,變量people在函數參數中定義,而且存在了 7 行,同理的還有sumcount,這他們使用了更長的名稱,讀者必須關注更普遍的代碼行。

我也可使用s而不是sum,用c(或n)而不是count,但這會將整個程序中的變量都彙集在相同的重要性上。我也可使用p而不是people,可是這樣又有一個問題,那就是for ... range循環中的變量又用什麼?單數的 person 看起來也很奇怪,生存時間極短命名卻比導出它的那個值更長。

Austin Luo:這裏說的是,若數組people用變量名p,那麼從數組中獲取的每個元素取名就成了問題,好比用person,即便使用person看起來也很奇怪,一方面是單數,一方面person的生存週期只有兩行(很短),命名比生存週期更長的ppeople)還長了。 小竅門:跟使用空行在文檔中分段同樣,使用空行將函數執行過程分段。在函數AverageAge中有按順序的三個操做。第一個是先決條件,檢查當people爲空時咱們不會除零,第二個是累加總和和計數,最後一個是計算平均數。

上下文是關鍵

絕大多數的命名建議都是根據上下文的,意識到這一點很重要。我喜歡稱之爲原則,而不是規則。

iindex 這兩個標識符有什麼不一樣?咱們很難確切地說其中一個比另外一個好,好比:

for index := 0; index < len(s); index++ {
  //
}
複製代碼

上述代碼的可讀性,基本上都會認爲比下面這段要強:

for i := 0; i < len(s); i++ {
  //
}
複製代碼

但我表示不贊同。由於不管是i仍是index,都是限定於for循環體的,更冗長的命名,並無讓咱們更容易地理解這段代碼。

話說回來,下面兩段代碼那一段可讀性更強呢?

func (s *SNMP) Fetch(oid []int, index int) (int, error)
複製代碼

或者

func (s *SNMP) Fetch(o []int, i int) (int, error)
複製代碼

在這個示例中,oidSNMP對象 ID 的縮寫,所以將其略寫爲 o 意味着開發者必須將他們在文檔中看到的常規符號轉換理解爲代碼中更短的符號。一樣地,將index簡略爲i,減小了其做爲SNMP消息的索引的含義。

小竅門:在參數聲明中不要混用長、短不一樣的命名風格。

命名中不要包含所屬類型的名稱

正如您給寵物取名同樣,您會給狗取名「汪汪」,給貓取名爲「咪咪」,但不會取名爲「汪汪狗」、「咪咪貓」。出於一樣的緣由,您也不該在變量名稱中包含其類型的名稱。

變量命名應該體現它的內容,而不是類型。咱們來看下面這個例子:

var usersMap map[string]*User
複製代碼

這樣的命名有什麼好處呢?咱們能知道它是個 map,而且它與*User類型有關,這可能還不錯。可是 Go 做爲一種靜態類型語言,它並不會容許咱們在須要標量變量的地方意外地使用到這個變量,所以Map後綴其實是多餘的。

如今咱們來看像下面這樣定義變量又是什麼狀況:

var (
  companiesMap map[string]*Company
  productsMap map[string]*Products
)
複製代碼

如今這個範圍內咱們有了三個 map 類型的變量了:usersMapcompaniesMap,以及 productsMap,全部這些都從字符串映射到了不一樣的類型。咱們知道它們都是 map,咱們也知道它們的 map 聲明會阻止咱們使用一個代替另外一個——若是咱們嘗試在須要map[string]*User的地方使用companiesMap,編譯器將拋出錯誤。在這種狀況下,很明顯Map後綴不會提升代碼的清晰度,它只是編程時須要鍵入的冗餘內容。(Austin Luo:陳舊的思惟方式)

個人建議是,避免給變量加上與類型相關的任何後綴。

小竅門:若是users不能描述得足夠清楚,那usersMap也必定不能。

這個建議也適用於函數參數,好比:

type Config struct {
  //
}

func WriteConfig(w io.Writer, config *Config)
複製代碼

*Config參數命名爲config是多餘的,咱們知道它是個*Config,函數簽名上寫得很清楚。

在這種狀況建議考慮conf或者c——若是生命週期足夠短的話。

若是在一個範圍內有超過一個*Config,那命名爲conf1conf2的描述性就比originalupdated更差,並且後者比前者更不容易出錯。

NOTE:不要讓包名佔用了更適合變量的名稱。 導入的標識符是會包含它所屬包的名稱的。 例如咱們很清楚context.Context是包context中的類型Context。這就致使咱們在咱們本身的包裏,再也沒法使用context做爲變量或類型名了。 func WriteLog(context context.Context, message string) 這沒法編譯。這也是爲何咱們一般將context.Context類型的變量命名爲ctx的緣由,如: func WriteLog(ctx context.Context, message string)

使用一致的命名風格

一個好名字的另外一個特色是它應該是可預測的。閱讀者應該能夠在第一次看到的時候就可以理解它如何使用。若是遇到一個約定俗稱的名字,他們應該可以認爲和上次看到這個名字同樣,一直以來它都沒有改變意義。

例如,若是您要傳遞一個數據庫句柄,請確保每次的參數命名都是同樣的。與其使用d *sql.DBdbase *sql.DBDB *sql.DBdatabase *sql.DB,還不如都統一爲:

db *sql.DB
複製代碼

這樣作能夠增進熟悉度:若是您看到db,那麼您就知道那是個*sql.DB,而且已經在本地定義或者由調用者提供了。

對於方法接收者也相似,在類型的每一個方法中使用相同的接收者名稱,這樣可讓閱讀者在跨方法閱讀和理解時更容易主觀推斷。

Austin Luo:「接收者」是一種特殊類型的參數。^2 好比func (b *Buffer) Read(p []byte) (n int, err error),它一般只用一到兩個字母來表示,但在不一樣的方法中仍然應當保持一致。 注意:Go 中對接收者的短命名規則慣例與目前提供的建議不一致。這只是早期作出的選擇之一,而且已經成爲首選的風格,就像使用CamelCase而不是snake_case同樣。 小竅門:Go 的命名風格規定接收器具備單個字母名稱或其派生類型的首字母縮略詞。有時您可能會發現接收器的名稱有時會與方法中參數的名稱衝突,在這種狀況下,請考慮使參數名稱稍長,而且仍然不要忘記一致地使用這個新名稱。

最後,某些單字母變量傳統上與循環和計數有關。例如,ij,和k一般是簡單的for循環變量。n一般與計數器或累加器有關。 v一般是某個值的簡寫,k一般用於映射的鍵,s一般用做string類型參數的簡寫。

與上面db的例子同樣,程序員指望i是循環變量。若是您保證i始終是一個循環變量——而不是在for循環以外的狀況下使用,那麼當讀者遇到一個名爲i或者j的變量時,他們就知道當前還在循環中。

小竅門:若是您發如今嵌套循環中您都使用完ijk了,那麼很顯然這已經到了將函數拆得更小的時候了。

使用一致的聲明風格

Go 中至少有 6 種聲明變量的方法(Austin Luo:做者說了 6 種,但只列了 5 種)

  • var x int = 1
  • var x = 1
  • var x int; x = 1
  • var x = int(1)
  • x := 1

我敢確定還有更多我沒想到的。這是 Go 的設計師認識到多是一個錯誤的地方,但如今改變它爲時已晚。有這麼多不一樣的方式來聲明變量,那麼咱們如何避免每一個 Go 程序員選擇本身個性獨特的聲明風格呢?

我想展現一些在我本身的程序裏聲明變量的建議。這是我儘量使用的風格。

  • 只聲明,不初始化時,使用*var***。**在聲明以後,將會顯式地初始化時,使用var關鍵字。
複製代碼

var players int // 0

var things []Thing // an empty slice of Things

var thing Thing // empty Thing struct

json.Unmarshall(reader, &thing)

複製代碼

var關鍵字代表這個變量被有意地聲明爲該類型的零值。這也與在包級別聲明變量時使用var而不是短聲明語法(Austin Luo::=)的要求一致——儘管我稍後會說您根本不該該使用包級變量。

  • 既聲明,也初始化時,使用*:=***。**當同時要聲明和初始化變量時,換言之咱們不讓變量隱式地被初始化爲零值時,我建議使用短聲明語法的形式。這使得讀者清楚地知道:=左側的變量是有意被初始化的。

爲解釋緣由,咱們回頭再看看上面的例子,但這一次每一個變量都被有意初始化了:

var players int = 0

var things []Thing = nil

var thing *Thing = new(Thing)
json.Unmarshall(reader, thing)
複製代碼

第一個和第三個示例中,由於 Go 沒有從一種類型到另外一種類型的自動轉換,賦值運算符左側和右側的類型一定是一致的。編譯器能夠從右側的類型推斷出左側所聲明變量的類型。對於這個示例能夠更簡潔地寫成這樣:

var players = 0

var things []Thing = nil

var thing = new(Thing)
json.Unmarshall(reader, thing)
複製代碼

因爲0players的零值,所以爲players顯式地初始化爲0就顯得多餘了。因此爲了更清晰地代表咱們使用了零值,應該寫成這樣:

var players int
複製代碼

那第二條語句呢?咱們不能忽視類型寫成:

var things = nil
複製代碼

由於nil根本就沒有類型^2。相反,咱們有一個選擇,咱們是否但願切片的零值?

var things []Thing
複製代碼

或者咱們是否但願建立一個沒有元素的切片?

var things = make([]Thing, 0)
複製代碼

若是咱們想要的是後者,這不是個切片類型的零值,那麼咱們應該使用短聲明語法讓閱讀者很清楚地明白咱們的選擇:

things := make([]Thing, 0)
複製代碼

這告訴了讀者咱們顯式地初始化了things

再來看看第三個聲明:

var thing = new(Thing)
複製代碼

這既顯式地初始化了變量,也引入了 Go 程序員不喜歡並且很不經常使用的new關鍵字。若是咱們遵循短命名語法的建議,那麼這句將變成:

thing := new(Thing)
複製代碼

這很清楚地代表,thing被顯式地初始化爲new(Thing)的結果——一個指向Thing的指針——但仍然保留了咱們不經常使用的new。咱們能夠經過使用緊湊結構初始化的形式來解決這個問題,

thing := &Thing{}
複製代碼

這和new(Thing)作了一樣的事——也所以不少 Go 程序員對這種重複感受不安。不過,這一句仍然意味着咱們爲thing明確地初始化了一個Thing{}的指針——一個Thing的零值。

在這裏,咱們應該意識到,thing被初始化爲了零值,而且將它的指針地址傳遞給了json.Unmarshall

var thing Thing
json.Unmarshall(reader, &thing)
複製代碼

注意:固然,對於任何經驗法則都有例外。好比,有些變量之間很相關,那麼與其寫成這樣: var min int max := 1000 不如寫成這樣更具可讀性: min, max := 0, 1000

綜上所述:

  • 只聲明,不初始化時,使用var
  • 既聲明,也顯式地初始化時,使用:=

小竅門:使得機巧的聲明更加顯而易見。 當某件事自己很複雜時,應當使它看起來就複雜。 var length uint32 = 0x80 這裏的length可能和一個須要有特定數字類型的庫一塊兒使用,而且length被很明確地指定爲uint32類型而不僅是短聲明形式: length := uint32(0x80) 在第一個例子中,我故意違反了使用var聲明形式和顯式初始化程序的規則。這個和我慣常形式不一樣的決定,可讓讀者意識到這裏須要注意。

成爲團隊合做者

我談到了軟件工程的目標,即生成可讀,可維護的代碼。而您的大部分職業生涯參與的項目可能您都不是惟一的做者。在這種狀況下個人建議是遵照團隊的風格。

在文件中間改變編碼風格是不適合的。一樣,即便您不喜歡,可維護性也比您的我的喜愛有價值得多。個人原則是:若是知足gofmt,那麼一般就不值得再進行代碼風格審查了。

小竅門:若是您要橫跨整個代碼庫進行重命名,那麼不要在其中混入其餘的修改。若是其餘人正在使用 git bisect,他們必定不肯意從幾千行代碼的重命名中「跋山涉水」地去尋找您別的修改。

代碼註釋

在咱們進行下一個更大的主題以前,我想先花幾分鐘說說註釋的事。

Good code has lots of comments, bad code requires lots of comments. — Dave Thomas and Andrew Hunt, The Pragmatic Programmer 好的代碼中附帶有大量的註釋,壞的代碼缺乏大量的註釋。

代碼註釋對 Go 程序的可讀性極爲重要。一個註釋應該作到以下三個方面的至少一個:

  1. 註釋應該解釋「作什麼」。
  2. 註釋應該解釋「怎麼作的」。
  3. 註釋應該解釋「爲何這麼作」。

第一種形式適合公開的符號:

// Open opens the named file for reading.
// If successful, methods on the returned file can be used for reading.
複製代碼

第二種形式適合方法內的註釋:

// queue all dependant actions
var results []chan error
for _, dep := range a.Deps {
        results = append(results, execute(seen, dep))
}
複製代碼

第三種形式,「爲何這麼作」,這是獨一無二的,沒法被前兩種取代,也沒法取代前兩種。第三種形式的註釋用於解釋更多的情況,而這些情況每每難以脫離上下文,不然將沒有意義,這些註釋就是用來闡述上下文的。

return &v2.Cluster_CommonLbConfig{
  // Disable HealthyPanicThreshold
  HealthyPanicThreshold: &envoy_type.Percent{
    Value: 0,
  },
}
複製代碼

在這個示例中,很難當即弄清楚把HealthyPanicThreshold的百分比設置爲零會產生什麼影響。註釋就用來明確將值設置爲0其實是禁用了panic閾值的這種行爲。

變量和常量上的註釋應當描述它的內容,而非目的

我以前談過,變量或常量的名稱應描述其目的。向變量或常量添加註釋時,應該描述變量的內容,而不是定義它的目的

const randomNumber = 6 // determined from an unbiased die
複製代碼

這個示例的註釋描述了「爲何」randomNumber被賦值爲 6,也說明了 6 這個值是從何而來的。但它沒有描述randomNumber會被用到什麼地方。下面是更多的例子:

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
複製代碼

如在 RFC 7231 的第 6.2.1 節中定義的那樣,在 HTTP 語境中 100 被當作StatusContinue

小竅門:對於那些沒有初始值的變量,註釋應當描述誰將負責初始化它們 // sizeCalculationDisabled indicates whether it is safe // to calculate Types' widths and alignments. See dowidth. var sizeCalculationDisabled bool 這裏,經過註釋讓讀者清楚函數dowidth在負責維護sizeCalculationDisabled的狀態。 小竅門:隱藏一目瞭然的東西 Kate Gregory 提到一點^3,有時一個好的命名,能夠省略沒必要要的註釋。 // registry of SQL drivers var registry = make(mapstringsql.Driver) 註釋是源碼做者加的,由於registry沒能解釋清楚定義它的目的——它是個註冊表,可是什麼的註冊表? 經過重命名變量名爲sqlDrivers,如今咱們很清楚這個變量的目的是存儲 SQL 驅動。 var sqlDrivers = make(mapstringsql.Driver) 如今註釋已經多餘了,能夠移除。

老是爲公開符號寫文檔說明

由於 godoc 將做爲您的包的文檔,您應該老是爲每一個公開的符號寫好註釋說明——包括變量、常量、函數和方法——全部定義在您包內的公開符號。

這裏是 Go 風格指南的兩條規則:

  • 任何既不明顯也不簡短的公共功能必須加以註釋。
  • 不管長度或複雜程度如何,都必須對庫中的任何函數進行註釋。
package ioutil

// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r io.Reader) ([]byte, error)
複製代碼

對這個規則有一個例外:您不須要爲實現接口的方法進行文檔說明,特別是不要這樣:

// Read implements the io.Reader interface
func (r *FileReader) Read(buf []byte) (int, error)
複製代碼

這個註釋等於說明都沒說,它沒有告訴您這個方法作了什麼,實際上更糟的是,它讓您去找別的地方的文檔。在這種狀況我建議將註釋整個去掉。

這裏有一個來自io這個包的示例:

// 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
}
複製代碼

請注意,LimitedReader的聲明緊接在使用它的函數以後,而且LimitedReader.Read又緊接着定義在LimitedReader以後,即使LimitedReader.Read自己沒有文檔註釋,那和很清楚它是io.Reader的一種實現。

小竅門:在您編寫函數以前先寫描述這個函數的註釋,若是您發現註釋很難寫,那就代表您正準備寫的這段代碼必定難以理解。

不要爲壞的代碼寫註釋,重寫它

Don’t comment bad code — rewrite it — Brian Kernighan 不要爲壞的代碼寫註釋——重寫它

爲粗製濫造的代碼片斷着重寫註釋是不夠的,若是您遭遇到一段這樣的註釋,您應該發起一個問題(issue)從而記得後續重構它。技術債務只要不是過多就沒有關係。

在標準庫的慣例是,批註一個 TODO 風格的註釋,說明是誰發現了壞代碼。

// TODO(dfc) this is O(N^2), find a faster way to do this.
複製代碼

註釋中的姓名並不意味着承諾去修復問題,但在解決問題時,他多是最合適的人選。其餘批註內容通常還有日期或者問題編號。

與其爲一大段代碼寫註釋,不如重構它

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. — Steve McConnell 好的代碼即爲最好的文檔。在您準備添加一行註釋時,問本身,「我要如何改進這段代碼從而使它不須要註釋?」優化代碼,而後註釋它使之更清晰。

函數應該只作一件事。若是您發現一段代碼由於與函數的其餘部分不相關於是須要註釋時,考慮將這段代碼拆分爲獨立的函數。

除了更容易理解以外,較小的函數更容易單獨測試,如今您將不相關的代碼隔離拆分到不一樣的函數中,估計只有函數名纔是惟一須要的文檔註釋了。

此文已由做者受權騰訊雲+社區發佈,更多原文請點擊

搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!

相關文章
相關標籤/搜索