├── .markdownlint.yaml ├── LICENSE ├── README.md ├── best-practices.md ├── decisions.md ├── guide.md └── index.md /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | { "MD033": false, "MD013": false, "MD030": false } 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Bo-Yi Wu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go 語言教學文件 2 | 3 | ## Go 風格指南 4 | 5 | (英文版) 6 | 7 | [概覽](index.md) | [指南](guide.md) | [決策](decisions.md) | 8 | [最佳實踐](best-practices.md) 9 | 10 | 這份文件旨在提供 Go 語言的教學指南,涵蓋風格指南、概覽、指南、決策和最佳實踐。 11 | -------------------------------------------------------------------------------- /best-practices.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Go 語言風格最佳實踐 4 | 5 | (英文版) 6 | 7 | [概覽](index.md) | [指南](guide.md) | [決策](decisions.md) | 8 | [最佳實踐](best-practices.md) 9 | 10 | **注意:** 這是一系列文件的一部分,概述了 Google 的 [Go 風格](index.md)。本文件**既不是 [規範性的](index.md#normative) 也不是 [權威性的](index.md#canonical)**,它是 [核心風格指南](guide.md) 的輔助文件。更多資訊請參見 [概述](index.md#about)。 11 | 12 | 13 | 14 | ## 關於 15 | 16 | 本文件記錄了**如何最佳應用 Go 風格指南的指導**。這些建議旨在針對經常出現的常見情況,但可能不適用於每一種情況。在可能的情況下,討論了多種替代方法以及決定何時應用或不應用它們的考量。 17 | 18 | 查看 [概述](index.md#about) 以獲得完整的風格指南文件集。 19 | 20 | 21 | 22 | ## 命名 23 | 24 | 25 | 26 | ### 函數和方法名稱 27 | 28 | 29 | 30 | #### 避免重複 31 | 32 | 在為函數或方法選擇名稱時,請考慮名稱將在何種上下文中被讀取。考慮以下建議,以避免在調用時過度[重複](decisions.md#repetition): 33 | 34 | - 以下通常可以從函數和方法名稱中省略: 35 | 36 | - 輸入和輸出的類型(當沒有衝突時) 37 | - 方法接收者的類型 38 | - 輸入或輸出是否為指針 39 | 40 | - 對於函數,不要[重複包的名稱](decisions.md#repetitive-with-package)。 41 | 42 | ```go 43 | // 不佳: 44 | package yamlconfig 45 | 46 | func ParseYAMLConfig(input string) (*Config, error) 47 | ``` 48 | 49 | ```go 50 | // 較佳: 51 | package yamlconfig 52 | 53 | func Parse(input string) (*Config, error) 54 | ``` 55 | 56 | - 對於方法,不要重複方法接收者的名稱。 57 | 58 | ```go 59 | // 不佳: 60 | func (c *Config) WriteConfigTo(w io.Writer) (int64, error) 61 | ``` 62 | 63 | ```go 64 | // 較佳: 65 | func (c *Config) WriteTo(w io.Writer) (int64, error) 66 | ``` 67 | 68 | - 不要重複作為參數傳遞的變數名稱。 69 | 70 | ```go 71 | // 不佳: 72 | func OverrideFirstWithSecond(dest, source *Config) error 73 | ``` 74 | 75 | ```go 76 | // 較佳: 77 | func Override(dest, source *Config) error 78 | ``` 79 | 80 | - 不要重複返回值的名稱和類別。 81 | 82 | ```go 83 | // 不佳: 84 | func TransformYAMLToJSON(input *Config) *jsonconfig.Config 85 | ``` 86 | 87 | ```go 88 | // 較佳: 89 | func Transform(input *Config) *jsonconfig.Config 90 | ``` 91 | 92 | 當需要區分同名的函數時,可以包含額外的信息。 93 | 94 | ```go 95 | // 較佳: 96 | func (c *Config) WriteTextTo(w io.Writer) (int64, error) 97 | func (c *Config) WriteBinaryTo(w io.Writer) (int64, error) 98 | ``` 99 | 100 | 101 | 102 | #### 命名慣例 103 | 104 | 在為函數和方法選擇名稱時,有一些其他常見的命名慣例: 105 | 106 | - 返回某物的函數給予類似名詞的名稱: 107 | 108 | ```go 109 | // 較佳: 110 | func (c *Config) JobName(key string) (value string, ok bool) 111 | ``` 112 | 113 | 這的推論是函數和方法名稱應該[避免使用前綴 `Get`](decisions.md#getters)。 114 | 115 | ```go 116 | // 不佳: 117 | func (c *Config) GetJobName(key string) (value string, ok bool) 118 | ``` 119 | 120 | - 做某事的函數給予類似動詞的名稱: 121 | 122 | ```go 123 | // 較佳: 124 | func (c *Config) WriteDetail(w io.Writer) (int64, error) 125 | ``` 126 | 127 | - 僅由涉及的類型不同的相同函數,在名稱的末尾包含類型名稱: 128 | 129 | ```go 130 | // 較佳: 131 | func ParseInt(input string) (int, error) 132 | func ParseInt64(input string) (int64, error) 133 | func AppendInt(buf []byte, value int) []byte 134 | func AppendInt64(buf []byte, value int64) []byte 135 | ``` 136 | 137 | 如果有一個明確的“主要”版本,可以從該版本的名稱中省略類型: 138 | 139 | ```go 140 | // 較佳: 141 | func (c *Config) Marshal() ([]byte, error) 142 | func (c *Config) MarshalText() (string, error) 143 | ``` 144 | 145 | 146 | 147 | ### 測試雙套件和類型 148 | 149 | 在為提供測試輔助工具,特別是[測試雙元件]的包和類型命名時,您可以應用幾種規則。測試雙元件可以是存根(stub)、假物件(fake)、模擬物件(mock)或間諜(spy)。 150 | 151 | 這些例子大多使用存根。如果您的代碼使用假物件或其他類型的測試雙元件,請相應更新您的名稱。 152 | 153 | [測試雙元件]: https://abseil.io/resources/swe-book/html/ch13.html#basic_concepts 154 | 155 | 假設您有一個專注的包,提供類似於以下的生產代碼: 156 | 157 | ```go 158 | package creditcard 159 | 160 | import ( 161 | "errors" 162 | 163 | "path/to/money" 164 | ) 165 | 166 | // ErrDeclined indicates that the issuer declines the charge. 167 | var ErrDeclined = errors.New("creditcard: declined") 168 | 169 | // Card contains information about a credit card, such as its issuer, 170 | // expiration, and limit. 171 | type Card struct { 172 | // omitted 173 | } 174 | 175 | // Service allows you to perform operations with credit cards against external 176 | // payment processor vendors like charge, authorize, reimburse, and subscribe. 177 | type Service struct { 178 | // omitted 179 | } 180 | 181 | func (s *Service) Charge(c *Card, amount money.Money) error { /* omitted */ } 182 | ``` 183 | 184 | 185 | 186 | #### 創建測試輔助套件 187 | 188 | 假設您想創建一個包含另一個包的測試雙元件的包。我們將使用上面的 `package creditcard` 作為這個例子: 189 | 190 | 一種方法是基於生產包為測試引入一個新的 Go 包。一個安全的選擇是將單詞 `test` 附加到原始包名("creditcard" + "test"): 191 | 192 | ```go 193 | // 較佳: 194 | package creditcardtest 195 | ``` 196 | 197 | 除非另有明確說明,下面各節的所有示例都在 `package creditcardtest` 中。 198 | 199 | 200 | 201 | #### 簡單情況 202 | 203 | 您想為 `Service` 添加一組測試雙元件。因為 `Card` 實際上是一種簡單的數據類型,類似於協議緩衝消息,所以在測試中不需要特殊處理,因此不需要雙元件。如果您預計只有一種類型(如 `Service`)的測試雙元件,您可以採取簡潔的命名方法: 204 | 205 | ```go 206 | // 較佳: 207 | import ( 208 | "path/to/creditcard" 209 | "path/to/money" 210 | ) 211 | 212 | // Stub stubs creditcard.Service and provides no behavior of its own. 213 | type Stub struct{} 214 | 215 | func (Stub) Charge(*creditcard.Card, money.Money) error { return nil } 216 | ``` 217 | 218 | 這絕對比像 `StubService` 或非常糟糕的 `StubCreditCardService` 這樣的命名選擇更好,因為基礎包名和其領域類型暗示了 `creditcardtest.Stub` 是什麼。 219 | 220 | 最後,如果包是用 Bazel 構建的,請確保新的 `go_library` 規則標記為 `testonly`: 221 | 222 | ```build 223 | # 較佳: 224 | go_library( 225 | name = "creditcardtest", 226 | srcs = ["creditcardtest.go"], 227 | deps = [ 228 | ":creditcard", 229 | ":money", 230 | ], 231 | testonly = True, 232 | ) 233 | ``` 234 | 235 | 上述方法是常規的,其他工程師將會相當理解。 236 | 237 | 另見: 238 | 239 | - [Go 提示 #42:編寫用於測試的存根](https://google.github.io/styleguide/go/index.html#gotip) 240 | 241 | #### 多種測試雙元件行為 242 | 243 | 當一種存根不夠時(例如,您還需要一個總是失敗的),我們建議根據它們模擬的行為命名存根。在這裡,我們將 `Stub` 重命名為 `AlwaysCharges`,並引入一個名為 `AlwaysDeclines` 的新存根: 244 | 245 | ```go 246 | // 較佳: 247 | // AlwaysCharges stubs creditcard.Service and simulates success. 248 | type AlwaysCharges struct{} 249 | 250 | func (AlwaysCharges) Charge(*creditcard.Card, money.Money) error { return nil } 251 | 252 | // AlwaysDeclines stubs creditcard.Service and simulates declined charges. 253 | type AlwaysDeclines struct{} 254 | 255 | func (AlwaysDeclines) Charge(*creditcard.Card, money.Money) error { 256 | return creditcard.ErrDeclined 257 | } 258 | ``` 259 | 260 | 261 | 262 | #### 多種類型的多個雙元件 263 | 264 | But now suppose that `package creditcard` contains multiple types worth creating 265 | doubles for, as seen below with `Service` and `StoredValue`: 266 | 267 | 但現在假設 `package creditcard` 包含多個值得創建雙元件的類型,如下所示的 `Service` 和 `StoredValue`: 268 | 269 | ```go 270 | package creditcard 271 | 272 | type Service struct { 273 | // omitted 274 | } 275 | 276 | type Card struct { 277 | // omitted 278 | } 279 | 280 | // StoredValue manages customer credit balances. This applies when returned 281 | // merchandise is credited to a customer's local account instead of processed 282 | // by the credit issuer. For this reason, it is implemented as a separate 283 | // service. 284 | type StoredValue struct { 285 | // omitted 286 | } 287 | 288 | func (s *StoredValue) Credit(c *Card, amount money.Money) error { /* omitted */ } 289 | ``` 290 | 291 | 在這種情況下,更明確的測試雙元件命名是合理的: 292 | 293 | ```go 294 | // 較佳: 295 | type StubService struct{} 296 | 297 | func (StubService) Charge(*creditcard.Card, money.Money) error { return nil } 298 | 299 | type StubStoredValue struct{} 300 | 301 | func (StubStoredValue) Credit(*creditcard.Card, money.Money) error { return nil } 302 | ``` 303 | 304 | 305 | 306 | #### 測試中的局部變量 307 | 308 | 當您的測試中的變數指向雙元件時,選擇一個名稱,根據上下文最清楚地區分雙元件和其他生產類型。考慮一些您想要測試的生產代碼: 309 | 310 | ```go 311 | package payment 312 | 313 | import ( 314 | "path/to/creditcard" 315 | "path/to/money" 316 | ) 317 | 318 | type CreditCard interface { 319 | Charge(*creditcard.Card, money.Money) error 320 | } 321 | 322 | type Processor struct { 323 | CC CreditCard 324 | } 325 | 326 | var ErrBadInstrument = errors.New("payment: instrument is invalid or expired") 327 | 328 | func (p *Processor) Process(c *creditcard.Card, amount money.Money) error { 329 | if c.Expired() { 330 | return ErrBadInstrument 331 | } 332 | return p.CC.Charge(c, amount) 333 | } 334 | ``` 335 | 336 | 在測試中,一個稱為 "spy" 的 `CreditCard` 測試雙元件與生產類型並置,因此在名稱前加上前綴可能會提高清晰度: 337 | 338 | ```go 339 | // 較佳: 340 | package payment 341 | 342 | import "path/to/creditcardtest" 343 | 344 | func TestProcessor(t *testing.T) { 345 | var spyCC creditcardtest.Spy 346 | 347 | proc := &Processor{CC: spyCC} 348 | 349 | // declarations omitted: card and amount 350 | if err := proc.Process(card, amount); err != nil { 351 | t.Errorf("proc.Process(card, amount) = %v, want %v", got, want) 352 | } 353 | 354 | charges := []creditcardtest.Charge{ 355 | {Card: card, Amount: amount}, 356 | } 357 | 358 | if got, want := spyCC.Charges, charges; !cmp.Equal(got, want) { 359 | t.Errorf("spyCC.Charges = %v, want %v", got, want) 360 | } 361 | } 362 | ``` 363 | 364 | 這比名稱沒有前綴時更清晰。 365 | 366 | ```go 367 | // 不佳: 368 | package payment 369 | 370 | import "path/to/creditcardtest" 371 | 372 | func TestProcessor(t *testing.T) { 373 | var cc creditcardtest.Spy 374 | 375 | proc := &Processor{CC: cc} 376 | 377 | // declarations omitted: card and amount 378 | if err := proc.Process(card, amount); err != nil { 379 | t.Errorf("proc.Process(card, amount) = %v, want %v", got, want) 380 | } 381 | 382 | charges := []creditcardtest.Charge{ 383 | {Card: card, Amount: amount}, 384 | } 385 | 386 | if got, want := cc.Charges, charges; !cmp.Equal(got, want) { 387 | t.Errorf("cc.Charges = %v, want %v", got, want) 388 | } 389 | } 390 | ``` 391 | 392 | 393 | 394 | ### 變數遮蔽 395 | 396 | **注意:** 本解釋使用了兩個非正式術語,_踩踏_ 和 _遮蔽_。它們不是 Go 語言規範中的官方概念。 397 | 398 | 像許多編程語言一樣,Go 擁有可變變數:對變數進行賦值會改變其值。 399 | 400 | ```go 401 | // 較佳: 402 | func abs(i int) int { 403 | if i < 0 { 404 | i *= -1 405 | } 406 | return i 407 | } 408 | ``` 409 | 410 | 當使用 [短變數聲明] 與 `:=` 運算符時,在某些情況下不會創建新變數。我們可以稱這為**踩踏**。當原始值不再需要時,這樣做是可以的。 411 | 412 | ```go 413 | // 較佳: 414 | // innerHandler is a helper for some request handler, which itself issues 415 | // requests to other backends. 416 | func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { 417 | // Unconditionally cap the deadline for this part of request handling. 418 | ctx, cancel := context.WithTimeout(ctx, 3*time.Second) 419 | defer cancel() 420 | ctxlog.Info(ctx, "Capped deadline in inner request") 421 | 422 | // Code here no longer has access to the original context. 423 | // This is good style if when first writing this, you anticipate 424 | // that even as the code grows, no operation legitimately should 425 | // use the (possibly unbounded) original context that the caller provided. 426 | 427 | // ... 428 | } 429 | ``` 430 | 431 | 不過,在新的作用域中使用短變數聲明要小心:這會引入一個新變數。我們可以稱這為**遮蔽**原始變數。塊結束後的代碼指的是原始變數。這是一個錯誤的嘗試,有條件地縮短截止日期: 432 | 433 | ```go 434 | // 不佳: 435 | func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { 436 | // Attempt to conditionally cap the deadline. 437 | if *shortenDeadlines { 438 | ctx, cancel := context.WithTimeout(ctx, 3*time.Second) 439 | defer cancel() 440 | ctxlog.Info(ctx, "Capped deadline in inner request") 441 | } 442 | 443 | // BUG: "ctx" here again means the context that the caller provided. 444 | // The above buggy code compiled because both ctx and cancel 445 | // were used inside the if statement. 446 | 447 | // ... 448 | } 449 | ``` 450 | 451 | 一個正確版本的代碼可能是: 452 | 453 | ```go 454 | // 較佳: 455 | func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { 456 | if *shortenDeadlines { 457 | var cancel func() 458 | // Note the use of simple assignment, = and not :=. 459 | ctx, cancel = context.WithTimeout(ctx, 3*time.Second) 460 | defer cancel() 461 | ctxlog.Info(ctx, "Capped deadline in inner request") 462 | } 463 | // ... 464 | } 465 | ``` 466 | 467 | 在我們稱為踩踏的情況下,因為沒有新變數,所賦予的類型必須與原始變數的類型匹配。有了遮蔽,就引入了一個全新的實體,所以它可以有不同的類型。故意的遮蔽可以是一種有用的做法,但如果使用新名稱可以提高[清晰度](guide.md#clarity),那麼您總是可以使用新名稱。 468 | 469 | 在非常小的作用域之外使用與標準包同名的變數並不是一個好主意,因為這會使該包中的自由函數和值無法訪問。相反地,當為您的包選擇名稱時,避免使用可能需要[導入重命名](decisions.md#import-renaming)或在客戶端導致遮蔽其他好的變數名稱的名稱。 470 | 471 | ```go 472 | // 不佳: 473 | func LongFunction() { 474 | url := "https://example.com/" 475 | // Oops, now we can't use net/url in code below. 476 | } 477 | ``` 478 | 479 | [短變數聲明]: https://go.dev/ref/spec#Short_variable_declarations 480 | 481 | 482 | 483 | ### 套件 484 | 485 | Go 的套件有一個在 `package` 聲明上指定的名稱,與導入路徑分開。對於可讀性來說,套件名稱比路徑更重要。 486 | 487 | Go 套件名稱應該[與套件提供的內容相關](decisions.md#package-names)。僅將一個套件命名為 `util`、`helper`、`common` 或類似的通常是一個糟糕的選擇(雖然可以用作名稱的**一部分**)。不具信息性的名稱使代碼更難閱讀,如果使用得太廣泛,它們可能會導致不必要的[導入衝突](decisions.md#import-renaming)。 488 | 489 | 相反,考慮調用點將會看起來像什麼。 490 | 491 | ```go 492 | // 較佳: 493 | db := spannertest.NewDatabaseFromFile(...) 494 | 495 | _, err := f.Seek(0, io.SeekStart) 496 | 497 | b := elliptic.Marshal(curve, x, y) 498 | ``` 499 | 500 | 您甚至可以在不知道導入列表(`cloud.google.com/go/spanner/spannertest`、`io` 和 `crypto/elliptic`)的情況下大致知道這些各自做什麼。如果使用不夠專注的名稱,這些可能會讀作: 501 | 502 | ```go 503 | // 不佳: 504 | db := test.NewDatabaseFromFile(...) 505 | 506 | _, err := f.Seek(0, common.SeekStart) 507 | 508 | b := helper.Marshal(curve, x, y) 509 | ``` 510 | 511 | 512 | 513 | ## 套件大小 514 | 515 | 如果您在問自己 Go 套件應該有多大,以及是否應該將相關類型放在同一個套件中或將它們分成不同的套件,一個好的起點是 [Go 博客文章關於套件名稱][blog-pkg-names]。儘管文章的標題只是關於命名,但它不僅僅是關於命名。它包含了一些有用的提示,並引用了幾篇有用的文章和演講。 516 | 517 | 這裡還有一些其他的考慮和筆記。 518 | 519 | 用戶可以在一個頁面上看到套件的 [godoc],並且套件提供的類型所導出的任何方法都按類型分組。Godoc 也將構造函數與它們返回的類型分組。如果**客戶端代碼**可能需要兩個不同類型的值互相交互,對用戶來說,將它們放在同一個套件中可能會比較方便。 520 | 521 | 套件內的代碼可以訪問套件中未導出的標識符。如果您有一些相關的類型,它們的**實現**緊密耦合,將它們放在同一個套件中可以實現這種耦合,而不用在公共 API 中暴露這些細節。對這種耦合的一個好測試是想像兩個套件的假設用戶,這些套件涵蓋了緊密相關的主題:如果用戶必須導入兩個套件才能以任何有意義的方式使用其中任何一個,那麼將它們合併在一起通常是正確的做法。標準庫通常很好地展示了這種範圍和分層。 522 | 523 | 儘管如此,將整個項目放在一個套件中可能會使該套件過大。當某些東西在概念上是獨立的,給它自己的小套件可以使它更容易使用。套件的短名稱與客戶端所知的導出類型名稱一起工作,形成一個有意義的標識符:例如 `bytes.Buffer`、`ring.New`。[博客文章][blog-pkg-names]有更多例子。 524 | 525 | Go 風格對文件大小很靈活,因為維護者可以在套件內將代碼從一個文件移動到另一個文件,而不影響調用者。但作為一般指導原則:通常不是一個好主意在一個文件中有數千行代碼,或者有許多微小的文件。與其他一些語言不同,沒有“一種類型,一個文件”的慣例。作為一個經驗法則,文件應該足夠專注,以至於維護者可以告訴文件中包含了什麼,並且文件應該足夠小,以便一旦到達那裡就容易找到。標準庫經常將大型套件分割為幾個源文件,按文件分組相關代碼。[套件 `bytes`] 的源代碼是一個很好的例子。具有長套件文檔的套件可能會選擇專門用一個名為 `doc.go` 的文件,其中包含[套件文檔](decisions.md#package-comments)、一個套件聲明,以及其他什麼都沒有,但這不是必需的。 526 | 527 | 在 Google 代碼庫和使用 Bazel 的項目中,Go 代碼的目錄布局與開源 Go 項目中的不同:您可以在單個目錄中有多個 `go_library` 目標。給每個套件自己的目錄的一個好理由是,如果您期望將來開源您的項目。 528 | 529 | 另見: 530 | 531 | - [測試雙元件套件](#naming-doubles) 532 | 533 | [blog-pkg-names]: https://go.dev/blog/package-names 534 | [套件 `bytes`]: https://go.dev/src/bytes/ 535 | [godoc]: https://pkg.go.dev/ 536 | 537 | 538 | 539 | ## 引入 540 | 541 | 542 | 543 | ### Protos 及存根 544 | 545 | 由於其跨語言特性,Proto 庫的引入與標準 Go 引入的處理方式不同。重新命名 proto 引入的慣例基於生成該套件的規則: 546 | 547 | - 一般情況下,`go_proto_library` 規則使用 `pb` 後綴。 548 | - 一般情況下,`go_grpc_library` 規則使用 `grpc` 後綴。 549 | 550 | 通常使用短的一個或兩個字母的前綴: 551 | 552 | ```go 553 | // 較佳: 554 | import ( 555 | fspb "path/to/package/foo_service_go_proto" 556 | fsgrpc "path/to/package/foo_service_go_grpc" 557 | ) 558 | ``` 559 | 560 | 如果一個套件只使用了一個 proto 或該套件與該 proto 緊密相關,則可以省略前綴: 561 | 562 | 如果 proto 中的符號是通用的或不是非常自描述的,或者如果使用縮寫縮短套件名稱不清晰,一個短詞可以作為前綴: 563 | 564 | ```go 565 | // 較佳: 566 | import ( 567 | mapspb "path/to/package/maps_go_proto" 568 | ) 569 | ``` 570 | 571 | 在這種情況下,如果相關代碼並不已經明確與地圖相關,`mapspb.Address` 可能比 `mpb.Address` 更清晰。 572 | 573 | 574 | 575 | ### 引入順序 576 | 577 | 引入通常按以下兩個(或更多)塊的順序分組: 578 | 579 | 1. 標準庫引入(例如,`"fmt"`) 580 | 1. 專案引入(例如,`"/path/to/somelib"`) 581 | 1. (可選)Protobuf 引入(例如,`fpb "path/to/foo_go_proto"`) 582 | 1. (可選)副作用引入(例如,`_ "path/to/package"`) 583 | 584 | 如果一個文件沒有上述可選類別中的一個分組,相關的引入將包含在專案引入組中。 585 | 586 | 任何清晰且易於理解的引入分組通常都是可以的。例如,一個團隊可能選擇將 gRPC 引入與 protobuf 引入分開。 587 | 588 | > **注意:** 對於只維護兩個強制分組的代碼(一組用於標準庫,另一組用於所有其他引入),`goimports` 工具產生的輸出與此指導相符。 589 | > 590 | > 然而,`goimports` 對於強制分組之外的分組沒有認識;使用可選分組時,這些分組容易被該工具使無效。當使用可選分組時,作者和審查者都需要注意,以確保分組保持合規。 591 | > 592 | > 兩種方法都可以,但不要讓引入部分處於不一致、部分分組的狀態。 593 | 594 | 595 | 596 | ## 錯誤處理 597 | 598 | 在 Go 中,[錯誤是值];它們由代碼創建並由代碼消費。錯誤可以是: 599 | 600 | - 轉換為顯示給人看的診斷信息 601 | - 由維護者使用 602 | - 被終端用戶解釋 603 | 604 | 錯誤消息還會出現在包括日誌消息、錯誤轉儲和渲染的 UI 等不同的表面上。 605 | 606 | 處理(產生或消費)錯誤的代碼應該要有意識地進行。忽略或盲目傳播錯誤返回值可能很誘人。然而,總是值得考慮當前調用棧中的函數是否處於最有效處理錯誤的位置。這是一個大話題,很難給出類別性的建議。使用您的判斷,但請記住以下考慮: 607 | 608 | - 創建錯誤值時,決定是否給它任何[結構](#error-structure)。 609 | - 處理錯誤時,考慮[添加信息](#error-extra-info),您擁有但調用者和/或被調用者可能沒有的信息。 610 | - 另見有關[錯誤日誌記錄](#error-logging)的指南。 611 | 612 | 雖然通常不適合忽略錯誤,但在協調相關操作時,通常只有第一個錯誤是有用的,這是一個合理的例外。[`errgroup`] 套件為可以作為一組失敗或取消的一組操作提供了方便的抽象。 613 | 614 | [錯誤是值]: https://go.dev/blog/errors-are-values 615 | [`errgroup`]: https://pkg.go.dev/golang.org/x/sync/errgroup 616 | 617 | 另見: 618 | 619 | - [Effective Go on errors](https://go.dev/doc/effective_go#errors) 620 | - [A post by the Go Blog on errors](https://go.dev/blog/go1.13-errors) 621 | - [Package `errors`](https://pkg.go.dev/errors) 622 | - [Package `upspin.io/errors`](https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html) 623 | - [GoTip #89: When to Use Canonical Status Codes as Errors](https://google.github.io/styleguide/go/index.html#gotip) 624 | - [GoTip #48: Error Sentinel Values](https://google.github.io/styleguide/go/index.html#gotip) 625 | - [GoTip #13: Designing Errors for Checking](https://google.github.io/styleguide/go/index.html#gotip) 626 | 627 | 628 | 629 | ### 錯誤結構 630 | 631 | 如果呼叫者需要檢查錯誤(例如,區分不同的錯誤情況),請給予錯誤值結構,以便可以通過程式方式完成,而不是讓呼叫者進行字串匹配。這個建議適用於生產代碼以及關心不同錯誤情況的測試。 632 | 633 | 最簡單的結構化錯誤是無參數的全域值。 634 | 635 | ```go 636 | type Animal string 637 | 638 | var ( 639 | // ErrDuplicate occurs if this animal has already been seen. 640 | ErrDuplicate = errors.New("duplicate") 641 | 642 | // ErrMarsupial occurs because we're allergic to marsupials outside Australia. 643 | // Sorry. 644 | ErrMarsupial = errors.New("marsupials are not supported") 645 | ) 646 | 647 | func process(animal Animal) error { 648 | switch { 649 | case seen[animal]: 650 | return ErrDuplicate 651 | case marsupial(animal): 652 | return ErrMarsupial 653 | } 654 | seen[animal] = true 655 | // ... 656 | return nil 657 | } 658 | ``` 659 | 660 | 呼叫者可以簡單地將函數返回的錯誤值與已知的錯誤值之一進行比較: 661 | 662 | ```go 663 | // 較佳: 664 | func handlePet(...) { 665 | switch err := process(an); err { 666 | case ErrDuplicate: 667 | return fmt.Errorf("feed %q: %v", an, err) 668 | case ErrMarsupial: 669 | // Try to recover with a friend instead. 670 | alternate = an.BackupAnimal() 671 | return handlePet(..., alternate, ...) 672 | } 673 | } 674 | ``` 675 | 676 | 上述方法使用了哨兵值,其中錯誤必須等於(在 `==` 的意義上)預期值。在許多情況下,這是完全足夠的。如果 `process` 返回包裝錯誤(如下所述),您可以使用 [`errors.Is`]。 677 | 678 | ```go 679 | // 較佳: 680 | func handlePet(...) { 681 | switch err := process(an); { 682 | case errors.Is(err, ErrDuplicate): 683 | return fmt.Errorf("feed %q: %v", an, err) 684 | case errors.Is(err, ErrMarsupial): 685 | // ... 686 | } 687 | } 688 | ``` 689 | 690 | Do not attempt to distinguish errors based on their string form. (See 691 | [Go Tip #13: Designing Errors for Checking](https://google.github.io/styleguide/go/index.html#gotip) 692 | for more.) 693 | 694 | ```go 695 | // 不佳: 696 | func handlePet(...) { 697 | err := process(an) 698 | if regexp.MatchString(`duplicate`, err.Error()) {...} 699 | if regexp.MatchString(`marsupial`, err.Error()) {...} 700 | } 701 | ``` 702 | 703 | 如果錯誤中有呼叫者需要以程式方式處理的額外資訊,理想情況下應以結構化方式呈現。例如,[`os.PathError`] 類型的文件說明將失敗操作的路徑名放在結構體欄位中,呼叫者可以輕鬆訪問。 704 | 705 | 其他錯誤結構可以根據需要使用,例如包含錯誤代碼和詳細字串的專案結構體。[`status` 套件][status] 是一種常見的封裝方式;如果選擇這種方法(您並不必須這樣做),請使用[標準代碼]。請參閱 [Go Tip #89: When to Use Canonical Status Codes as Errors](https://google.github.io/styleguide/go/index.html#gotip) 以了解是否應使用狀態代碼。 706 | 707 | [`os.PathError`]: https://pkg.go.dev/os#PathError 708 | [`errors.Is`]: https://pkg.go.dev/errors#Is 709 | [`errors.As`]: https://pkg.go.dev/errors#As 710 | [`package cmp`]: https://pkg.go.dev/github.com/google/go-cmp/cmp 711 | [status]: https://pkg.go.dev/google.golang.org/grpc/status 712 | [標準代碼]: https://pkg.go.dev/google.golang.org/grpc/codes 713 | 714 | 715 | 716 | ### 為錯誤添加資訊 717 | 718 | 任何返回錯誤的函數都應該努力使錯誤值變得有用。通常,該函數位於調用鏈的中間,只是傳播它所調用的其他函數(可能甚至是來自另一個包)的錯誤。在這種情況下,有機會用額外的信息來註釋錯誤,但程序員應確保錯誤中有足夠的信息,而不添加重複或無關的細節。如果不確定,請嘗試在開發過程中觸發錯誤條件:這是一種評估錯誤觀察者(無論是人類還是代碼)最終會得到什麼的好方法。 719 | 720 | 約定和良好的文檔有助於此。例如,標準包 `os` 宣傳其錯誤在可用時包含路徑信息。這是一種有用的風格,因為調用者在返回錯誤時不需要用他們已經提供給失敗函數的訊息來註釋它。 721 | 722 | ```go 723 | // 較佳: 724 | if err := os.Open("settings.txt"); err != nil { 725 | return err 726 | } 727 | 728 | // Output: 729 | // 730 | // open settings.txt: no such file or directory 731 | ``` 732 | 733 | 如果有關於錯誤的**意義**有什麼有趣的事情要說,當然可以添加。只需考慮哪個呼叫鏈的層級最適合理解這個意義。 734 | 735 | ```go 736 | // 較佳: 737 | if err := os.Open("settings.txt"); err != nil { 738 | // 我們傳達這個錯誤對我們的重要性。請注意,目前的函數可能會執行多個可能失敗的文件操作, 739 | // 因此這些註釋也可以用來向呼叫者澄清到底出了什麼問題。 740 | return fmt.Errorf("啟動代碼不可用: %v", err) 741 | } 742 | 743 | // 輸出: 744 | // 745 | // 啟動代碼不可用: open settings.txt: no such file or directory 746 | ``` 747 | 748 | 對比這裡的冗餘信息: 749 | 750 | ```go 751 | // 不佳: 752 | if err := os.Open("settings.txt"); err != nil { 753 | return fmt.Errorf("無法打開 settings.txt: %w", err) 754 | } 755 | 756 | // 輸出: 757 | // 758 | // 無法打開 settings.txt: open settings.txt: no such file or directory 759 | ``` 760 | 761 | 當向傳播的錯誤添加信息時,你可以選擇包裝錯誤或呈現一個新的錯誤。使用 `fmt.Errorf` 中的 `%w` 來包裝錯誤允許呼叫者訪問原始錯誤的數據。這在某些時候非常有用,但在其他情況下,這些細節對呼叫者來說可能是誤導或不感興趣的。請參閱[錯誤包裝的博客文章](https://blog.golang.org/go1.13-errors)以獲取更多信息。包裝錯誤還會以不明顯的方式擴展你的包的 API 表面,如果你更改包的實現細節,這可能會導致破壞。 762 | 763 | 除非你也記錄(並有測試驗證)你暴露的底層錯誤,否則最好避免使用 `%w`。如果你不期望你的呼叫者調用 `errors.Unwrap`、`errors.Is` 等等,那麼就不要使用 `%w`。 764 | 765 | 同樣的概念適用於[結構化錯誤](#error-structure)如 [`*status.Status`][status](請參閱[標準代碼])。例如,如果你的服務器向後端發送格式錯誤的請求並收到 `InvalidArgument` 代碼,假設客戶端沒有做錯任何事情,這個代碼不應該傳播給客戶端。相反,應該向客戶端返回一個 `Internal` 標準代碼。 766 | 767 | 然而,註釋錯誤有助於自動化日誌系統保留錯誤的狀態負載。例如,在內部函數中註釋錯誤是合適的: 768 | 769 | ```go 770 | // 較佳: 771 | func (s *Server) internalFunction(ctx context.Context) error { 772 | // ... 773 | if err != nil { 774 | return fmt.Errorf("無法找到遠程文件: %w", err) 775 | } 776 | } 777 | ``` 778 | 779 | 直接在系統邊界(通常是 RPC、IPC、存儲等)處的代碼應使用標準錯誤空間報告錯誤。這裡的代碼有責任處理特定領域的錯誤並以標準方式表示它們。例如: 780 | 781 | ```go 782 | // 不佳: 783 | func (*FortuneTeller) SuggestFortune(context.Context, *pb.SuggestionRequest) (*pb.SuggestionResponse, error) { 784 | // ... 785 | if err != nil { 786 | return nil, fmt.Errorf("無法找到遠程文件: %w", err) 787 | } 788 | } 789 | ``` 790 | 791 | ```go 792 | // 較佳: 793 | import ( 794 | "google.golang.org/grpc/codes" 795 | "google.golang.org/grpc/status" 796 | ) 797 | func (*FortuneTeller) SuggestFortune(context.Context, *pb.SuggestionRequest) (*pb.SuggestionResponse, error) { 798 | // ... 799 | if err != nil { 800 | // 或者使用 fmt.Errorf 和 %w 動詞,如果故意包裝一個呼叫者應該解包的錯誤。 801 | return nil, status.Errorf(codes.Internal, "無法找到財富數據庫", status.ErrInternal) 802 | } 803 | } 804 | ``` 805 | 806 | 另請參閱: 807 | 808 | - [錯誤文檔約定](#documentation-conventions-errors) 809 | 810 | 811 | 812 | ### %w 在錯誤中的位置 813 | 814 | 建議將 `%w` 放在錯誤字串的末尾。 815 | 816 | 錯誤可以使用 [`%w` 動詞](https://blog.golang.org/go1.13-errors) 包裝,或將它們放在實作 `Unwrap() error` 的[結構化錯誤](https://google.github.io/styleguide/go/index.html#gotip)中(例如:[`fs.PathError`](https://pkg.go.dev/io/fs#PathError))。 817 | 818 | 包裝的錯誤會形成錯誤鏈:每一層新的包裝都會在錯誤鏈的前端新增一個新條目。錯誤鏈可以使用 `Unwrap() error` 方法遍歷。例如: 819 | 820 | ```go 821 | err1 := fmt.Errorf("err1") 822 | err2 := fmt.Errorf("err2: %w", err1) 823 | err3 := fmt.Errorf("err3: %w", err2) 824 | ``` 825 | 826 | 這形成了一個如下形式的錯誤鏈, 827 | 828 | ```mermaid 829 | flowchart LR 830 | err3 == err3 wraps err2 ==> err2; 831 | err2 == err2 wraps err1 ==> err1; 832 | ``` 833 | 834 | 無論 `%w` 動詞放在哪裡,返回的錯誤總是代表錯誤鏈的前端,而 `%w` 是下一個子錯誤。同樣,`Unwrap() error` 總是從最新的錯誤遍歷到最舊的錯誤。 835 | 836 | 然而,`%w` 動詞的位置會影響錯誤鏈是按最新到最舊、最舊到最新還是既不是最新到最舊也不是最舊到最新的順序打印: 837 | 838 | ```go 839 | // 較佳: 840 | err1 := fmt.Errorf("err1") 841 | err2 := fmt.Errorf("err2: %w", err1) 842 | err3 := fmt.Errorf("err3: %w", err2) 843 | fmt.Println(err3) // err3: err2: err1 844 | // err3 是一個從最新到最舊的錯誤鏈,按最新到最舊的順序打印。 845 | ``` 846 | 847 | ```go 848 | // 不佳: 849 | err1 := fmt.Errorf("err1") 850 | err2 := fmt.Errorf("%w: err2", err1) 851 | err3 := fmt.Errorf("%w: err3", err2) 852 | fmt.Println(err3) // err1: err2: err3 853 | // err3 是一個從最新到最舊的錯誤鏈,按最舊到最新的順序打印。 854 | ``` 855 | 856 | ```go 857 | // 不佳: 858 | err1 := fmt.Errorf("err1") 859 | err2 := fmt.Errorf("err2-1 %w err2-2", err1) 860 | err3 := fmt.Errorf("err3-1 %w err3-2", err2) 861 | fmt.Println(err3) // err3-1 err2-1 err1 err2-2 err3-2 862 | // err3 是一個從最新到最舊的錯誤鏈,既不是按最新到最舊也不是按最舊到最新的順序打印。 863 | ``` 864 | 865 | 因此,為了使錯誤文本反映錯誤鏈結構,建議將 `%w` 動詞放在末尾,形式為 `[...]: %w`。 866 | 867 | 868 | 869 | ### 錯誤日誌 870 | 871 | 函式有時需要告訴外部系統發生了錯誤,但不會將錯誤傳遞給它們的呼叫者。此時記錄日誌是一個明顯的選擇;但要注意你記錄錯誤的內容和方式。 872 | 873 | - 就像[好的測試失敗訊息]一樣,日誌訊息應該清楚地表達出問題所在,並透過包含相關資訊來幫助維護者診斷問題。 874 | 875 | - 避免重複。如果你返回一個錯誤,通常最好不要自己記錄日誌,而是讓呼叫者處理它。呼叫者可以選擇記錄錯誤,或者使用 [`rate.Sometimes`] 來限制日誌記錄的頻率。其他選項包括嘗試恢復或甚至[停止程式]。無論如何,讓呼叫者控制有助於避免日誌垃圾。 876 | 877 | 然而,這種方法的缺點是,任何日誌都是使用呼叫者的行號記錄的。 878 | 879 | - 小心處理[個人識別資訊 (PII)]。許多日誌接收端並不適合存放敏感的終端使用者資訊。 880 | 881 | - 謹慎使用 `log.Error`。ERROR 等級的日誌會觸發刷新,並且比較低等級的日誌更耗費資源。這可能對你的程式碼造成嚴重的效能影響。在決定使用錯誤等級還是警告等級時,考慮最佳實踐,即錯誤等級的訊息應該是可操作的,而不是比警告更「嚴重」。 882 | 883 | - 在 Google 內部,我們有監控系統,可以設置更有效的警報,而不是寫入日誌檔案並希望有人注意到它。這類似但不完全等同於標準函式庫中的 [package `expvar`]。 884 | 885 | [好的測試失敗訊息]: https://google.github.io/styleguide/go/decisions.md#useful-test-failures 886 | [停止程式]: #checks-and-panics 887 | [`rate.Sometimes`]: https://pkg.go.dev/golang.org/x/time/rate#Sometimes 888 | [個人識別資訊 (PII)]: https://en.wikipedia.org/wiki/Personal_data 889 | [package `expvar`]: https://pkg.go.dev/expvar 890 | 891 | 892 | 893 | #### 詳細等級 894 | 895 | 善用詳細日誌記錄([`log.V`])。詳細日誌記錄對於開發和追蹤非常有用。建立一個關於詳細等級的慣例會很有幫助。例如: 896 | 897 | - 在 `V(1)` 寫入少量額外資訊 898 | - 在 `V(2)` 追蹤更多資訊 899 | - 在 `V(3)` 傾倒大量內部狀態 900 | 901 | 為了將詳細日誌記錄的成本降到最低,你應該確保即使在 `log.V` 關閉時也不會意外呼叫昂貴的函式。`log.V` 提供了兩種 API。較方便的一種有可能會導致這種意外的開銷。如果有疑慮,請使用稍微冗長的風格。 902 | 903 | ```go 904 | // 較佳: 905 | for _, sql := range queries { 906 | log.V(1).Infof("Handling %v", sql) 907 | if log.V(2) { 908 | log.Infof("Handling %v", sql.Explain()) 909 | } 910 | sql.Run(...) 911 | } 912 | ``` 913 | 914 | ```go 915 | // 不佳: 916 | // 即使這個日誌沒有被打印,sql.Explain 也會被呼叫。 917 | log.V(2).Infof("處理 %v", sql.Explain()) 918 | ``` 919 | 920 | [`log.V`]: https://pkg.go.dev/github.com/golang/glog#V 921 | 922 | 923 | 924 | ### 程式初始化 925 | 926 | 程式初始化錯誤(例如錯誤的標誌和配置)應該向上傳遞到 `main`,`main` 應該調用 `log.Exit` 並附帶解釋如何修復錯誤的訊息。在這些情況下,一般不應使用 `log.Fatal`,因為指向檢查的堆疊追蹤不太可能像人類生成的可操作訊息那樣有用。 927 | 928 | 929 | 930 | ### 程式檢查和恐慌 931 | 932 | 如[反對恐慌的決定]所述,標準錯誤處理應圍繞錯誤返回值進行結構化。庫應該更傾向於向調用者返回錯誤,而不是中止程式,特別是對於臨時錯誤。 933 | 934 | 有時需要對不變量進行一致性檢查,如果違反則終止程式。一般來說,只有當不變量檢查失敗意味著內部狀態已經無法恢復時才會這樣做。在 Google 代碼庫中,最可靠的方法是調用 `log.Fatal`。在這些情況下使用 `panic` 不可靠,因為延遲函數可能會死鎖或進一步損壞內部或外部狀態。 935 | 936 | 同樣,抵制恢復恐慌以避免崩潰的誘惑,因為這樣做可能會導致傳播損壞的狀態。離恐慌越遠,你對程式狀態的了解就越少,程式可能持有鎖或其他資源。然後程式可能會出現其他意想不到的故障模式,使問題更難診斷。與其嘗試在代碼中處理意外的恐慌,不如使用監控工具來顯示意外的故障,並優先修復相關的錯誤。 937 | 938 | **注意:** 標準的 [`net/http` 服務器] 違反了這個建議,並從請求處理程序中恢復恐慌。經驗豐富的 Go 工程師一致認為這是歷史性的錯誤。如果你從其他語言的應用服務器中抽樣服務器日誌,通常會發現大量未處理的堆棧跟踪。在你的服務器中避免這個陷阱。 939 | 940 | [反對恐慌的決定]: https://google.github.io/styleguide/go/decisions.md#dont-panic 941 | [`net/http` 服務器]: https://pkg.go.dev/net/http#Server 942 | 943 | 944 | 945 | ### 何時恐慌 946 | 947 | 標準庫在 API 誤用時會恐慌。例如,[`reflect`] 在許多情況下發出恐慌,當值以表明它被誤解的方式訪問時。這類似於核心語言錯誤的恐慌,例如訪問超出範圍的切片元素。代碼審查和測試應該發現這些錯誤,這些錯誤不應出現在生產代碼中。這些恐慌充當不依賴於庫的不變量檢查,因為標準庫無法訪問 Google 代碼庫使用的[分級 `log`] 包。 948 | 949 | [`reflect`]: https://pkg.go.dev/reflect 950 | [分級 `log`]: decisions.md#logging 951 | 952 | 另一種恐慌可能有用的情況,雖然不常見,是作為包的內部實現細節,總是在調用鏈中有匹配的恢復。解析器和類似的深度嵌套、緊密耦合的內部函數組可以從這種設計中受益,其中管道錯誤返回增加了複雜性而沒有價值。這種設計的關鍵屬性是這些恐慌永遠不允許跨包邊界傳播,並且不構成包的 API 的一部分。這通常通過頂層延遲恢復來實現,將傳播的恐慌轉換為在公共 API 表面返回的錯誤。 953 | 954 | 當編譯器無法識別不可達代碼時,例如使用不會返回的函數 955 | 956 | ```go 957 | // 較佳: 958 | func answer(i int) string { 959 | switch i { 960 | case 42: 961 | return "yup" 962 | case 54: 963 | return "base 13, huh" 964 | default: 965 | log.Fatalf("Sorry, %d is not the answer.", i) 966 | panic("unreachable") 967 | } 968 | } 969 | ``` 970 | 971 | [在標誌解析之前不要調用 `log` 函數。](https://pkg.go.dev/github.com/golang/glog#pkg-overview) 972 | 如果你必須在 `init` 函數中終止,恐慌(panic)是可以接受的,代替記錄調用。 973 | 974 | 975 | 976 | ## 文件 977 | 978 | 979 | 980 | ### 慣例 981 | 982 | 本節補充了決策文件的[評論]部分。 983 | 984 | 以熟悉的風格記錄的 Go 代碼更易於閱讀,也不太可能被誤用,而不是被錯誤記錄或根本沒有記錄的代碼。可運行的[示例]會顯示在 Godoc 和代碼搜索中,是解釋如何使用代碼的絕佳方式。 985 | 986 | [示例]: decisions.md#examples 987 | 988 | 989 | 990 | #### 參數和配置 991 | 992 | 並非每個參數都必須在文檔中列出。這適用於: 993 | 994 | - 函數和方法參數 995 | - 結構字段 996 | - 選項的 API 997 | 998 | 通過說明它們為什麼有趣來記錄容易出錯或不明顯的字段和參數。 999 | 1000 | 在以下代碼片段中,突出顯示的評論對讀者幾乎沒有用處: 1001 | 1002 | ```go 1003 | // 不佳: 1004 | // Sprintf formats according to a format specifier and returns the resulting 1005 | // string. 1006 | // 1007 | // format is the format, and data is the interpolation data. 1008 | func Sprintf(format string, data ...any) string 1009 | ``` 1010 | 1011 | 然而,這個片段展示了一個類似於前面的代碼場景,其中評論改為說明一些不明顯或對讀者有實質幫助的內容: 1012 | 1013 | ```go 1014 | // 較佳: 1015 | // Sprintf 根據格式規範進行格式化並返回結果字符串。 1016 | // 1017 | // 提供的數據用於插值格式字符串。如果數據與預期的格式動詞不匹配或數據量不滿足格式規範,該函數將根據上面描述的格式錯誤部分將格式錯誤警告內聯到輸出字符串中。 1018 | func Sprintf(format string, data ...any) string 1019 | ``` 1020 | 1021 | 在選擇記錄什麼以及記錄到什麼深度時,請考慮你的可能受眾。維護者、團隊的新成員、外部用戶,甚至是六個月後的你自己,可能會欣賞與你首次編寫文檔時所想的略有不同的信息。 1022 | 1023 | 另請參見: 1024 | 1025 | - [GoTip #41: 識別函數調用參數] 1026 | - [GoTip #51: 配置模式] 1027 | 1028 | [GoTip #41: 識別函數調用參數]: https://google.github.io/styleguide/go/index.html#gotip 1029 | [GoTip #51: 配置模式]: https://google.github.io/styleguide/go/index.html#gotip 1030 | 1031 | 1032 | 1033 | #### 上下文 1034 | 1035 | 上下文參數的取消會中斷提供給它的函數,這是隱含的。如果函數可以返回錯誤,通常是 `ctx.Err()`。 1036 | 1037 | 這一事實不需要重述: 1038 | 1039 | ```go 1040 | // 不佳: 1041 | // Run executes the worker's run loop. 1042 | // 1043 | // The method will process work until the context is cancelled and accordingly 1044 | // returns an error. 1045 | func (Worker) Run(ctx context.Context) error 1046 | ``` 1047 | 1048 | 因為這是隱含的,以下更好: 1049 | 1050 | ```go 1051 | // 較佳: 1052 | // Run executes the worker's run loop. 1053 | func (Worker) Run(ctx context.Context) error 1054 | ``` 1055 | 1056 | 當上下文行為不同或不明顯時,如果以下任何一項為真,則應明確記錄。 1057 | 1058 | - 當上下文被取消時,函數返回 `ctx.Err()` 以外的錯誤: 1059 | 1060 | ```go 1061 | // 較佳: 1062 | // Run executes the worker's run loop. 1063 | // 1064 | // If the context is cancelled, Run returns a nil error. 1065 | func (Worker) Run(ctx context.Context) error 1066 | ``` 1067 | 1068 | - 函數有其他機制可能中斷它或影響其生命周期: 1069 | 1070 | ```go 1071 | // 較佳: 1072 | // Run 執行工作者的運行循環。 1073 | // 1074 | // Run 處理工作,直到上下文被取消或調用 Stop。 1075 | // 上下文取消在內部異步處理:run 可能在所有工作停止之前返回。 1076 | // Stop 方法是同步的,並等待運行循環中的所有操作完成。 1077 | // 使用 Stop 進行優雅的關閉。 1078 | func (Worker) Run(ctx context.Context) error 1079 | 1080 | func (Worker) Stop() 1081 | ``` 1082 | 1083 | - 函數對上下文生命周期、血統或附加值有特殊期望: 1084 | 1085 | ```go 1086 | // 較佳: 1087 | // NewReceiver 開始接收發送到指定隊列的消息。 1088 | // 上下文不應該有截止日期。 1089 | func NewReceiver(ctx context.Context) *Receiver 1090 | 1091 | // Principal 返回發起調用的方的可讀名稱。 1092 | // 上下文必須具有從 security.NewContext 附加的值。 1093 | func Principal(ctx context.Context) (name string, ok bool) 1094 | ``` 1095 | 1096 | **警告:** 避免設計使其調用者做出這樣要求(如上下文沒有截止日期)的 API。上述僅是如何記錄這種情況的示例,而不是對該模式的認可。 1097 | 1098 | 1099 | 1100 | #### 並發 1101 | 1102 | Go 使用者假設概念上只讀操作是安全的,可以並發使用,且不需要額外的同步。 1103 | 1104 | 在這個 Godoc 中,可以安全地刪除關於並發的額外說明: 1105 | 1106 | ```go 1107 | // Len 返回緩衝區未讀部分的字節數; 1108 | // b.Len() == len(b.Bytes())。 1109 | // 1110 | // 它可以安全地被多個 goroutine 並發調用。 1111 | func (*Buffer) Len() int 1112 | ``` 1113 | 1114 | 然而,變更操作則不假設是安全的並發使用,並且需要使用者考慮同步。 1115 | 1116 | 同樣,關於並發的額外說明可以安全地刪除: 1117 | 1118 | ```go 1119 | // Grow 增加緩衝區的容量。 1120 | // 1121 | // 它不安全,不能被多個 goroutine 並發調用。 1122 | func (*Buffer) Grow(n int) 1123 | ``` 1124 | 1125 | 如果以下任何一項為真,強烈建議進行文檔記錄。 1126 | 1127 | - 不清楚操作是只讀還是變更: 1128 | 1129 | ```go 1130 | // 較佳: 1131 | package lrucache 1132 | 1133 | // Lookup 返回緩存中與鍵相關聯的數據。 1134 | // 1135 | // 此操作不安全,不能並發使用。 1136 | func (*Cache) Lookup(key string) (data []byte, ok bool) 1137 | ``` 1138 | 1139 | 為什麼?查找鍵時的緩存命中會在內部更改 LRU 緩存。這種實現方式對所有讀者來說可能並不明顯。 1140 | 1141 | - 同步由 API 提供: 1142 | 1143 | ```go 1144 | // 較佳: 1145 | package fortune_go_proto 1146 | 1147 | // NewFortuneTellerClient 返回一個用於 FortuneTeller 服務的 *rpc.Client。 1148 | // 它可以安全地被多個 goroutine 同時使用。 1149 | func NewFortuneTellerClient(cc *rpc.ClientConn) *FortuneTellerClient 1150 | ``` 1151 | 1152 | 為什麼?Stubby 提供同步。 1153 | 1154 | **注意:** 如果 API 是一種類型,並且 API 完全提供同步,通常只有類型定義記錄語義。 1155 | 1156 | - API 消費者使用用戶實現的接口類型,並且接口的消費者有特定的並發要求: 1157 | 1158 | ```go 1159 | // 較佳: 1160 | package health 1161 | 1162 | // A Watcher reports the health of some entity (usually a backend service). 1163 | // 1164 | // Watcher 方法可以安全地被多個 goroutine 同時使用。 1165 | type Watcher interface { 1166 | // Watch 當 Watcher 的狀態發生變化時,會在傳入的通道上發送 true。 1167 | Watch(changed chan<- bool) (unwatch func()) 1168 | 1169 | // Health 如果被監視的實體是健康的,則返回 nil,否則返回一個非 nil 的錯誤,解釋為什麼實體不健康。 1170 | Health() error 1171 | } 1172 | ``` 1173 | 1174 | 為什麼?API 是否可以安全地被多個 goroutine 使用是其契約的一部分。 1175 | 1176 | 1177 | 1178 | #### 清理 1179 | 1180 | 記錄 API 的任何明確清理要求。否則,調用者將無法正確使用 API,導致資源洩漏和其他可能的錯誤。 1181 | 1182 | 指出由調用者負責的清理工作: 1183 | 1184 | ```go 1185 | // 較佳: 1186 | // NewTicker 返回一個新的 Ticker,其中包含一個通道,該通道會在每次滴答後發送當前時間。 1187 | // 1188 | // 完成後調用 Stop 以釋放 Ticker 的相關資源。 1189 | func NewTicker(d Duration) *Ticker 1190 | 1191 | func (*Ticker) Stop() 1192 | ``` 1193 | 1194 | If it is potentially unclear how to clean up the resources, explain how: 1195 | 1196 | ```go 1197 | // 較佳: 1198 | // Get 發送一個 GET 請求到指定的 URL。 1199 | // 1200 | // 當 err 為 nil 時,resp 總是包含一個非 nil 的 resp.Body。 1201 | // 調用者在讀取完 resp.Body 後應該關閉它。 1202 | // 1203 | // resp, err := http.Get("http://example.com/") 1204 | // if err != nil { 1205 | // // handle error 1206 | // } 1207 | // defer resp.Body.Close() 1208 | // body, err := io.ReadAll(resp.Body) 1209 | func (c *Client) Get(url string) (resp *Response, err error) 1210 | ``` 1211 | 1212 | 另請參見: 1213 | 1214 | - [GoTip #110: 不要將 Exit 與 Defer 混用] 1215 | 1216 | [GoTip #110: 不要將 Exit 與 Defer 混用]: https://google.github.io/styleguide/go/index.html#gotip 1217 | 1218 | 1219 | 1220 | #### 錯誤 1221 | 1222 | 記錄你的函數返回給調用者的重要錯誤哨兵值或錯誤類型,以便調用者可以預期他們可以在代碼中處理哪些類型的情況。 1223 | 1224 | ```go 1225 | // 較佳: 1226 | package os 1227 | 1228 | // Read 從文件中讀取最多 len(b) 字節並將它們存儲在 b 中。它返回讀取的字節數和遇到的任何錯誤。 1229 | // 1230 | // 在文件結尾,Read 返回 0 和 io.EOF。 1231 | func (*File) Read(b []byte) (n int, err error) { 1232 | ``` 1233 | 1234 | 當函數返回特定錯誤類型時,正確註明錯誤是否為指針接收者: 1235 | 1236 | ```go 1237 | // 較佳: 1238 | package os 1239 | 1240 | type PathError struct { 1241 | Op string 1242 | Path string 1243 | Err error 1244 | } 1245 | 1246 | // Chdir 更改當前工作目錄為指定目錄。 1247 | // 1248 | // 如果有錯誤,它將是 *PathError 類型。 1249 | func Chdir(dir string) error { 1250 | ``` 1251 | 1252 | 記錄返回值是否為指針接收者,使調用者能夠正確地使用 [`errors.Is`]、[`errors.As`] 和 [`package cmp`] 來比較錯誤。這是因為非指針值不等同於指針值。 1253 | 1254 | **注意:** 在 `Chdir` 示例中,返回類型寫為 `error` 而不是 `*PathError`,這是由於[空接口值的工作方式](https://go.dev/doc/faq#nil_error)。 1255 | 1256 | 當行為適用於包中的大多數錯誤時,在[包的文檔](decisions.md#package-comments)中記錄整體錯誤約定: 1257 | 1258 | ```go 1259 | // 較佳: 1260 | // Package os 提供與操作系統功能的跨平台接口。 1261 | // 1262 | // 通常,錯誤中會包含更多信息。例如,如果一個需要文件名的調用失敗,如 Open 或 Stat,錯誤將在打印時包含失敗的文件名,並且將是 *PathError 類型,可以解包以獲取更多信息。 1263 | package os 1264 | ``` 1265 | 1266 | 深思熟慮地應用這些方法可以在不費太多力氣的情況下為錯誤添加[額外信息](#error-extra-info),並幫助調用者避免添加冗餘的註釋。 1267 | 1268 | 另請參見: 1269 | 1270 | - [Go Tip #106: 錯誤命名慣例](https://google.github.io/styleguide/go/index.html#gotip) 1271 | - [Go Tip #89: 何時使用標準狀態碼作為錯誤](https://google.github.io/styleguide/go/index.html#gotip) 1272 | 1273 | 1274 | 1275 | ### 預覽 1276 | 1277 | Go 提供了一個[文檔服務器](https://pkg.go.dev/golang.org/x/pkgsite/cmd/pkgsite)。建議在代碼審查過程中預覽你的代碼生成的文檔,這有助於驗證[godoc 格式]是否正確渲染。 1278 | 1279 | [godoc 格式]: #godoc-formatting 1280 | 1281 | 1282 | 1283 | ### Godoc 格式化 1284 | 1285 | [Godoc] 提供了一些特定的語法來[格式化文檔]。 1286 | 1287 | - 段落之間需要空行: 1288 | 1289 | ```go 1290 | // 較佳: 1291 | // LoadConfig 從指定的文件中讀取配置。 1292 | // 1293 | // 有關配置文件格式的詳細信息,請參見 some/shortlink。 1294 | ``` 1295 | 1296 | - 測試文件可以包含[可運行的示例],這些示例會附加到 godoc 中的相應文檔: 1297 | 1298 | ```go 1299 | // 較佳: 1300 | func ExampleConfig_WriteTo() { 1301 | cfg := &Config{ 1302 | Name: "example", 1303 | } 1304 | if err := cfg.WriteTo(os.Stdout); err != nil { 1305 | log.Exitf("Failed to write config: %s", err) 1306 | } 1307 | // Output: 1308 | // { 1309 | // "name": "example" 1310 | // } 1311 | } 1312 | ``` 1313 | 1314 | - 行首額外縮進兩個空格會按原樣格式化它們: 1315 | 1316 | ```go 1317 | // 較佳: 1318 | // Update 以原子交易方式運行該函數。 1319 | // 1320 | // 這通常與匿名的 TransactionFunc 一起使用: 1321 | // 1322 | // if err := db.Update(func(state *State) { state.Foo = bar }); err != nil { 1323 | // //... 1324 | // } 1325 | ``` 1326 | 1327 | 請注意,將代碼放在可運行的示例中通常比將其包含在註釋中更合適。 1328 | 1329 | 這種逐字格式化可以用於 godoc 本身不支持的格式,例如列表和表格: 1330 | 1331 | ```go 1332 | // 較佳: 1333 | // LoadConfig 從指定的文件中讀取配置。 1334 | // 1335 | // LoadConfig 以特殊方式處理以下鍵: 1336 | // "import" 將使此配置繼承自指定文件。 1337 | // "env" 如果存在,將用系統環境填充。 1338 | ``` 1339 | 1340 | - 以大寫字母開頭的單行,不包含標點符號(括號和逗號除外),並且後面跟著另一個段落,會被格式化為標題: 1341 | 1342 | ```go 1343 | // 較佳: 1344 | // The following line is formatted as a heading. 1345 | // 1346 | // Using headings 1347 | // 1348 | // Headings come with autogenerated anchor tags for easy linking. 1349 | ``` 1350 | 1351 | [格式化文檔]: https://go.dev/doc/comment 1352 | [可運行的示例]: decisions.md#examples 1353 | 1354 | 1355 | 1356 | ### 信號增強 1357 | 1358 | 有時一行代碼看起來像是常見的東西,但實際上不是。最好的例子之一是 `err == nil` 檢查(因為 `err != nil` 更常見)。以下兩個條件檢查很難區分: 1359 | 1360 | ```go 1361 | // 較佳: 1362 | if err := doSomething(); err != nil { 1363 | // ... 1364 | } 1365 | ``` 1366 | 1367 | ```go 1368 | // 不佳: 1369 | if err := doSomething(); err == nil { 1370 | // ... 1371 | } 1372 | ``` 1373 | 1374 | 你可以通過添加註釋來“增強”條件的信號: 1375 | 1376 | ```go 1377 | // 較佳: 1378 | if err := doSomething(); err == nil { // if NO error 1379 | // ... 1380 | } 1381 | ``` 1382 | 1383 | 這條註釋引起了對條件差異的注意。 1384 | 1385 | 1386 | 1387 | ## 變數宣告 1388 | 1389 | 1390 | 1391 | ### 初始化 1392 | 1393 | 為了一致性,當用非零值初始化新變數時,優先使用 `:=` 而不是 `var`。 1394 | 1395 | ```go 1396 | // 較佳: 1397 | i := 42 1398 | ``` 1399 | 1400 | ```go 1401 | // 不佳: 1402 | var i = 42 1403 | ``` 1404 | 1405 | 1406 | 1407 | ### 非指針零值 1408 | 1409 | 以下宣告使用[零值]: 1410 | 1411 | ```go 1412 | // 較佳: 1413 | var ( 1414 | coords Point 1415 | magic [4]byte 1416 | primes []int 1417 | ) 1418 | ``` 1419 | 1420 | [零值]: https://golang.org/ref/spec#The_zero_value 1421 | 1422 | 當你想傳達一個**已準備好供以後使用**的空值時,應該使用零值來宣告值。使用帶有顯式初始化的複合文字可能會很笨拙: 1423 | 1424 | ```go 1425 | // 不佳: 1426 | var ( 1427 | coords = Point{X: 0, Y: 0} 1428 | magic = [4]byte{0, 0, 0, 0} 1429 | primes = []int(nil) 1430 | ) 1431 | ``` 1432 | 1433 | 零值宣告的一個常見應用是在解組時用作變數的輸出: 1434 | 1435 | ```go 1436 | // 較佳: 1437 | var coords Point 1438 | if err := json.Unmarshal(data, &coords); err != nil { 1439 | ``` 1440 | 1441 | 如果你需要在結構中使用鎖或其他[不能被複製](decisions.md#copying)的字段,你可以將其設為值類型以利用零值初始化。這意味著包含該字段的類型現在必須通過指針而不是值來傳遞。該類型的方法必須使用指針接收者。 1442 | 1443 | ```go 1444 | // 較佳: 1445 | type Counter struct { 1446 | // This field does not have to be "*sync.Mutex". However, 1447 | // users must now pass *Counter objects between themselves, not Counter. 1448 | mu sync.Mutex 1449 | data map[string]int64 1450 | } 1451 | 1452 | // Note this must be a pointer receiver to prevent copying. 1453 | func (c *Counter) IncrementBy(name string, n int64) 1454 | ``` 1455 | 1456 | 對於包含不可複製字段的複合類型(如結構和數組)的局部變量,使用值類型是可以接受的。然而,如果該複合類型由函數返回,或者所有對它的訪問最終都需要取地址,則應該從一開始就將變量聲明為指針類型。同樣,protobufs 應該聲明為指針類型。 1457 | 1458 | ```go 1459 | // 較佳: 1460 | func NewCounter(name string) *Counter { 1461 | c := new(Counter) // "&Counter{}" is also fine. 1462 | registerCounter(name, c) 1463 | return c 1464 | } 1465 | 1466 | var myMsg = new(pb.Bar) // or "&pb.Bar{}". 1467 | ``` 1468 | 1469 | 這是因為 `*pb.Something` 滿足 [`proto.Message`] 而 `pb.Something` 則不滿足。 1470 | 1471 | ```go 1472 | // 不佳: 1473 | func NewCounter(name string) *Counter { 1474 | var c Counter 1475 | registerCounter(name, &c) 1476 | return &c 1477 | } 1478 | 1479 | var myMsg = pb.Bar{} 1480 | ``` 1481 | 1482 | > **重要:** 映射類型必須在修改之前顯式初始化。然而,從零值映射中讀取是完全可以的。 1483 | > 1484 | > 對於映射和切片類型,如果代碼對性能特別敏感並且你提前知道大小,請參見[大小提示](#vardeclsize)部分。 1485 | 1486 | 1487 | 1488 | ### 複合文字 1489 | 1490 | 以下是[複合文字]宣告: 1491 | 1492 | ```go 1493 | // 較佳: 1494 | var ( 1495 | coords = Point{X: x, Y: y} 1496 | magic = [4]byte{'I', 'W', 'A', 'D'} 1497 | primes = []int{2, 3, 5, 7, 11} 1498 | captains = map[string]string{"Kirk": "James Tiberius", "Picard": "Jean-Luc"} 1499 | ) 1500 | ``` 1501 | 1502 | 當你知道初始元素或成員時,應該使用複合文字來宣告值。 1503 | 1504 | 相比之下,使用複合文字來宣告空值或無成員值,與[零值初始化](#vardeclzero)相比,可能會顯得視覺上雜亂。 1505 | 1506 | 當你需要一個指向零值的指針時,你有兩個選擇:空的複合文字和 `new`。兩者都可以,但 `new` 關鍵字可以提醒讀者,如果需要非零值,複合文字將不起作用: 1507 | 1508 | ```go 1509 | // 較佳: 1510 | var ( 1511 | buf = new(bytes.Buffer) // non-empty Buffers are initialized with constructors. 1512 | msg = new(pb.Message) // non-empty proto messages are initialized with builders or by setting fields one by one. 1513 | ) 1514 | ``` 1515 | 1516 | [複合文字]: https://golang.org/ref/spec#Composite_literals 1517 | 1518 | 1519 | 1520 | ### 大小提示 1521 | 1522 | 以下是利用大小提示來預分配容量的宣告: 1523 | 1524 | ```go 1525 | // 較佳: 1526 | var ( 1527 | // Preferred buffer size for target filesystem: st_blksize. 1528 | buf = make([]byte, 131072) 1529 | // Typically process up to 8-10 elements per run (16 is a safe assumption). 1530 | q = make([]Node, 0, 16) 1531 | // Each shard processes shardSize (typically 32000+) elements. 1532 | seen = make(map[string]bool, shardSize) 1533 | ) 1534 | ``` 1535 | 1536 | 大小提示和預分配是重要的步驟**當與代碼及其集成的實證分析結合使用時**,可以創建性能敏感和資源高效的代碼。 1537 | 1538 | 大多數代碼不需要大小提示或預分配,可以允許運行時根據需要增長切片或映射。當最終大小已知時(例如在映射和切片之間轉換時),預分配是可以接受的,但這不是可讀性的要求,在小情況下可能不值得這樣做。 1539 | 1540 | **警告:** 預分配超過所需的內存可能會浪費內存,甚至損害性能。如果有疑問,請參見[GoTip #3: Benchmarking Go Code],並默認使用[零初始化](#vardeclzero)或[複合文字宣告](#vardeclcomposite)。 1541 | 1542 | [GoTip #3: Benchmarking Go Code]: https://google.github.io/styleguide/go/index.html#gotip 1543 | 1544 | 1545 | 1546 | ### 通道方向 1547 | 1548 | 儘可能指定[通道方向]。 1549 | 1550 | ```go 1551 | // 較佳: 1552 | // sum computes the sum of all of the values. It reads from the channel until 1553 | // the channel is closed. 1554 | func sum(values <-chan int) int { 1555 | // ... 1556 | } 1557 | ``` 1558 | 1559 | 這可以防止在沒有指定的情況下可能發生的隨意編程錯誤: 1560 | 1561 | ```go 1562 | // 不佳: 1563 | func sum(values chan int) (out int) { 1564 | for v := range values { 1565 | out += v 1566 | } 1567 | // values must already be closed for this code to be reachable, which means 1568 | // a second close triggers a panic. 1569 | close(values) 1570 | } 1571 | ``` 1572 | 1573 | 當指定方向時,編譯器會捕捉到這樣的簡單錯誤。 1574 | 它還有助於傳達對類型的一定所有權。 1575 | 1576 | 另請參見 Bryan Mills 的演講 "Rethinking Classical Concurrency Patterns": 1577 | [slides][rethinking-concurrency-slides] [video][rethinking-concurrency-video]. 1578 | 1579 | [rethinking-concurrency-slides]: https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view?usp=sharing 1580 | [rethinking-concurrency-video]: https://www.youtube.com/watch?v=5zXAHh5tJqQ 1581 | [通道方向]: https://go.dev/ref/spec#Channel_types 1582 | 1583 | 1584 | 1585 | ## 函數參數列表 1586 | 1587 | 不要讓函數的簽名變得太長。隨著函數中參數的增加,單個參數的角色變得不那麼明確,相同類型的相鄰參數更容易混淆。參數數量多的函數不易記住,並且在調用時更難閱讀。 1588 | 1589 | 在設計 API 時,考慮將簽名變得複雜的高度可配置函數拆分為幾個更簡單的函數。如果需要,這些函數可以共享一個(未導出)實現。 1590 | 1591 | 當函數需要許多輸入時,考慮為某些參數引入[選項結構](#option-structure)或採用更高級的[可變參數選項](#variadic-options)技術。選擇哪種策略的主要考慮因素應該是函數調用在所有預期用例中的外觀。 1592 | 1593 | 以下建議主要適用於導出的 API,這些 API 被要求達到比未導出的 API 更高的標準。這些技術對於你的用例可能是不必要的。使用你的判斷,並平衡[清晰性](guide.md#clarity)和[最少機制](guide.md#least-mechanism)的原則。 1594 | 1595 | 另請參見: 1596 | [Go Tip #24: Use Case-Specific Constructions](https://google.github.io/styleguide/go/index.html#gotip) 1597 | 1598 | 1599 | 1600 | ### 選項結構 1601 | 1602 | 選項結構是一種結構類型,它收集函數或方法的一些或全部參數,然後作為最後一個參數傳遞給函數或方法。(只有在導出函數中使用時,該結構才應導出。) 1603 | 1604 | 使用選項結構有很多好處: 1605 | 1606 | - 結構字面量包括每個參數的字段和值,這使它們具有自我記錄功能且不易混淆。 1607 | - 不相關或“默認”的字段可以省略。 1608 | - 調用者可以共享選項結構並編寫助手來操作它。 1609 | - 結構比函數參數提供更清晰的每字段文檔。 1610 | - 選項結構可以隨時間增長而不影響調用點。 1611 | 1612 | 這是一個可以改進的函數示例: 1613 | 1614 | ```go 1615 | // 不佳: 1616 | func EnableReplication(ctx context.Context, config *replicator.Config, primaryRegions, readonlyRegions []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) { 1617 | // ... 1618 | } 1619 | ``` 1620 | 1621 | 上面的函數可以使用選項結構重寫如下: 1622 | 1623 | ```go 1624 | // 較佳: 1625 | type ReplicationOptions struct { 1626 | Config *replicator.Config 1627 | PrimaryRegions []string 1628 | ReadonlyRegions []string 1629 | ReplicateExisting bool 1630 | OverwritePolicies bool 1631 | ReplicationInterval time.Duration 1632 | CopyWorkers int 1633 | HealthWatcher health.Watcher 1634 | } 1635 | 1636 | func EnableReplication(ctx context.Context, opts ReplicationOptions) { 1637 | // ... 1638 | } 1639 | ``` 1640 | 1641 | 然後可以在不同的包中調用該函數: 1642 | 1643 | ```go 1644 | // 較佳: 1645 | func foo(ctx context.Context) { 1646 | // Complex call: 1647 | storage.EnableReplication(ctx, storage.ReplicationOptions{ 1648 | Config: config, 1649 | PrimaryRegions: []string{"us-east1", "us-central2", "us-west3"}, 1650 | ReadonlyRegions: []string{"us-east5", "us-central6"}, 1651 | OverwritePolicies: true, 1652 | ReplicationInterval: 1 * time.Hour, 1653 | CopyWorkers: 100, 1654 | HealthWatcher: watcher, 1655 | }) 1656 | 1657 | // Simple call: 1658 | storage.EnableReplication(ctx, storage.ReplicationOptions{ 1659 | Config: config, 1660 | PrimaryRegions: []string{"us-east1", "us-central2", "us-west3"}, 1661 | }) 1662 | } 1663 | ``` 1664 | 1665 | **注意:** [上下文從不包含在選項結構中](decisions.md#contexts)。 1666 | 1667 | 當以下某些情況適用時,通常首選此選項: 1668 | 1669 | - 所有調用者都需要指定一個或多個選項。 1670 | - 許多調用者需要提供許多選項。 1671 | - 選項在用戶將調用的多個函數之間共享。 1672 | 1673 | 1674 | 1675 | ### 可變參數選項 1676 | 1677 | 使用可變參數選項,創建導出函數,這些函數返回可以傳遞給函數的[可變參數 (`...`) 參數](https://golang.org/ref/spec#Passing_arguments_to_..._parameters)的閉包。該函數將選項的值(如果有)作為其參數,並且返回的閉包接受一個可變引用(通常是指向結構類型的指針),該引用將根據輸入進行更新。 1678 | 1679 | 使用可變參數選項可以提供許多好處: 1680 | 1681 | - 當不需要配置時,選項在調用點不佔用空間。 1682 | - 選項仍然是值,因此調用者可以共享它們、編寫助手並累積它們。 1683 | - 選項可以接受多個參數(例如 `cartesian.Translate(dx, dy int) TransformOption`)。 1684 | - 選項函數可以返回一個命名類型,以在 godoc 中將選項組合在一起。 1685 | - 包可以允許(或防止)第三方包定義(或防止定義)自己的選項。 1686 | 1687 | **注意:** 使用可變參數選項需要大量額外的代碼(請參見以下示例),因此僅在優勢超過開銷時使用。 1688 | 1689 | 這是一個可以改進的函數示例: 1690 | 1691 | ```go 1692 | // 不佳: 1693 | func EnableReplication(ctx context.Context, config *placer.Config, primaryCells, readonlyCells []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) { 1694 | ... 1695 | } 1696 | ``` 1697 | 1698 | 上面的例子可以使用可變參數選項重寫如下: 1699 | 1700 | ```go 1701 | // 較佳: 1702 | type replicationOptions struct { 1703 | readonlyCells []string 1704 | replicateExisting bool 1705 | overwritePolicies bool 1706 | replicationInterval time.Duration 1707 | copyWorkers int 1708 | healthWatcher health.Watcher 1709 | } 1710 | 1711 | // A ReplicationOption configures EnableReplication. 1712 | type ReplicationOption func(*replicationOptions) 1713 | 1714 | // ReadonlyCells adds additional cells that should additionally 1715 | // contain read-only replicas of the data. 1716 | // 1717 | // Passing this option multiple times will add additional 1718 | // read-only cells. 1719 | // 1720 | // Default: none 1721 | func ReadonlyCells(cells ...string) ReplicationOption { 1722 | return func(opts *replicationOptions) { 1723 | opts.readonlyCells = append(opts.readonlyCells, cells...) 1724 | } 1725 | } 1726 | 1727 | // ReplicateExisting controls whether files that already exist in the 1728 | // primary cells will be replicated. Otherwise, only newly-added 1729 | // files will be candidates for replication. 1730 | // 1731 | // Passing this option again will overwrite earlier values. 1732 | // 1733 | // Default: false 1734 | func ReplicateExisting(enabled bool) ReplicationOption { 1735 | return func(opts *replicationOptions) { 1736 | opts.replicateExisting = enabled 1737 | } 1738 | } 1739 | 1740 | // ... other options ... 1741 | 1742 | // DefaultReplicationOptions control the default values before 1743 | // applying options passed to EnableReplication. 1744 | var DefaultReplicationOptions = []ReplicationOption{ 1745 | OverwritePolicies(true), 1746 | ReplicationInterval(12 * time.Hour), 1747 | CopyWorkers(10), 1748 | } 1749 | 1750 | func EnableReplication(ctx context.Context, config *placer.Config, primaryCells []string, opts ...ReplicationOption) { 1751 | var options replicationOptions 1752 | for _, opt := range DefaultReplicationOptions { 1753 | opt(&options) 1754 | } 1755 | for _, opt := range opts { 1756 | opt(&options) 1757 | } 1758 | } 1759 | ``` 1760 | 1761 | 然後可以在不同的包中調用該函數: 1762 | 1763 | ```go 1764 | // 較佳: 1765 | func foo(ctx context.Context) { 1766 | // Complex call: 1767 | storage.EnableReplication(ctx, config, []string{"po", "is", "ea"}, 1768 | storage.ReadonlyCells("ix", "gg"), 1769 | storage.OverwritePolicies(true), 1770 | storage.ReplicationInterval(1*time.Hour), 1771 | storage.CopyWorkers(100), 1772 | storage.HealthWatcher(watcher), 1773 | ) 1774 | 1775 | // Simple call: 1776 | storage.EnableReplication(ctx, config, []string{"po", "is", "ea"}) 1777 | } 1778 | ``` 1779 | 1780 | 當以下許多情況適用時,請優先選擇此選項: 1781 | 1782 | - 大多數調用者不需要指定任何選項。 1783 | - 大多數選項很少使用。 1784 | - 有大量選項。 1785 | - 選項需要參數。 1786 | - 選項可能會失敗或設置不正確(在這種情況下,選項函數返回 `error`)。 1787 | - 選項需要大量文檔,這些文檔很難適合結構中。 1788 | - 用戶或其他包可以提供自定義選項。 1789 | 1790 | 這種風格的選項應該接受參數,而不是使用存在來表示它們的值;後者會使參數的動態組合變得更加困難。例如,二進制設置應該接受布爾值(例如 `rpc.FailFast(enable bool)` 比 `rpc.EnableFailFast()` 更可取)。枚舉選項應該接受枚舉常量(例如 `log.Format(log.Capacitor)` 比 `log.CapacitorFormat()` 更可取)。替代方法使得必須以編程方式選擇要傳遞的選項的用戶更加困難;這些用戶被迫更改參數的實際組合,而不是簡單地更改選項的參數。不要假設所有用戶都會靜態地知道所有選項集。 1791 | 1792 | 一般來說,選項應按順序處理。如果存在衝突或非累積選項被多次傳遞,則最後一個參數應獲勝。 1793 | 1794 | 在此模式中,選項函數的參數通常不導出,以限制選項僅在包內部定義。這是一個很好的默認設置,儘管有時允許其他包定義選項是合適的。 1795 | 1796 | 請參見 [Rob Pike's original blog post] 和 [Dave Cheney's talk] 以更深入地了解如何使用這些選項。 1797 | 1798 | [Rob Pike's original blog post]: http://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html 1799 | [Dave Cheney's talk]: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis 1800 | 1801 | 1802 | 1803 | ## 複雜的命令行介面 1804 | 1805 | 一些程序希望向用戶提供豐富的命令行介面,包括子命令。例如,`kubectl create`、`kubectl run` 和許多其他子命令都是由程序 `kubectl` 提供的。至少有以下常用庫可以實現這一點。 1806 | 1807 | 如果你沒有偏好或其他考慮因素相同,建議使用 [subcommands],因為它是最簡單且易於正確使用的。然而,如果你需要它不提供的不同功能,請選擇其他選項之一。 1808 | 1809 | - **[cobra]** 1810 | 1811 | - Flag convention: getopt 1812 | - 在 Google 代碼庫外部常見。 1813 | - 許多額外功能。 1814 | - 使用中的陷阱(見下文)。 1815 | 1816 | - **[subcommands]** 1817 | 1818 | - Flag convention: Go 1819 | - 簡單且易於正確使用。 1820 | - 如果不需要額外功能,建議使用。 1821 | 1822 | **警告**: cobra 命令函數應使用 `cmd.Context()` 獲取上下文,而不是使用 `context.Background` 創建自己的根上下文。使用 subcommands 包的代碼已經作為函數參數接收到正確的上下文。 1823 | 1824 | 你不需要將每個子命令放在單獨的包中,通常也沒有必要這樣做。應用與任何 Go 代碼庫中相同的包邊界考慮。如果你的代碼既可以作為庫使用,也可以作為二進制文件使用,通常將 CLI 代碼和庫分開是有益的,使 CLI 成為其客戶端之一。(這並不是特定於具有子命令的 CLI,但在這裡提到是因為這是一個常見的情況。) 1825 | 1826 | [subcommands]: https://pkg.go.dev/github.com/google/subcommands 1827 | [cobra]: https://pkg.go.dev/github.com/spf13/cobra 1828 | 1829 | 1830 | 1831 | ## 測試 1832 | 1833 | 1834 | 1835 | ### 測試留給 `Test` 函數 1836 | 1837 | 1841 | 1842 | Go 區分了“測試助手”和“斷言助手”: 1843 | 1844 | - **測試助手** 是執行設置或清理任務的函數。所有在測試助手中發生的失敗都預期是環境的失敗(而不是被測代碼的失敗)——例如,當測試數據庫無法啟動,因為這台機器上沒有更多的可用端口。對於這樣的函數,調用 `t.Helper` 是合適的,以[將它們標記為測試助手](decisions.md#mark-test-helpers)。有關更多詳細信息,請參見[測試助手中的錯誤處理](#test-helper-error-handling)。 1845 | 1846 | - **斷言助手** 是檢查系統正確性並在期望未滿足時使測試失敗的函數。在 Go 中,[斷言助手不被認為是慣用的](decisions.md#assert)。 1847 | 1848 | 測試的目的是報告被測代碼的通過/失敗條件。理想的測試失敗位置是在 `Test` 函數內,因為這確保了[失敗消息](decisions.md#useful-test-failures)和測試邏輯是清晰的。 1849 | 1850 | 隨著測試代碼的增長,可能需要將一些功能分解到單獨的函數中。標準的軟體工程考量仍然適用,因為 _測試代碼仍然是代碼_。如果功能不與測試框架交互,那麼所有通常的規則都適用。然而,當公共代碼與框架交互時,必須小心避免常見的陷阱,這些陷阱可能導致無信息的失敗消息和難以維護的測試。 1851 | 1852 | 如果許多單獨的測試案例需要相同的驗證邏輯,請按以下方式之一安排測試,而不是使用斷言助手或複雜的驗證函數: 1853 | 1854 | - 在 `Test` 函數中內聯邏輯(包括驗證和失敗),即使它是重複的。這在簡單情況下效果最好。 1855 | - 如果輸入相似,考慮將它們統一到 [表驅動測試] 中,同時在循環中內聯邏輯。這有助於避免重複,同時保持驗證和失敗在 `Test` 中。 1856 | - 如果有多個調用者需要相同的驗證函數,但表測試不適用(通常是因為輸入不夠簡單或驗證是操作序列的一部分),請安排驗證函數返回一個值(通常是 `error`),而不是接受 `testing.T` 參數並使用它來使測試失敗。在 `Test` 中使用邏輯來決定是否失敗,並提供 [有用的測試失敗]。您還可以創建測試助手來分解公共樣板設置代碼。 1857 | 1858 | 最後一點中概述的設計保持了正交性。例如,[package `cmp`] 並不是為了使測試失敗而設計的,而是為了比較(和差異)值。因此,它不需要知道進行比較的上下文,因為調用者可以提供這些上下文。如果您的公共測試代碼為您的數據類型提供 `cmp.Transformer`,這通常是最簡單的設計。對於其他驗證,考慮返回一個 `error` 值。 1859 | 1860 | ```go 1861 | // 較佳: 1862 | // polygonCmp returns a cmp.Option that equates s2 geometry objects up to 1863 | // some small floating-point error. 1864 | func polygonCmp() cmp.Option { 1865 | return cmp.Options{ 1866 | cmp.Transformer("polygon", func(p *s2.Polygon) []*s2.Loop { return p.Loops() }), 1867 | cmp.Transformer("loop", func(l *s2.Loop) []s2.Point { return l.Vertices() }), 1868 | cmpopts.EquateApprox(0.00000001, 0), 1869 | cmpopts.EquateEmpty(), 1870 | } 1871 | } 1872 | 1873 | func TestFenceposts(t *testing.T) { 1874 | // This is a test for a fictional function, Fenceposts, which draws a fence 1875 | // around some Place object. The details are not important, except that 1876 | // the result is some object that has s2 geometry (github.com/golang/geo/s2) 1877 | got := Fencepost(tomsDiner, 1*meter) 1878 | if diff := cmp.Diff(want, got, polygonCmp()); diff != "" { 1879 | t.Errorf("Fencepost(tomsDiner, 1m) returned unexpected diff (-want+got):\n%v", diff) 1880 | } 1881 | } 1882 | 1883 | func FuzzFencepost(f *testing.F) { 1884 | // Fuzz test (https://go.dev/doc/fuzz) for the same. 1885 | 1886 | f.Add(tomsDiner, 1*meter) 1887 | f.Add(school, 3*meter) 1888 | 1889 | f.Fuzz(func(t *testing.T, geo Place, padding Length) { 1890 | got := Fencepost(geo, padding) 1891 | // Simple reference implementation: not used in prod, but easy to 1892 | // reason about and therefore useful to check against in random tests. 1893 | reference := slowFencepost(geo, padding) 1894 | 1895 | // In the fuzz test, inputs and outputs can be large so don't 1896 | // bother with printing a diff. cmp.Equal is enough. 1897 | if !cmp.Equal(got, reference, polygonCmp()) { 1898 | t.Errorf("Fencepost returned wrong placement") 1899 | } 1900 | }) 1901 | } 1902 | ``` 1903 | 1904 | `polygonCmp` 函數對於如何被調用是不可知的;它不接受具體的輸入類型,也不會處理兩個對象不匹配的情況。因此,更多的調用者可以使用它。 1905 | 1906 | **注意:** 測試助手和普通庫代碼之間有類比。庫中的代碼通常[不應該 panic],除非在極少數情況下;從測試中調用的代碼不應該停止測試,除非 [繼續進行沒有意義]。 1907 | 1908 | [表驅動測試]: decisions.md#table-driven-tests 1909 | [有用的測試失敗]: decisions.md#useful-test-failures 1910 | [package `cmp`]: https://pkg.go.dev/github.com/google/go-cmp/cmp 1911 | [不應該 panic]: decisions.md#dont-panic 1912 | [繼續進行沒有意義]: #t-fatal 1913 | 1914 | 1915 | 1916 | ### 設計可擴展的驗證 API 1917 | 1918 | 大部分風格指南中關於測試的建議都是針對測試您自己的程式碼。本節介紹如何提供機制,讓其他人測試其編寫的程式碼,以確保其符合您的函式庫要求。 1919 | 1920 | 1921 | 1922 | #### 驗收測試 1923 | 1924 | 這類測試被稱為 [驗收測試]。此類測試的前提在於,使用者並不瞭解測試中發生的每個細節;他們僅僅將輸入交由測試機制來處理。這可以被視為一種 [控制反轉]。 1925 | 1926 | 在典型的 Go 測試中,測試函式控制程式流程,而 [no assert](decisions.md#assert) 以及 [test functions](#test-functions) 的指導方針鼓勵您保持這種模式。本節解釋如何以與 Go 風格一致的方式撰寫這些測試的支援程式碼。 1927 | 1928 | 在深入探討如何實現之前,請參考下方摘錄自 [`io/fs`] 的範例: 1929 | 1930 | ```go 1931 | type FS interface { 1932 | Open(name string) (File, error) 1933 | } 1934 | ``` 1935 | 1936 | 儘管有眾所周知的 `fs.FS` 實現,Go 開發者仍可能被期望自行編寫一個。為了幫助驗證使用者所實作的 `fs.FS` 是否正確,在 [`testing/fstest`] 中提供了一個名為 [`fstest.TestFS`] 的通用函式庫。此 API 將該實作當作黑盒處理,以確保其遵守 `io/fs` 合約中最基本的部分。 1937 | 1938 | [驗收測試]: https://en.wikipedia.org/wiki/Acceptance_testing 1939 | [控制反轉]: https://en.wikipedia.org/wiki/Inversion_of_control 1940 | [`io/fs`]: https://pkg.go.dev/io/fs 1941 | [`testing/fstest`]: https://pkg.go.dev/testing/fstest 1942 | [`fstest.TestFS`]: https://pkg.go.dev/testing/fstest#TestFS 1943 | 1944 | 1945 | 1946 | #### 撰寫驗收測試 1947 | 1948 | 既然我們知道什麼是驗收測試以及使用它的原因,接下來讓我們探討如何為 `package chess` 建立驗收測試,此 package 用於模擬棋局。`chess` 的使用者預期會實作 `chess.Player` 介面。這些實作是我們主要要驗證的部分。我們的驗收測試關注的是玩家實作是否做出合法的行棋,而非行棋是否聰明。 1949 | 1950 | 1. 建立一個用於驗證行為的新 package,[依慣例命名](#naming-doubles-helper-package)方式是在原 package 名稱後加上 `test` 一詞(例如,`chesstest`)。 1951 | 1952 | 1. 建立進行驗證的函式,該函式透過接收被測實作作為參數並執行其操作: 1953 | 1954 | ```go 1955 | // ExercisePlayer tests a Player implementation in a single turn on a board. 1956 | // The board itself is spot checked for sensibility and correctness. 1957 | // 1958 | // It returns a nil error if the player makes a correct move in the context 1959 | // of the provided board. Otherwise ExercisePlayer returns one of this 1960 | // package's errors to indicate how and why the player failed the 1961 | // validation. 1962 | func ExercisePlayer(b *chess.Board, p chess.Player) error 1963 | ``` 1964 | 1965 | 測試應記錄哪些不變量被破壞以及如何破壞。您的設計可以在兩種失敗報告方式中選擇: 1966 | 1967 | - **Fail fast**: 一旦實作違反了不變量,立即返回錯誤。 1968 | 1969 | 這是最簡單的方法,如果驗收測試預期能夠快速執行,這種方法運作得很好。在這裡可以輕易地使用簡單的錯誤[標記]和[自定義類型],這反過來又使測試驗收測試變得容易。 1970 | 1971 | ```go 1972 | for color, army := range b.Armies { 1973 | // The king should never leave the board, because the game ends at 1974 | // checkmate. 1975 | if army.King == nil { 1976 | return &MissingPieceError{Color: color, Piece: chess.King} 1977 | } 1978 | } 1979 | ``` 1980 | 1981 | - **Aggregate all failures**: 收集所有失敗案例,並全部回報。 1982 | 1983 | 此方法類似於 feel 中的 [keep going](decisions.md#keep-going) 指導原則,如果預期驗收測試執行緩慢,則可能較為適用。 1984 | 1985 | 您如何聚合失敗情況,應取決於您是否希望讓使用者或您自己能夠查詢各別失敗(例如,供您測試驗收測試)。下方示範了如何使用一個[自定義錯誤型別][custom types]來[聚合錯誤]: 1986 | 1987 | ```go 1988 | var badMoves []error 1989 | 1990 | move := p.Move() 1991 | if putsOwnKingIntoCheck(b, move) { 1992 | badMoves = append(badMoves, PutsSelfIntoCheckError{Move: move}) 1993 | } 1994 | 1995 | if len(badMoves) > 0 { 1996 | return SimulationError{BadMoves: badMoves} 1997 | } 1998 | return nil 1999 | ``` 2000 | 2001 | 驗收測試應遵循 [keep going](decisions.md#keep-going) 指導原則,除非測試檢測到正在執行的系統中的不變量被破壞,否則不要調用 `t.Fatal`。 2002 | 2003 | 例如,通常將 `t.Fatal` 保留用於特殊情況,例如 [設定失敗](#test-helper-error-handling): 2004 | 2005 | ```go 2006 | func ExerciseGame(t *testing.T, cfg *Config, p chess.Player) error { 2007 | t.Helper() 2008 | 2009 | if cfg.Simulation == Modem { 2010 | conn, err := modempool.Allocate() 2011 | if err != nil { 2012 | t.Fatalf("No modem for the opponent could be provisioned: %v", err) 2013 | } 2014 | t.Cleanup(func() { modempool.Return(conn) }) 2015 | } 2016 | // Run acceptance test (a whole game). 2017 | } 2018 | ``` 2019 | 2020 | 此技術可幫助您建立簡潔且具代表性的驗證。但請勿試圖利用此技術來繞過[assertions 的指導方針](decisions.md#assert). 2021 | 2022 | 最終呈現給最終用戶的產品應該類似如下形式: 2023 | 2024 | ```go 2025 | // 較佳: 2026 | package deepblue_test 2027 | 2028 | import ( 2029 | "chesstest" 2030 | "deepblue" 2031 | ) 2032 | 2033 | func TestAcceptance(t *testing.T) { 2034 | player := deepblue.New() 2035 | err := chesstest.ExerciseGame(t, chesstest.SimpleGame, player) 2036 | if err != nil { 2037 | t.Errorf("Deep Blue player failed acceptance test: %v", err) 2038 | } 2039 | } 2040 | ``` 2041 | 2042 | [標記]: https://google.github.io/styleguide/go/index.html#gotip 2043 | [custom types]: https://google.github.io/styleguide/go/index.html#gotip 2044 | [聚合錯誤]: https://google.github.io/styleguide/go/index.html#gotip 2045 | 2046 | 2047 | 2048 | ### 使用真實傳輸 2049 | 2050 | 在測試組件集成時,尤其是使用 HTTP 或 RPC 作為組件之間的底層傳輸時,應優先使用真實的底層傳輸來連接到後端的測試版本。 2051 | 2052 | 例如,假設您要測試的代碼(有時稱為“被測系統”或 SUT)與實現 [long running operations] API 的後端交互。要測試您的 SUT,請使用連接到 [OperationsServer] 的 [test double](https://abseil.io/resources/swe-book/html/ch13.html#basic_concepts)(例如,模擬、存根或假冒)的真實 [OperationsClient]。 2053 | 2054 | [test double]: https://abseil.io/resources/swe-book/html/ch13.html#basic_concepts 2055 | [long running operations]: https://pkg.go.dev/google.golang.org/genproto/googleapis/longrunning 2056 | [OperationsClient]: https://pkg.go.dev/google.golang.org/genproto/googleapis/longrunning#OperationsClient 2057 | [OperationsServer]: https://pkg.go.dev/google.golang.org/genproto/googleapis/longrunning#OperationsServer 2058 | 2059 | 這比手動實現客戶端更推薦,因為正確模仿客戶端行為的複雜性。通過使用具有測試特定服務器的生產客戶端,您可以確保您的測試使用盡可能多的真實代碼。 2060 | 2061 | **提示:** 在可能的情況下,使用被測服務的作者提供的測試庫。 2062 | 2063 | 2064 | 2065 | ### `t.Error` vs. `t.Fatal` 2066 | 2067 | 如[決策](decisions.md#keep-going)中所述,測試通常不應在遇到第一個問題時中止。 2068 | 2069 | 然而,有些情況要求測試不能繼續進行。當某些測試設置失敗時,特別是在[測試設置助手](#test-helper-error-handling)中,調用 `t.Fatal` 是合適的,否則您無法運行其餘的測試。在表驅動測試中,`t.Fatal` 適用於在測試循環之前設置整個測試函數的失敗。影響測試表中單個條目的失敗,使得無法繼續該條目的測試,應報告如下: 2070 | 2071 | - 如果您沒有使用 `t.Run` 子測試,請使用 `t.Error`,然後使用 `continue` 語句繼續下一個表條目。 2072 | - 如果您正在使用子測試(並且在調用 `t.Run` 內部),請使用 `t.Fatal`,這將結束當前的子測試並允許您的測試用例進入下一個子測試。 2073 | 2074 | **警告:** 調用 `t.Fatal` 和類似函數並不總是安全的。[更多詳細信息在這裡](#t-fatal-goroutine)。 2075 | 2076 | 2077 | 2078 | ### 測試助手中的錯誤處理 2079 | 2080 | **注意:** 本節討論 Go 使用術語[測試助手](decisions.md#mark-test-helpers)的意義:執行測試設置和清理的函數,而不是常見的斷言工具。更多討論請參見[測試函數](#test-functions)部分。 2081 | 2082 | 測試助手執行的操作有時會失敗。例如,設置包含文件的目錄涉及 I/O,這可能會失敗。當測試助手失敗時,通常意味著測試無法繼續,因為設置前提條件失敗。當這種情況發生時,請優先調用助手中的 `Fatal` 函數: 2083 | 2084 | ```go 2085 | // 較佳: 2086 | func mustAddGameAssets(t *testing.T, dir string) { 2087 | t.Helper() 2088 | if err := os.WriteFile(path.Join(dir, "pak0.pak"), pak0, 0644); err != nil { 2089 | t.Fatalf("Setup failed: could not write pak0 asset: %v", err) 2090 | } 2091 | if err := os.WriteFile(path.Join(dir, "pak1.pak"), pak1, 0644); err != nil { 2092 | t.Fatalf("Setup failed: could not write pak1 asset: %v", err) 2093 | } 2094 | } 2095 | ``` 2096 | 2097 | 這保持了調用方的清潔性,而不是讓助手將錯誤返回給測試本身: 2098 | 2099 | ```go 2100 | // 不佳: 2101 | func addGameAssets(t *testing.T, dir string) error { 2102 | t.Helper() 2103 | if err := os.WriteFile(path.Join(d, "pak0.pak"), pak0, 0644); err != nil { 2104 | return err 2105 | } 2106 | if err := os.WriteFile(path.Join(d, "pak1.pak"), pak1, 0644); err != nil { 2107 | return err 2108 | } 2109 | return nil 2110 | } 2111 | ``` 2112 | 2113 | **警告:** 調用 `t.Fatal` 和類似函數並不總是安全的。[更多詳細信息在這裡](#t-fatal-goroutine)。 2114 | 2115 | 失敗消息應包括發生了什麼的描述。這很重要,因為您可能會向許多用戶提供測試 API,特別是隨著助手中產生錯誤的步驟數量增加。當測試失敗時,用戶應該知道在哪裡以及為什麼失敗。 2116 | 2117 | **提示:** Go 1.14 引入了一個 [`t.Cleanup`] 函數,可以用來註冊在測試完成時運行的清理函數。該函數也適用於測試助手。請參見 [GoTip #4: Cleaning Up Your Tests](https://google.github.io/styleguide/go/index.html#gotip) 以獲取有關簡化測試助手的指導。 2118 | 2119 | 下面在名為 `paint_test.go` 的虛構文件中的代碼片段演示了 `(*testing.T).Helper` 如何影響 Go 測試中的失敗報告: 2120 | 2121 | ```go 2122 | package paint_test 2123 | 2124 | import ( 2125 | "fmt" 2126 | "testing" 2127 | ) 2128 | 2129 | func paint(color string) error { 2130 | return fmt.Errorf("no %q paint today", color) 2131 | } 2132 | 2133 | func badSetup(t *testing.T) { 2134 | // This should call t.Helper, but doesn't. 2135 | if err := paint("taupe"); err != nil { 2136 | t.Fatalf("Could not paint the house under test: %v", err) // line 15 2137 | } 2138 | } 2139 | 2140 | func mustGoodSetup(t *testing.T) { 2141 | t.Helper() 2142 | if err := paint("lilac"); err != nil { 2143 | t.Fatalf("Could not paint the house under test: %v", err) 2144 | } 2145 | } 2146 | 2147 | func TestBad(t *testing.T) { 2148 | badSetup(t) 2149 | // ... 2150 | } 2151 | 2152 | func TestGood(t *testing.T) { 2153 | mustGoodSetup(t) // line 32 2154 | // ... 2155 | } 2156 | ``` 2157 | 2158 | 以下是運行時此輸出的示例。請注意突出顯示的文本及其不同之處: 2159 | 2160 | ```text 2161 | === RUN TestBad 2162 | paint_test.go:15: Could not paint the house under test: no "taupe" paint today 2163 | --- FAIL: TestBad (0.00s) 2164 | === RUN TestGood 2165 | paint_test.go:32: Could not paint the house under test: no "lilac" paint today 2166 | --- FAIL: TestGood (0.00s) 2167 | FAIL 2168 | ``` 2169 | 2170 | `paint_test.go:15` 的錯誤指的是 `badSetup` 中設置函數失敗的那一行: 2171 | 2172 | `t.Fatalf("Could not paint the house under test: %v", err)` 2173 | 2174 | 而 `paint_test.go:32` 指的是 `TestGood` 中測試失敗的那一行: 2175 | 2176 | `goodSetup(t)` 2177 | 2178 | 正確使用 `(*testing.T).Helper` 在以下情況下能更好地標示失敗的位置: 2179 | 2180 | - 幫助函數增長 2181 | - 幫助函數調用其他幫助函數 2182 | - 測試函數中幫助函數的使用量增長 2183 | 2184 | **提示:** 如果幫助函數調用 `(*testing.T).Error` 或 `(*testing.T).Fatal`,請在格式字符串中提供一些上下文,以幫助確定出了什麼問題以及為什麼。 2185 | 2186 | **提示:** 如果幫助函數所做的任何事情都不會導致測試失敗,則不需要調用 `t.Helper`。通過從函數參數列表中刪除 `t` 來簡化其簽名。 2187 | 2188 | [`t.Cleanup`]: https://pkg.go.dev/testing#T.Cleanup 2189 | 2190 | 2191 | 2192 | ### 不要從單獨的 goroutine 調用 `t.Fatal` 2193 | 2194 | 如[在 testing 包中記錄的](https://pkg.go.dev/testing#T),從運行 Test 函數(或子測試)的 goroutine 之外的任何 goroutine 調用 `t.FailNow`、`t.Fatal` 等都是不正確的。如果您的測試啟動了新的 goroutine,它們不得從這些 goroutine 內部調用這些函數。 2195 | 2196 | [測試助手](#test-functions) 通常不會從新的 goroutine 發出失敗信號,因此它們可以使用 `t.Fatal`。如果有疑問,請調用 `t.Error` 並返回。 2197 | 2198 | ```go 2199 | // 較佳: 2200 | func TestRevEngine(t *testing.T) { 2201 | engine, err := Start() 2202 | if err != nil { 2203 | t.Fatalf("Engine failed to start: %v", err) 2204 | } 2205 | 2206 | num := 11 2207 | var wg sync.WaitGroup 2208 | wg.Add(num) 2209 | for i := 0; i < num; i++ { 2210 | go func() { 2211 | defer wg.Done() 2212 | if err := engine.Vroom(); err != nil { 2213 | // This cannot be t.Fatalf. 2214 | t.Errorf("No vroom left on engine: %v", err) 2215 | return 2216 | } 2217 | if rpm := engine.Tachometer(); rpm > 1e6 { 2218 | t.Errorf("Inconceivable engine rate: %d", rpm) 2219 | } 2220 | }() 2221 | } 2222 | wg.Wait() 2223 | 2224 | if seen := engine.NumVrooms(); seen != num { 2225 | t.Errorf("engine.NumVrooms() = %d, want %d", seen, num) 2226 | } 2227 | } 2228 | ``` 2229 | 2230 | 在測試或子測試中添加 `t.Parallel` 並不會使調用 `t.Fatal` 變得不安全。 2231 | 2232 | 當所有對 `testing` API 的調用都在[測試函數](#test-functions)中時,通常很容易發現不正確的用法,因為 `go` 關鍵字很明顯。傳遞 `testing.T` 參數會使跟踪這種用法變得更加困難。通常,傳遞這些參數的原因是引入測試助手,而這些助手不應依賴於被測系統。因此,如果測試助手[註冊致命測試失敗](#test-helper-error-handling),它可以並且應該從測試的 goroutine 中這樣做。 2233 | 2234 | 2235 | 2236 | ### 在結構體字面量中使用字段名稱 2237 | 2238 | 在表驅動測試中,初始化測試案例結構體字面量時,最好指定字段名稱。當測試案例覆蓋大量垂直空間(例如超過 20-30 行)、當有相同類型的相鄰字段以及當您希望省略具有零值的字段時,這是很有幫助的。例如: 2239 | 2240 | ```go 2241 | // 較佳: 2242 | func TestStrJoin(t *testing.T) { 2243 | tests := []struct { 2244 | slice []string 2245 | separator string 2246 | skipEmpty bool 2247 | want string 2248 | }{ 2249 | { 2250 | slice: []string{"a", "b", ""}, 2251 | separator: ",", 2252 | want: "a,b,", 2253 | }, 2254 | { 2255 | slice: []string{"a", "b", ""}, 2256 | separator: ",", 2257 | skipEmpty: true, 2258 | want: "a,b", 2259 | }, 2260 | // ... 2261 | } 2262 | // ... 2263 | } 2264 | ``` 2265 | 2266 | 2267 | 2268 | ### 將設置代碼限定在特定測試範圍內 2269 | 2270 | 在可能的情況下,資源和依賴項的設置應盡可能限定在特定測試案例的範圍內。例如,給定一個設置函數: 2271 | 2272 | ```go 2273 | // mustLoadDataSet loads a data set for the tests. 2274 | // 2275 | // This example is very simple and easy to read. Often realistic setup is more 2276 | // complex, error-prone, and potentially slow. 2277 | func mustLoadDataset(t *testing.T) []byte { 2278 | t.Helper() 2279 | data, err := os.ReadFile("path/to/your/project/testdata/dataset") 2280 | 2281 | if err != nil { 2282 | t.Fatalf("Could not load dataset: %v", err) 2283 | } 2284 | return data 2285 | } 2286 | ``` 2287 | 2288 | 在需要的測試函數中顯式調用 `mustLoadDataset`: 2289 | 2290 | ```go 2291 | // 較佳: 2292 | func TestParseData(t *testing.T) { 2293 | data := mustLoadDataset(t) 2294 | parsed, err := ParseData(data) 2295 | if err != nil { 2296 | t.Fatalf("Unexpected error parsing data: %v", err) 2297 | } 2298 | want := &DataTable{ /* ... */ } 2299 | if got := parsed; !cmp.Equal(got, want) { 2300 | t.Errorf("ParseData(data) = %v, want %v", got, want) 2301 | } 2302 | } 2303 | 2304 | func TestListContents(t *testing.T) { 2305 | data := mustLoadDataset(t) 2306 | contents, err := ListContents(data) 2307 | if err != nil { 2308 | t.Fatalf("Unexpected error listing contents: %v", err) 2309 | } 2310 | want := []string{ /* ... */ } 2311 | if got := contents; !cmp.Equal(got, want) { 2312 | t.Errorf("ListContents(data) = %v, want %v", got, want) 2313 | } 2314 | } 2315 | 2316 | func TestRegression682831(t *testing.T) { 2317 | if got, want := guessOS("zpc79.example.com"), "grhat"; got != want { 2318 | t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want) 2319 | } 2320 | } 2321 | ``` 2322 | 2323 | 測試函數 `TestRegression682831` 不使用數據集,因此不調用 `mustLoadDataset`,這可能會很慢且容易出錯: 2324 | 2325 | ```go 2326 | // 不佳: 2327 | var dataset []byte 2328 | 2329 | func TestParseData(t *testing.T) { 2330 | // As documented above without calling mustLoadDataset directly. 2331 | } 2332 | 2333 | func TestListContents(t *testing.T) { 2334 | // As documented above without calling mustLoadDataset directly. 2335 | } 2336 | 2337 | func TestRegression682831(t *testing.T) { 2338 | if got, want := guessOS("zpc79.example.com"), "grhat"; got != want { 2339 | t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want) 2340 | } 2341 | } 2342 | 2343 | func init() { 2344 | dataset = mustLoadDataset() 2345 | } 2346 | ``` 2347 | 2348 | 用戶可能希望單獨運行某個函數,不應因這些因素而受到影響: 2349 | 2350 | ```shell 2351 | # No reason for this to perform the expensive initialization. 2352 | $ go test -run TestRegression682831 2353 | ``` 2354 | 2355 | 2356 | 2357 | ### 何時使用自定義 `TestMain` 入口點 2358 | 2359 | 如果**包中的所有測試**都需要共同的設置,並且**設置需要拆卸**,你可以使用[自定義 testmain 入口點]。這種情況可能發生在測試案例所需的資源設置特別昂貴,並且成本應該被攤銷的時候。通常在這種情況下,你已經從測試套件中提取了任何不相關的測試。它通常僅用於[功能測試]。 2360 | 2361 | 由於正確使用需要大量的注意,自定義 `TestMain` **不應該是你的首選**。首先考慮[攤銷共同測試設置]部分中的解決方案或普通的[測試助手]是否足夠滿足你的需求。 2362 | 2363 | [自定義 testmain 入口點]: https://golang.org/pkg/testing/#hdr-Main 2364 | [功能測試]: https://en.wikipedia.org/wiki/Functional_testing 2365 | [攤銷共同測試設置]: #t-setup-amortization 2366 | [測試助手]: #t-common-setup-scope 2367 | 2368 | ```go 2369 | // 較佳: 2370 | var db *sql.DB 2371 | 2372 | func TestInsert(t *testing.T) { /* omitted */ } 2373 | 2374 | func TestSelect(t *testing.T) { /* omitted */ } 2375 | 2376 | func TestUpdate(t *testing.T) { /* omitted */ } 2377 | 2378 | func TestDelete(t *testing.T) { /* omitted */ } 2379 | 2380 | // runMain sets up the test dependencies and eventually executes the tests. 2381 | // It is defined as a separate function to enable the setup stages to clearly 2382 | // defer their teardown steps. 2383 | func runMain(ctx context.Context, m *testing.M) (code int, err error) { 2384 | ctx, cancel := context.WithCancel(ctx) 2385 | defer cancel() 2386 | 2387 | d, err := setupDatabase(ctx) 2388 | if err != nil { 2389 | return 0, err 2390 | } 2391 | defer d.Close() // Expressly clean up database. 2392 | db = d // db is defined as a package-level variable. 2393 | 2394 | // m.Run() executes the regular, user-defined test functions. 2395 | // Any defer statements that have been made will be run after m.Run() 2396 | // completes. 2397 | return m.Run(), nil 2398 | } 2399 | 2400 | func TestMain(m *testing.M) { 2401 | code, err := runMain(context.Background(), m) 2402 | if err != nil { 2403 | // Failure messages should be written to STDERR, which log.Fatal uses. 2404 | log.Fatal(err) 2405 | } 2406 | // NOTE: defer statements do not run past here due to os.Exit 2407 | // terminating the process. 2408 | os.Exit(code) 2409 | } 2410 | ``` 2411 | 2412 | 理想情況下,一個測試案例在自身調用之間以及與其他測試案例之間應該是獨立的。 2413 | 2414 | 至少,確保個別測試案例在修改了任何全局狀態後,能夠重置這些狀態(例如,如果測試正在與外部數據庫交互)。 2415 | 2416 | 2417 | 2418 | #### 攤銷共同測試設置 2419 | 2420 | 如果以下所有條件都適用於共同設置,則使用 `sync.Once` 可能是合適的,但不是必需的: 2421 | 2422 | - 它是昂貴的。 2423 | - 它僅適用於某些測試。 2424 | - 它不需要拆卸。 2425 | 2426 | ```go 2427 | // 較佳: 2428 | var dataset struct { 2429 | once sync.Once 2430 | data []byte 2431 | err error 2432 | } 2433 | 2434 | func mustLoadDataset(t *testing.T) []byte { 2435 | t.Helper() 2436 | dataset.once.Do(func() { 2437 | data, err := os.ReadFile("path/to/your/project/testdata/dataset") 2438 | // dataset is defined as a package-level variable. 2439 | dataset.data = data 2440 | dataset.err = err 2441 | }) 2442 | if err := dataset.err; err != nil { 2443 | t.Fatalf("Could not load dataset: %v", err) 2444 | } 2445 | return dataset.data 2446 | } 2447 | ``` 2448 | 2449 | 當 `mustLoadDataset` 在多個測試函數中使用時,其成本被攤銷: 2450 | 2451 | ```go 2452 | // 較佳: 2453 | func TestParseData(t *testing.T) { 2454 | data := mustLoadDataset(t) 2455 | 2456 | // As documented above. 2457 | } 2458 | 2459 | func TestListContents(t *testing.T) { 2460 | data := mustLoadDataset(t) 2461 | 2462 | // As documented above. 2463 | } 2464 | 2465 | func TestRegression682831(t *testing.T) { 2466 | if got, want := guessOS("zpc79.example.com"), "grhat"; got != want { 2467 | t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want) 2468 | } 2469 | } 2470 | ``` 2471 | 2472 | 共同拆卸之所以棘手,是因為沒有統一的地方來註冊清理程序。如果設置函數(在這種情況下是 `loadDataset`)依賴於上下文,`sync.Once` 可能會有問題。這是因為兩個競爭調用設置函數中的第二個調用需要等待第一個調用完成後才能返回。這段等待時間不能輕易地遵守上下文的取消。 2473 | 2474 | 2475 | 2476 | ## 字串連接 2477 | 2478 | 在 Go 語言中,有幾種方法可以連接字串。以下是一些例子: 2479 | 2480 | - The "+" operator 2481 | - `fmt.Sprintf` 2482 | - `strings.Builder` 2483 | - `text/template` 2484 | - `safehtml/template` 2485 | 2486 | 雖然沒有一刀切的規則來選擇哪種方法,但以下指導方針概述了每種方法的優先使用情況。 2487 | 2488 | 2489 | 2490 | ### 簡單情況下優先使用 "+" 2491 | 2492 | 當連接少量字串時,優先使用 "+". 這種方法在語法上是最簡單的,且不需要導入任何包。 2493 | 2494 | ```go 2495 | // 較佳: 2496 | key := "projectid: " + p 2497 | ``` 2498 | 2499 | 2500 | 2501 | ### 格式化時優先使用 `fmt.Sprintf` 2502 | 2503 | 格式化複雜字串時,優先使用 `fmt.Sprintf`。使用多個 "+" 操作符可能會使最終結果變得模糊。 2504 | 2505 | ```go 2506 | // 較佳: 2507 | str := fmt.Sprintf("%s [%s:%d]-> %s", src, qos, mtu, dst) 2508 | ``` 2509 | 2510 | ```go 2511 | // 不佳: 2512 | bad := src.String() + " [" + qos.String() + ":" + strconv.Itoa(mtu) + "]-> " + dst.String() 2513 | ``` 2514 | 2515 | **最佳實踐:** 當字串構建操作的輸出是 `io.Writer` 時,不要僅僅為了將其發送到 Writer 而構建臨時字串。相反,應直接使用 `fmt.Fprintf` 將其輸出到 Writer。 2516 | 2517 | 當格式化更為複雜時,根據需要優先使用 [`text/template`] 或 [`safehtml/template`]。 2518 | 2519 | [`text/template`]: https://pkg.go.dev/text/template 2520 | [`safehtml/template`]: https://pkg.go.dev/github.com/google/safehtml/template 2521 | 2522 | 2523 | 2524 | ### 偏好使用 `strings.Builder` 來逐步構建字串 2525 | 2526 | 偏好在逐步構建字串時使用 `strings.Builder`。 2527 | `strings.Builder` 需要攤銷線性時間,而 "+" 和 `fmt.Sprintf` 2528 | 在順序調用以形成更大的字串時需要二次時間。 2529 | 2530 | ```go 2531 | // 較佳: 2532 | b := new(strings.Builder) 2533 | for i, d := range digitsOfPi { 2534 | fmt.Fprintf(b, "the %d digit of pi is: %d\n", i, d) 2535 | } 2536 | str := b.String() 2537 | ``` 2538 | 2539 | **注意:** 更多討論請參見 2540 | [GoTip #29: Building Strings Efficiently](https://google.github.io/styleguide/go/index.html#gotip)。 2541 | 2542 | 2543 | 2544 | ### 常量字串 2545 | 2546 | 偏好在構建常量、多行字串字面量時使用反引號 (\`)。 2547 | 2548 | ```go 2549 | // 較佳: 2550 | usage := `Usage: 2551 | 2552 | custom_tool [args]` 2553 | ``` 2554 | 2555 | ```go 2556 | // 不佳: 2557 | usage := "" + 2558 | "Usage:\n" + 2559 | "\n" + 2560 | "custom_tool [args]" 2561 | ``` 2562 | 2563 | 2566 | 2567 | {% endraw %} 2568 | 2569 | 2570 | 2571 | ## 全域狀態 2572 | 2573 | 庫不應迫使用戶使用依賴[全域狀態](https://en.wikipedia.org/wiki/Global_variable)的 API。建議庫不要公開控制所有用戶行為的 API 或導出[封裝層級](https://go.dev/ref/spec#TopLevelDecl)變數作為其 API 的一部分。本節餘下部分使用「全域」和「封裝層級狀態」兩個術語互換使用。 2574 | 2575 | 相反地,如果您的功能需要維護狀態,請允許您的用戶建立和使用實例值。 2576 | 2577 | **重要:** 雖然此指引適用於所有開發人員,但對於提供庫、整合與服務給其他團隊的基礎設施供應商來說,此指引尤為重要。 2578 | 2579 | ```go 2580 | // 較佳: 2581 | // Package sidecar manages subprocesses that provide features for applications. 2582 | package sidecar 2583 | 2584 | type Registry struct { plugins map[string]*Plugin } 2585 | 2586 | func New() *Registry { return &Registry{plugins: make(map[string]*Plugin)} } 2587 | 2588 | func (r *Registry) Register(name string, p *Plugin) error { ... } 2589 | ``` 2590 | 2591 | 用戶將實例化所需的數據(即一個 `*sidecar.Registry`),然後將它作為明確的依賴傳遞: 2592 | 2593 | ```go 2594 | // 較佳: 2595 | package main 2596 | 2597 | func main() { 2598 | sidecars := sidecar.New() 2599 | if err := sidecars.Register("Cloud Logger", cloudlogger.New()); err != nil { 2600 | log.Exitf("could not setup cloud logger: %v", err) 2601 | } 2602 | cfg := &myapp.Config{Sidecars: sidecars} 2603 | myapp.Run(context.Background(), cfg) 2604 | } 2605 | ``` 2606 | 2607 | 有多種方法可以將現有代碼遷移以支援依賴傳遞。您將主要使用的方法是將依賴作為參數傳遞給構造函數、函數、方法或呼叫鏈上的結構字段。 2608 | 2609 | 另見: 2610 | 2611 | - [Go Tip #5: Slimming Your Client Libraries](https://google.github.io/styleguide/go/index.html#gotip) 2612 | - [Go Tip #24: Use Case-Specific Constructions](https://google.github.io/styleguide/go/index.html#gotip) 2613 | - [Go Tip #40: Improving Time Testability with Function Parameters](https://google.github.io/styleguide/go/index.html#gotip) 2614 | - [Go Tip #41: Identify Function Call Parameters](https://google.github.io/styleguide/go/index.html#gotip) 2615 | - [Go Tip #44: Improving Time Testability with Struct Fields](https://google.github.io/styleguide/go/index.html#gotip) 2616 | - [Go Tip #80: Dependency Injection Principles](https://google.github.io/styleguide/go/index.html#gotip) 2617 | 2618 | 不支援顯式依賴傳遞的 API 隨著客戶數量的增加而變得脆弱: 2619 | 2620 | ```go 2621 | // 不佳: 2622 | package sidecar 2623 | 2624 | var registry = make(map[string]*Plugin) 2625 | 2626 | func Register(name string, p *Plugin) error { /* registers plugin in registry */ } 2627 | ``` 2628 | 2629 | 考慮在測試中執行依賴雲端日誌 sidecar 傳遞的程式碼時會發生什麼情況。 2630 | 2631 | ```go 2632 | // 不佳: 2633 | package app 2634 | 2635 | import ( 2636 | "cloudlogger" 2637 | "sidecar" 2638 | "testing" 2639 | ) 2640 | 2641 | func TestEndToEnd(t *testing.T) { 2642 | // The system under test (SUT) relies on a sidecar for a production cloud 2643 | // logger already being registered. 2644 | ... // Exercise SUT and check invariants. 2645 | } 2646 | 2647 | func TestRegression_NetworkUnavailability(t *testing.T) { 2648 | // We had an outage because of a network partition that rendered the cloud 2649 | // logger inoperative, so we added a regression test to exercise the SUT with 2650 | // a test double that simulates network unavailability with the logger. 2651 | sidecar.Register("cloudlogger", cloudloggertest.UnavailableLogger) 2652 | ... // Exercise SUT and check invariants. 2653 | } 2654 | 2655 | func TestRegression_InvalidUser(t *testing.T) { 2656 | // The system under test (SUT) relies on a sidecar for a production cloud 2657 | // logger already being registered. 2658 | // 2659 | // Oops. cloudloggertest.UnavailableLogger is still registered from the 2660 | // previous test. 2661 | ... // Exercise SUT and check invariants. 2662 | } 2663 | ``` 2664 | 2665 | Go 測試預設是按順序執行的,因此上述測試的運行順序為: 2666 | 2667 | 1. `TestEndToEnd` 2668 | 2. `TestRegression_NetworkUnavailability`,這會覆蓋 cloudlogger 的默認值 2669 | 3. `TestRegression_InvalidUser`,這需要在 `package sidecar` 中註冊的 cloudlogger 默認值 2670 | 2671 | 這會創建一個依賴順序的測試案例,這會破壞使用測試過濾器的運行,並防止測試並行運行或分片。 2672 | 2673 | 使用全局狀態會帶來一些難以解決的問題,對你和 API 的客戶來說: 2674 | 2675 | - 如果客戶需要在同一個進程空間中使用不同且獨立運行的 `Plugin` 集(例如,支持多個服務器),會發生什麼? 2676 | 2677 | - 如果客戶想在測試中用替代實現替換已註冊的 `Plugin`,例如 [測試替身],會發生什麼? 2678 | 2679 | 如果客戶的測試需要在 `Plugin` 實例之間或所有已註冊的插件之間保持密封性,會發生什麼? 2680 | 2681 | - 如果多個客戶在同一名稱下 `Register` 一個 `Plugin`,會發生什麼?哪一個會勝出,如果有的話? 2682 | 2683 | 應該如何 [處理錯誤](decisions.md#handle-errors)?如果代碼崩潰或調用 `log.Fatal`,這對於所有 API 被調用的地方是否總是 [合適](decisions.md#dont-panic)?客戶能否在這樣做之前驗證它不會做壞事? 2684 | 2685 | - 在程序的啟動階段或生命周期的某些階段是否有 `Register` 可以被調用的時候,或者不能被調用的時候? 2686 | 2687 | 如果在錯誤的時間調用 `Register` 會發生什麼?客戶可能在 [`func init`](https://go.dev/ref/spec#Package_initialization) 中調用 `Register`,在標誌解析之前,或在 `main` 之後。調用函數的階段會影響錯誤處理。如果 API 的作者假設 API _僅_ 在程序初始化期間被調用,而不要求它是,這個假設可能會促使作者設計錯誤處理來 [中止程序](best-practices#program-init),通過將 API 模型化為 `Must` 類函數。中止對於可以在任何階段使用的通用庫函數是不合適的。 2688 | 2689 | - 如果客戶和設計者的並發需求不匹配,會發生什麼? 2690 | 2691 | 另見: 2692 | 2693 | - [Go Tip #36: Enclosing Package-Level State](https://google.github.io/styleguide/go/index.html#gotip) 2694 | - [Go Tip #71: Reducing Parallel Test Flakiness](https://google.github.io/styleguide/go/index.html#gotip) 2695 | - [Go Tip #80: Dependency Injection Principles](https://google.github.io/styleguide/go/index.html#gotip) 2696 | - 錯誤處理: 2697 | [Look Before You Leap](https://docs.python.org/3/glossary.html#term-LBYL) 2698 | 與 2699 | [Easier to Ask for Forgiveness than Permission](https://docs.python.org/3/glossary.html#term-EAFP) 2700 | - [公共 API 的單元測試實踐] 2701 | 2702 | 全局狀態對 [Google 代碼庫的健康](guide.md#maintainability) 具有連鎖效應。應該以 **極端審慎** 的態度對待全局狀態。 2703 | 2704 | [全局狀態有多種形式](#globals-forms),你可以使用一些 [試金石來識別何時是安全的](#globals-litmus-tests)。 2705 | 2706 | [公共 API 的單元測試實踐]: index.md#unit-testing-practices 2707 | 2708 | 2709 | 2710 | ### 套件狀態 API 的主要形式 2711 | 2712 | 以下列舉幾種最常見的有問題的 API 形式: 2713 | 2714 | - 不論是否被導出的頂層變數。 2715 | 2716 | ```go 2717 | // 不佳: 2718 | package logger 2719 | 2720 | // Sinks manages the default output sources for this package's logging API. This 2721 | // variable should be set at package initialization time and never thereafter. 2722 | var Sinks []Sink 2723 | ``` 2724 | 2725 | 請參閱 [litmus tests (檢驗測試)](#globals-litmus-tests) 以了解何時這些情況是安全的。 2726 | 2727 | - [Service locator pattern (服務定位器模式)](https://en.wikipedia.org/wiki/Service_locator_pattern)。 2728 | 請參閱 [第一個範例](#globals)。問題並不在於服務定位器模式本身,而在於定位器被定義為全域。 2729 | 2730 | - 用於 [callbacks (回呼函數)]() 2731 | 以及類似行為的註冊表。 2732 | 2733 | ```go 2734 | // 不佳: 2735 | package health 2736 | 2737 | var unhealthyFuncs []func 2738 | 2739 | func OnUnhealthy(f func()) { 2740 | unhealthyFuncs = append(unhealthyFuncs, f) 2741 | } 2742 | ``` 2743 | 2744 | - 適用於後端、儲存、資料存取層及其他系統資源等項目的 Thick-Client 單例。這些通常會對服務可靠性產生額外問題。 2745 | 2746 | ```go 2747 | // 不佳: 2748 | package useradmin 2749 | 2750 | var client pb.UserAdminServiceClientInterface 2751 | 2752 | func Client() *pb.UserAdminServiceClient { 2753 | if client == nil { 2754 | client = ... // Set up client. 2755 | } 2756 | return client 2757 | } 2758 | ``` 2759 | 2760 | > **注意:** Google 程式碼庫中的許多舊有 API 並未遵循這項指導原則; 2761 | > 事實上,部分 Go 標準庫允許透過全域值進行配置。 2762 | > 儘管如此,舊有 API 違反此指導原則 2763 | > **[不應作為延續該模式的先例](guide.md#local-consistency)**。 2764 | > 2765 | > 今天投資於適當的 API 設計,比日後為重新設計而付出代價要好。 2766 | 2767 | 2768 | 2769 | ### 檢驗測試 2770 | 2771 | [使用上述模式的 API](#globals-forms) 在以下情況下是不安全的: 2772 | 2773 | - 儘管它們在其他方面相互獨立(例如,由不同的作者在完全不同的目錄中撰寫),但在相同程式中執行時,可能會有多個函數透過全域狀態相互作用。 2774 | - 獨立的測試案例透過全域狀態相互作用。 2775 | - API 的使用者可能會為了測試目的而交換或替換全域狀態,特別是以 [test double](例如存根、假物件、間諜或模擬物)方式替換狀態的任何部分。 2776 | - 使用者在與全域狀態互動時必須考慮特殊的順序需求:例如 `func init`,以及 flags 是否已解析等。 2777 | 2778 | 只要避免以上情況,這些 API 在 **少數有限的情況下是安全的**,也就是當下列任一條件成立時: 2779 | 2780 | - 全域狀態在邏輯上是常數 ([範例](https://github.com/klauspost/compress/blob/290f4cfacb3eff892555a491e3eeb569a48665e7/zstd/snappy.go#L413))。 2781 | - 套件可觀察的行為是無狀態的。例如,一個公開函數可能會使用私有的全域變數作為快取,但只要呼叫者無法區分快取命中與未命中,該函數就算無狀態。 2782 | - 全域狀態不會滲透到程式外部的事物,例如 sidecar 進程或共享檔案系統中的檔案。 2783 | - 不期望有可預測的行為 ([範例](https://pkg.go.dev/math/rand))。 2784 | 2785 | > **注意:** > [Sidecar processes](https://www.oreilly.com/library/view/designing-distributed-systems/9781491983638/ch02.html) 2786 | > 可能 **不** 嚴格局限於單一進程。它們可以且通常會被多個應用程式進程共享。此外,這些 sidecar 常常與外部分散式系統互動。 2787 | > 2788 | > 此外,上述基本考量之外,同樣的無狀態、冪等且局部的規則也適用於 sidecar 進程本身的程式碼! 2789 | 2790 | 其中一個安全情況的範例是 [`package image`](https://pkg.go.dev/image) 與其 [`image.RegisterFormat`](https://pkg.go.dev/image#RegisterFormat) 函數。請參考上述檢驗測試,應用於處理 [PNG](https://pkg.go.dev/image/png) 格式的典型解碼器: 2791 | 2792 | - 對於 `package image` 中使用已註冊解碼器的 API(例如 `image.Decode`)的多次呼叫不會互相干擾,測試亦然。唯一的例外是 `image.RegisterFormat`,但這已由下列要點緩解。 2793 | - 使用者極不可能會希望將解碼器替換成 [test double],因為 PNG 解碼器典範了我們程式庫偏好實體物件的情況。然而,如果解碼器以狀態性的方式與作業系統資源(例如網絡)相互作用,使用者更有可能會用測試替身來替換解碼器。 2794 | - 在註冊時可能會出現衝突,但在實際運作中可能罕見。 2795 | - 這些解碼器是無狀態、冪等且純粹的。 2796 | 2797 | 2798 | 2799 | ### 提供預設實例 2800 | 2801 | 雖然不推薦,但若需要最大化使用者便利性,可以提供使用套件層級狀態的簡化 API。 2802 | 2803 | 在這類情況下,請依據以下指導原則並遵循 [litmus tests](#globals-litmus-tests): 2804 | 2805 | 1. 套件必須提供客戶端能夠依據上述[描述](#globals-forms)建立獨立實例的能力。 2806 | 2. 使用全域狀態的公共 API 必須僅作為先前 API 的薄型代理。一個不錯的例子是 2807 | [`http.Handle`](https://pkg.go.dev/net/http#Handle) 在內部對套件變數 2808 | [`http.DefaultServeMux`](https://pkg.go.dev/net/http#DefaultServeMux) 呼叫 2809 | [`(*http.ServeMux).Handle`](https://pkg.go.dev/net/http#ServeMux.Handle)。 2810 | 3. 除非函式庫正在進行重構以支援依賴注入,否則此套件層級的 API 只應由 [binary build targets] 使用,而不應由 [libraries] 使用。可以被其他套件匯入的基礎設施函式庫不得依賴其匯入套件的套件層級狀態。 2811 | 2812 | 例如,一個實作 sidecar 的基礎設施提供者,如果預計以頂層 API 與其他團隊共享,應該提供相應的 API 以予支持: 2813 | 2814 | ```go 2815 | // 較佳: 2816 | package cloudlogger 2817 | 2818 | func New() *Logger { ... } 2819 | 2820 | func Register(r *sidecar.Registry, l *Logger) { 2821 | r.Register("Cloud Logging", l) 2822 | } 2823 | ``` 2824 | 2825 | 4. 此套件層級的 API 必須[文件化](#documentation-conventions)並且強制其不變性(例如,必須在程式執行的特定階段呼叫它,以及它是否可以被並行使用)。此外,它必須提供一個 API 來將全域狀態重置為已知的預設值(例如,以方便進行測試)。 2826 | 2827 | [binary build targets]: https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/rules.md#go_binary 2828 | [libraries]: https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/rules.md#go_library 2829 | 2830 | 參考資料: 2831 | 2832 | - [Go Tip #36: 包層級狀態的封閉](https://google.github.io/styleguide/go/index.html#gotip) 2833 | - [Go Tip #80: 依賴注入原則](https://google.github.io/styleguide/go/index.html#gotip) 2834 | -------------------------------------------------------------------------------- /decisions.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Go 風格決策 4 | 5 | (英文版) 6 | 7 | [概覽](index.md) | [指南](guide.md) | [決策](decisions.md) | 8 | [最佳實踐](best-practices.md) 9 | 10 | **注意:** 這是一系列概述 Google 的 [Go 風格](index.md) 文件的一部分。本文件是 **[規範性](index.md#normative) 但非 [典範性](index.md#canonical)**,並且從屬於 [核心風格指南](guide.md)。更多資訊請見[概覽](index.md#about)。 11 | 12 | 13 | 14 | ## 關於 15 | 16 | 本文件包含旨在統一並提供標準指導、解釋和範例的風格決策,供 Go 可讀性導師提供建議。 17 | 18 | 本文件**非詳盡無遺**,將隨著時間的推移而增長。在 [核心風格指南](guide) 與此處提供的建議相矛盾時,**風格指南優先**,並且本文件應相應更新。 19 | 20 | 查看 [概覽](https://google.github.io/styleguide/go#about) 以獲得完整的 Go 風格文件集。 21 | 22 | 以下部分已從風格決策移至指南的其他部分: 23 | 24 | - **混合大小寫**:見 [指南#混合大小寫](guide.md#mixed-caps) 25 | 26 | 27 | - **格式化**:見 [指南#格式化](guide.md#formatting) 28 | 29 | 30 | - **行長**:見 [指南#行長](guide.md#line-length) 31 | 32 | 33 | 34 | 35 | ## 命名 36 | 37 | 有關命名的總體指導,請參見 [核心風格指南](guide.md#naming) 中的命名部分。以下部分對命名的特定領域提供了進一步的澄清。 38 | 39 | 40 | 41 | ### 底線 42 | 43 | Go 中的名稱一般不應包含底線。此原則有三個例外: 44 | 45 | 1. 僅由生成的代碼導入的套件名稱可能包含底線。有關如何選擇多詞套件名稱的更多細節,請參見 [套件名稱](#package-names)。 46 | 1. `*_test.go` 文件中的測試、基準測試和示例函數名稱可能包含底線。 47 | 1. 與操作系統或 cgo 互操作的低級庫可能會重用標識符,如 [`syscall`] 所做的那樣。這在大多數代碼庫中預期非常罕見。 48 | 49 | [`syscall`]: https://pkg.go.dev/syscall#pkg-constants 50 | 51 | 52 | 53 | ### 套件名稱 54 | 55 | 56 | 57 | Go 套件名稱應該短且僅包含小寫字母。由多個單詞組成的套件名稱應保持不間斷的全小寫。例如,套件 [`tabwriter`] 不是命名為 `tabWriter`、`TabWriter` 或 `tab_writer`。 58 | 59 | 避免選擇可能被常用的局部變量名稱 [遮蔽] 的套件名稱。例如,`usercount` 是比 `count` 更好的套件名稱,因為 `count` 是常用的變量名稱。 60 | 61 | Go 套件名稱不應有底線。如果您需要導入一個包含底線的套件名稱(通常來自生成的或第三方代碼),則必須在導入時重命名為適合在 Go 代碼中使用的名稱。 62 | 63 | 此規則的例外是,僅由生成的代碼導入的套件名稱可能包含底線。具體例子包括: 64 | 65 | - 使用 `_test` 後綴的外部測試套件,例如集成測試 66 | 67 | - 使用 `_test` 後綴的 68 | [套件級文檔示例](https://go.dev/blog/examples) 69 | 70 | [`tabwriter`]: https://pkg.go.dev/text/tabwriter 71 | [遮蔽]: best-practices#shadowing 72 | 73 | 避免使用像 `util`、`utility`、`common`、`helper` 等無信息性的套件名稱。更多關於[所謂的 "工具套件"](best-practices#util-packages)。 74 | 75 | 當導入的套件被重命名時(例如 `import foopb "path/to/foo_go_proto"`),套件的本地名稱必須遵守上述規則,因為本地名稱決定了文件中如何引用該套件中的符號。如果在多個文件中重命名了給定的導入,特別是在相同或相鄰的套件中,應盡可能使用相同的本地名稱以保持一致性。 76 | 77 | 78 | 79 | 另見:[Go 博客關於套件名稱的文章](https://go.dev/blog/package-names)。 80 | 81 | 82 | 83 | ### 接收者名稱 84 | 85 | 86 | 87 | [接收者] 變量名稱必須是: 88 | 89 | - 短的(通常是一到兩個字母長) 90 | - 該類型本身的縮寫 91 | - 對該類型的每個接收者一致應用 92 | 93 | | Long Name | Better Name | 94 | | --------------------------- | ------------------------- | 95 | | `func (tray Tray)` | `func (t Tray)` | 96 | | `func (info *ResearchInfo)` | `func (ri *ResearchInfo)` | 97 | | `func (this *ReportWriter)` | `func (w *ReportWriter)` | 98 | | `func (self *Scanner)` | `func (s *Scanner)` | 99 | 100 | [接收者]: https://golang.org/ref/spec#Method_declarations 101 | 102 | 103 | 104 | ### 常量名稱 105 | 106 | 常量名稱必須使用 [混合大小寫],就像 Go 中的所有其他名稱一樣。([導出] 的常量以大寫開頭,而未導出的常量以小寫開頭。)即使這樣做打破了其他語言中的慣例也適用。常量名稱不應該是其值的衍生物,而應該解釋該值表示什麼。 107 | 108 | ```go 109 | // 好的: 110 | const MaxPacketSize = 512 111 | 112 | const ( 113 | ExecuteBit = 1 << iota 114 | WriteBit 115 | ReadBit 116 | ) 117 | ``` 118 | 119 | [混合大小寫]: guide.md#mixed-caps 120 | [導出]: https://tour.golang.org/basics/3 121 | 122 | 不要使用非混合大小寫的常量名稱或帶有 `K` 前綴的常量。 123 | 124 | ```go 125 | // 不佳: 126 | const MAX_PACKET_SIZE = 512 127 | const kMaxBufferSize = 1024 128 | const KMaxUsersPergroup = 500 129 | ``` 130 | 131 | 根據常量的角色命名,而不是它們的值。如果一個常量除了它的值之外沒有其他角色,那麼將它定義為常量是不必要的。 132 | 133 | ```go 134 | // 不好的範例 135 | const Twelve = 12 136 | 137 | const ( 138 | UserNameColumn = "username" 139 | GroupColumn = "group" 140 | ) 141 | ``` 142 | 143 | 144 | 145 | 146 | 147 | ### 首字母縮略詞 148 | 149 | 150 | 151 | 名稱中作為首字母縮略詞或縮寫的單詞(例如,`URL` 和 `NATO`)應該具有相同的大小寫。`URL` 應該出現為 `URL` 或 `url`(如在 `urlPony` 或 `URLPony` 中),永遠不應該是 `Url`。這也適用於 `ID` 當它是 "identifier" 的縮寫時;寫作 `appID` 而不是 `appId`。 152 | 153 | - 在包含多個首字母縮略詞的名稱中(例如 `XMLAPI` 因為它包含 `XML` 和 `API`),給定首字母縮略詞內的每個字母應該具有相同的大小寫,但名稱中的每個首字母縮略詞不需要具有相同的大小寫。 154 | - 在包含含有小寫字母的首字母縮略詞的名稱中(例如 `DDoS`、`iOS`、`gRPC`),首字母縮略詞應該出現如同在標準散文中一樣,除非您需要為了 [導出性] 改變第一個字母。在這些情況下,整個首字母縮略詞應該是相同的大小寫(例如 `ddos`、`IOS`、`GRPC`)。 155 | 156 | [導出性]: https://golang.org/ref/spec#Exported_identifiers 157 | 158 | 159 | 160 | | 首字母縮略詞 | 範圍 | 正確 | 錯誤 | 161 | | ------------ | ------ | -------- | -------------------------------------- | 162 | | XML API | 導出 | `XMLAPI` | `XmlApi`, `XMLApi`, `XmlAPI`, `XMLapi` | 163 | | XML API | 未導出 | `xmlAPI` | `xmlapi`, `xmlApi` | 164 | | iOS | 導出 | `IOS` | `Ios`, `IoS` | 165 | | iOS | 未導出 | `iOS` | `ios` | 166 | | gRPC | 導出 | `GRPC` | `Grpc` | 167 | | gRPC | 未導出 | `gRPC` | `grpc` | 168 | | DDoS | 導出 | `DDoS` | `DDOS`, `Ddos` | 169 | | DDoS | 未導出 | `ddos` | `dDoS`, `dDOS` | 170 | 171 | 172 | 173 | 174 | 175 | ### Getters 176 | 177 | 178 | 179 | 函數和方法名稱不應使用 `Get` 或 `get` 前綴,除非底層概念使用了 "get" 這個詞(例如一個 HTTP GET)。優先直接以名詞開頭命名,例如使用 `Counts` 而不是 `GetCounts`。 180 | 181 | 如果函數涉及執行複雜計算或執行遠程調用,可以使用 `Compute` 或 `Fetch` 等不同的詞代替 `Get`,以便讀者清楚地知道函數調用可能需要時間,並且可能會阻塞或失敗。 182 | 183 | 184 | 185 | 186 | 187 | ### 變量名稱 188 | 189 | 190 | 191 | 一般的經驗法則是,名稱的長度應該與其範圍的大小成正比,與它在該範圍內使用的次數成反比。在文件範圍創建的變量可能需要多個單詞,而在單個內部塊範圍內的變量可能只是一個單詞甚至只是一兩個字符,以保持代碼清晰並避免多餘的資訊。 192 | 193 | 這裡是一個大致的基線。這些數字指南不是嚴格的規則。基於上下文、[清晰性] 和 [簡練性] 應用判斷。 194 | 195 | - 小範圍是執行一兩個小操作的範圍,比如 1-7 行。 196 | - 中等範圍是幾個小操作或一個大操作,比如 8-15 行。 197 | - 大範圍是一個或幾個大操作,比如 15-25 行。 198 | - 非常大的範圍是跨越超過一頁的任何範圍(比如,超過 25 行)。 199 | 200 | [清晰性]: guide.md#clarity 201 | [簡練性]: guide.md#concision 202 | 203 | 在小範圍內可能非常清晰的名稱(例如,`c` 用於計數器)在更大的範圍內可能不夠,並且需要澄清以提醒讀者在代碼中更遠的地方其用途。在有許多變量的範圍內,或者變量代表相似的值或概念,可能需要比範圍建議的更長的變量名稱。 204 | 205 | 概念的具體性也可以幫助保持變量名稱的簡練。例如,假設只使用一個數據庫,一個短變量名稱如 `db` 通常可能保留給非常小的範圍,即使範圍非常大也可能保持完全清晰。在這種情況下,單詞 `database` 基於範圍的大小可能是可以接受的,但不是必需的,因為 `db` 是該單詞的非常常見的縮寫,幾乎沒有其他解釋。 206 | 207 | 局部變量的名稱應該反映它包含的內容以及它在當前上下文中的使用方式,而不是值的來源。例如,通常情況下,最好的局部變量名稱與結構或協議緩衝區字段名稱不同。 208 | 209 | 一般來說: 210 | 211 | - 單詞名稱如 `count` 或 `options` 是一個好的起點。 212 | - 可以添加額外的單詞來消除相似名稱的歧義,例如 `userCount` 和 `projectCount`。 213 | - 不要簡單地刪除字母來節省打字。例如,`Sandbox` 比 `Sbx` 更好,特別是對於導出的名稱。 214 | - 從大多數變量名稱中省略 [類型和類型化的詞]。 215 | - 對於一個數字,`userCount` 是一個比 `numUsers` 或 `usersInt` 更好的名稱。 216 | - 對於一個切片,`users` 是一個比 `userSlice` 更好的名稱。 217 | - 如果範圍內有兩個版本的值,可以接受包含類型化的限定詞,例如你可能有一個存儲在 `ageString` 中的輸入,並使用 `age` 作為解析後的值。 218 | - 省略從 [周圍上下文] 中清楚的詞。例如,在 `UserCount` 方法的實現中,一個叫做 `userCount` 的局部變量可能是多餘的;`count`、`users` 或甚至 `c` 一樣可讀。 219 | 220 | [類型和類型化的詞]: #repetitive-with-type 221 | [周圍上下文]: #repetitive-in-context 222 | 223 | 224 | 225 | #### 單字母變量名稱 226 | 227 | 單字母變量名稱可以是一個有用的工具來最小化[重複](#repetition),但也可能使代碼不必要地晦澀。將它們的使用限制在完整單詞是明顯的情況下,並且如果單字母變量代替完整單詞會重複。 228 | 229 | 一般來說: 230 | 231 | - 對於 [方法接收者變量],一個字母或兩個字母的名稱是首選。 232 | - 對於常見類型使用熟悉的變量名稱通常很有幫助: 233 | - `r` 用於 `io.Reader` 或 `*http.Request` 234 | - `w` 用於 `io.Writer` 或 `http.ResponseWriter` 235 | - 單字母標識符作為整數循環變量是可以接受的,特別是對於索引(例如,`i`)和坐標(例如,`x` 和 `y`)。 236 | - 當範圍短時,縮寫可以作為可接受的循環標識符,例如 `for _, n := range nodes { ... }`。 237 | 238 | [方法接收者變量]: #receiver-names 239 | 240 | 241 | 242 | ### 重複 243 | 244 | 249 | 250 | Go 源代碼應該避免不必要的重複。一個常見的來源是重複的名稱,這些名稱經常包含不必要的詞或重複它們的上下文或類型。如果相同或類似的代碼段在接近的位置多次出現,代碼本身也可能是不必要地重複的。 251 | 252 | 重複命名可以有多種形式,包括: 253 | 254 | 255 | 256 | #### 套件與導出符號名稱 257 | 258 | 在命名導出符號時,套件的名稱在套件外總是可見的,因此兩者之間的冗餘信息應該被減少或消除。如果一個套件只導出一種類型,且以套件本身命名,則構造函數的標準名稱為 `New`(如果需要的話)。 259 | 260 | > **範例:** 重複的名稱 -> 更好的名稱 261 | > 262 | > - `widget.NewWidget` -> `widget.New` 263 | > - `widget.NewWidgetWithName` -> `widget.NewWithName` 264 | > - `db.LoadFromDatabase` -> `db.Load` 265 | > - `goatteleportutil.CountGoatsTeleported` -> `gtutil.CountGoatsTeleported` 266 | > 或 `goatteleport.Count` 267 | > - `myteampb.MyTeamMethodRequest` -> `mtpb.MyTeamMethodRequest` 或 268 | > `myteampb.MethodRequest` 269 | 270 | 271 | 272 | #### 變量名稱與類型 273 | 274 | 編譯器總是知道一個變量的類型,在大多數情況下,讀者也能通過變量的使用方式清楚地知道變量的類型。只有在同一作用域內變量的值出現兩次時,才需要明確變量的類型。 275 | 276 | | 重複的名稱 | 更好的名稱 | 277 | | ----------------------------- | ---------------------- | 278 | | `var numUsers int` | `var users int` | 279 | | `var nameString string` | `var name string` | 280 | | `var primaryProject *Project` | `var primary *Project` | 281 | 282 | 如果值以多種形式出現,可以通過額外的單詞如 `raw` 和 `parsed` 或底層表示來進行說明: 283 | 284 | ```go 285 | // 較佳: 286 | limitStr := r.FormValue("limit") 287 | limit, err := strconv.Atoi(limitStr) 288 | ``` 289 | 290 | 291 | 292 | #### 外部上下文與本地名稱 293 | 294 | 包含來自其周圍上下文信息的名稱經常創造額外的噪音而沒有好處。套件名稱、方法名稱、類型名稱、函數名稱、導入路徑,甚至檔案名稱都可以提供自動資格所有內部名稱的上下文。 295 | 296 | ```go 297 | // 不佳: 298 | // 在套件 "ads/targeting/revenue/reporting" 299 | type AdsTargetingRevenueReport struct{} 300 | 301 | func (p *Project) ProjectName() string 302 | ``` 303 | 304 | ```go 305 | // 較佳: 306 | // 在套件 "ads/targeting/revenue/reporting" 307 | type Report struct{} 308 | 309 | func (p *Project) Name() string 310 | ``` 311 | 312 | ```go 313 | // 不佳: 314 | // 在套件 "sqldb" 315 | type DBConnection struct{} 316 | ``` 317 | 318 | ```go 319 | // 較佳: 320 | // 在套件 "sqldb" 321 | type Connection struct{} 322 | ``` 323 | 324 | ```go 325 | // 不佳: 326 | // 在套件 "ads/targeting" 327 | func Process(in *pb.FooProto) *Report { 328 | adsTargetingID := in.GetAdsTargetingID() 329 | } 330 | ``` 331 | 332 | ```go 333 | // 較佳: 334 | // 在套件 "ads/targeting" 335 | func Process(in *pb.FooProto) *Report { 336 | id := in.GetAdsTargetingID() 337 | } 338 | ``` 339 | 340 | 重複通常應該在使用符號的用戶的上下文中評估,而不是孤立地。例如,以下代碼有很多名稱在某些情況下可能是好的,但在上下文中是多餘的: 341 | 342 | ```go 343 | // 不佳: 344 | func (db *DB) UserCount() (userCount int, err error) { 345 | var userCountInt64 int64 346 | if dbLoadError := db.LoadFromDatabase("count(distinct users)", &userCountInt64); dbLoadError != nil { 347 | return 0, fmt.Errorf("failed to load user count: %s", dbLoadError) 348 | } 349 | userCount = int(userCountInt64) 350 | return userCount, nil 351 | } 352 | ``` 353 | 354 | 相反,從上下文或使用中清楚的名稱信息通常可以省略: 355 | 356 | ```go 357 | // 較佳: 358 | func (db *DB) UserCount() (int, error) { 359 | var count int64 360 | if err := db.Load("count(distinct users)", &count); err != nil { 361 | return 0, fmt.Errorf("failed to load user count: %s", err) 362 | } 363 | return int(count), nil 364 | } 365 | ``` 366 | 367 | 368 | 369 | ## 註解 370 | 371 | 關於註解的慣例(包括該評論什麼、使用什麼風格、如何提供可運行的範例等)旨在支持閱讀公共 API 文檔的體驗。有關更多信息,請參見 [Effective Go](http://golang.org/doc/effective_go.html#commentary)。 372 | 373 | 最佳實踐文件的 [文檔慣例] 部分進一步討論了這個問題。 374 | 375 | **最佳實踐:** 在開發和代碼審查期間使用 [doc preview],以查看文檔和可運行範例是否有用,並且是否按照您期望的方式呈現。 376 | 377 | **提示:** Godoc 使用的特殊格式化非常少;列表和代碼片段通常應該縮進以避免換行。除了縮進,通常應避免裝飾。 378 | 379 | [doc preview]: best-practices#documentation-preview 380 | [文檔慣例]: best-practices#documentation-conventions 381 | 382 | 383 | 384 | ### 註解行長 385 | 386 | 確保即使在窄屏幕上也能從源代碼中閱讀註解。 387 | 388 | 當註解太長時,建議將其拆分為多個單行註解。盡可能瞄準在 80 列寬的終端上閱讀良好的註解,但這不是一個硬性截止;Go 中沒有固定的註解行長限制。例如,標準庫通常選擇根據標點符號來斷開註解,這有時會使個別行接近 60-70 個字符標記。 389 | 390 | 有很多現有代碼中的註解超過了 80 個字符的長度。這個指南不應該被用作在可讀性審查中更改此類代碼的理由(參見 [一致性](guide.md#consistency)),儘管鼓勵團隊在其他重構的過程中抓住機會更新註解以遵循這一指南。這個指南的主要目標是確保所有 Go 可讀性導師在提出建議時做出相同的建議。 391 | 392 | 有關註解的更多信息,請參見 [The Go Blog 上的文章關於文檔]。 393 | 394 | [The Go Blog 上的文章關於文檔]: https://blog.golang.org/godoc-documenting-go-code 395 | 396 | ```text 397 | # 較佳: 398 | // 這是一段註解。 399 | // 個別行的長度在 Godoc 中並不重要; 400 | // 但選擇換行的方式使其在窄屏幕上易於閱讀。 401 | // 402 | // 不用太擔心長 URL: 403 | // https://supercalifragilisticexpialidocious.example.com:8080/Animalia/Chordata/Mammalia/Rodentia/Geomyoidea/Geomyidae/ 404 | // 405 | // 同樣,如果您有其他信息因過多的換行而變得尷尬, 406 | // 請運用您的判斷,如果包含長行有助於而不是妨礙,則包含它。 407 | ``` 408 | 409 | 避免會在小屏幕上反复換行的註解,這會導致糟糕的閱讀體驗。 410 | 411 | ```text 412 | # 不佳: 413 | // 這是一段註解。個別行的長度在 Godoc 中並不重要; 414 | // 但選擇換行的方式會在窄屏幕或代碼審查中造成參差不齊的行, 415 | // 這可能很煩人,尤其是在一個會反复換行的註解塊中。 416 | // 417 | // 不用太擔心長 URL: 418 | // https://supercalifragilisticexpialidocious.example.com:8080/Animalia/Chordata/Mammalia/Rodentia/Geomyoidea/Geomyidae/ 419 | ``` 420 | 421 | 422 | 423 | ### 文件註解 424 | 425 | 426 | 427 | 所有頂層導出名稱必須有文件註解,未導出的類型或函數聲明如果行為或含義不明顯也應該有。這些註解應該是以被描述對象的名稱開頭的[完整句子]。名稱前可以加上冠詞("a", "an", "the")使其讀起來更自然。 428 | 429 | ```go 430 | // 較佳: 431 | // A Request represents a request to run a command. 432 | type Request struct { ... 433 | 434 | // Encode writes the JSON encoding of req to w. 435 | func Encode(w io.Writer, req *Request) { ... 436 | ``` 437 | 438 | 文件註解出現在 [Godoc](https://pkg.go.dev/) 中,並且會被 IDE 展示,因此應該為使用套件的任何人編寫。 439 | 440 | [完整句子]: #comment-sentences 441 | 442 | 文件註解適用於以下符號,或者如果它出現在結構體中則適用於字段群。 443 | 444 | ```go 445 | // 較佳: 446 | // Options configure the group management service. 447 | type Options struct { 448 | // General setup: 449 | Name string 450 | Group *FooGroup 451 | 452 | // Dependencies: 453 | DB *sql.DB 454 | 455 | // Customization: 456 | LargeGroupThreshold int // optional; default: 10 457 | MinimumMembers int // optional; default: 2 458 | } 459 | ``` 460 | 461 | **最佳實踐:** 如果您對未導出的代碼有文件註解,請遵循與其被導出時相同的習慣(即,註解以未導出的名稱開頭)。這樣通過簡單替換註解和代碼中的未導出名稱與新導出的名稱,就容易將其後來導出。 462 | 463 | 464 | 465 | ### 註解句子 466 | 467 | 468 | 469 | 完整句子的註解應該像標準英語句子一樣使用大寫並加上標點符號。(作為一個例外,如果其他方面清楚,以未大寫的標識符名稱開始一個句子是可以接受的。這種情況最好只在段落的開頭進行。) 470 | 471 | 句子片段的註解對於標點符號或大寫沒有這樣的要求。 472 | 473 | [文件註解] 應該總是完整句子,因此應該總是使用大寫和標點符號。簡單的行尾註解(特別是對於結構體字段)可以是假設字段名是主題的簡單短語。 474 | 475 | ```go 476 | // 較佳: 477 | // A Server handles serving quotes from the collected works of Shakespeare. 478 | type Server struct { 479 | // BaseDir points to the base directory under which Shakespeare's works are stored. 480 | // 481 | // The directory structure is expected to be the following: 482 | // {BaseDir}/manifest.json 483 | // {BaseDir}/{name}/{name}-part{number}.txt 484 | BaseDir string 485 | 486 | WelcomeMessage string // displayed when user logs in 487 | ProtocolVersion string // checked against incoming requests 488 | PageLength int // lines per page when printing (optional; default: 20) 489 | } 490 | ``` 491 | 492 | [文件註解]: #doc-comments 493 | 494 | 495 | 496 | ### 範例 497 | 498 | 499 | 500 | 套件應該清楚地記錄其預期用法。嘗試提供一個[可運行的範例];範例會出現在 Godoc 中。可運行的範例屬於測試文件,而不是生產源文件。參見此範例([Godoc],[源代碼])。 501 | 502 | [可運行的範例]: http://blog.golang.org/examples 503 | [Godoc]: https://pkg.go.dev/time#example-Duration 504 | [源代碼]: https://cs.opensource.google/go/go/+/HEAD:src/time/example_test.go 505 | 506 | 如果不可行提供一個可運行的範例,範例代碼可以在代碼註解中提供。與註解中的其他代碼和命令行片段一樣,它應該遵循標準格式化慣例。 507 | 508 | 509 | 510 | ### 命名結果參數 511 | 512 | 513 | 514 | 在命名參數時,考慮函數簽名在 Godoc 中的顯示方式。函數本身的名稱和結果參數的類型通常已經足夠清楚。 515 | 516 | ```go 517 | // 較佳: 518 | func (n *Node) Parent1() *Node 519 | func (n *Node) Parent2() (*Node, error) 520 | ``` 521 | 522 | 如果函數返回兩個或多個相同類型的參數,添加名稱可能會有用。 523 | 524 | ```go 525 | // 較佳: 526 | func (n *Node) Children() (left, right *Node, err error) 527 | ``` 528 | 529 | 如果調用者必須對特定結果參數採取行動,命名它們可以幫助建議什麼是行動: 530 | 531 | ```go 532 | // 較佳: 533 | // WithTimeout returns a context that will be canceled no later than d duration 534 | // from now. 535 | // 536 | // The caller must arrange for the returned cancel function to be called when 537 | // the context is no longer needed to prevent a resource leak. 538 | func WithTimeout(parent Context, d time.Duration) (ctx Context, cancel func()) 539 | ``` 540 | 541 | 在上面的代碼中,取消是調用者必須採取的特定行動。然而,如果結果參數僅寫為 `(Context, func())`,則不清楚所指的“取消函數”是什麼。 542 | 543 | 不要在名稱產生[不必要的重複](#repetitive-with-type)時使用命名結果參數。 544 | 545 | ```go 546 | // 不佳: 547 | func (n *Node) Parent1() (node *Node) 548 | func (n *Node) Parent2() (node *Node, err error) 549 | ``` 550 | 551 | 不要為了避免在函數內聲明一個變量而命名結果參數。這種做法導致不必要的 API 冗長,代價是輕微的實現簡潔。 552 | 553 | [裸返回] 只在小函數中是可接受的。一旦它是一個中等大小的函數,請明確地返回您的值。同樣,不要僅因為它使您能夠使用裸返回就命名結果參數。[清晰度](guide.md#clarity)總是比在函數中節省幾行更重要。 554 | 555 | 如果必須在延遲閉包中更改結果參數的值,則命名結果參數始終是可接受的。 556 | 557 | > **提示:** 在函數簽名中,類型往往比名稱更清晰。 558 | > [GoTip #38: 函數作為命名類型] 展示了這一點。 559 | > 560 | > 在上面的 [`WithTimeout`] 中,真正的代碼使用了一個 [`CancelFunc`] 而不是原始的 `func()` 作為結果參數列表,並且很少需要文檔來說明。 561 | 562 | [裸返回]: https://tour.golang.org/basics/7 563 | [GoTip #38: 函數作為命名類型]: https://google.github.io/styleguide/go/index.html#gotip 564 | [`WithTimeout`]: https://pkg.go.dev/context#WithTimeout 565 | [`CancelFunc`]: https://pkg.go.dev/context#CancelFunc 566 | 567 | 568 | 569 | ### 套件註解 570 | 571 | 572 | 573 | 套件註解必須出現在套件宣告的正上方,註解和套件名稱之間不能有空行。範例: 574 | 575 | ```go 576 | // 較佳: 577 | // Package math 提供基本常數和數學函數。 578 | // 579 | // 本套件不保證在不同架構間有位元完全相同的結果。 580 | package math 581 | ``` 582 | 583 | 每個套件必須有一個套件註解。如果一個套件由多個文件組成,則正好有一個文件應該有一個套件註解。 584 | 585 | `main` 套件的註解有一種略微不同的形式,其中 BUILD 文件中的 `go_binary` 規則的名稱取代了套件名稱。 586 | 587 | ```go 588 | // 較佳: 589 | // seed_generator 命令是一個實用工具,它從一組 JSON 研究配置生成 Finch 種子文件。 590 | package main 591 | ``` 592 | 593 | 只要二進制名稱與 BUILD 文件中寫的完全一致,其他風格的註解也是可以的。當二進制名稱是第一個單詞時,即使它並不嚴格匹配命令行調用的拼寫,也需要將其大寫。 594 | 595 | ```go 596 | // 較佳: 597 | // 二進制 seed_generator ... 598 | // 命令 seed_generator ... 599 | // 程序 seed_generator ... 600 | // seed_generator 命令 ... 601 | // seed_generator 程序 ... 602 | // Seed_generator ... 603 | ``` 604 | 605 | Tips: 606 | 607 | - 示例命令行調用和 API 使用可以是有用的文檔。對於 Godoc 格式,縮進包含代碼的註解行。 608 | 609 | - 如果沒有明顯的主文件,或者如果套件註解非常長,則可以接受將文檔註解放在一個名為 `doc.go` 的文件中,該文件僅包含註解和套件子句。 610 | 611 | - 多行註解可以代替多個單行註解。如果文檔包含可能需要從源文件中複製和粘貼的部分,這主要是有用的,如示例命令行(對於二進制文件)和模板示例。 612 | 613 | ```go 614 | // 較佳: 615 | /* 616 | seed_generator 命令是一個實用工具,它從一組 JSON 研究配置生成 Finch 種子文件。 617 | 618 | seed_generator *.json | base64 > finch-seed.base64 619 | */ 620 | package template 621 | ``` 622 | 623 | - 旨在供維護者使用且適用於整個文件的註解通常放在導入聲明之後。這些在 Godoc 中不會顯示,也不受上述套件註解規則的約束。 624 | 625 | 626 | 627 | ## 引入 628 | 629 | 630 | 631 | 632 | 633 | ### 引入重命名 634 | 635 | 引入只應該在與其他引入發生名稱衝突時才重命名。(由此衍生的一個結論是,[良好的套件名稱](#package-names)不應該需要重命名。)在名稱衝突發生時,優先重命名最本地或項目特定的引入。對於套件的本地名稱(別名)必須遵循[套件命名的指導原則](#package-names),包括禁止使用下劃線和大寫字母。 636 | 637 | 生成的協議緩衝區套件必須重命名以去除其名稱中的下劃線,並且它們的別名必須有一個 `pb` 後綴。有關更多信息,請參見[proto 和 stub 最佳實踐]。 638 | 639 | [proto 和 stub 最佳實踐]: best-practices#import-protos 640 | 641 | ```go 642 | // 較佳: 643 | import ( 644 | fspb "path/to/package/foo_service_go_proto" 645 | ) 646 | ``` 647 | 648 | 引入具有沒有有用識別信息的套件名稱(例如 `package v1`)時,應該重新命名以包含前一個路徑組件。重新命名必須與其他本地文件引入相同套件時保持一致,並且可以包含版本號。 649 | 650 | **注意:** 建議將套件重新命名以符合[良好的套件名稱](#package-names),但對於 vendored 目錄中的套件來說,這通常不可行。 651 | 652 | ```go 653 | // 較佳: 654 | import ( 655 | core "github.com/kubernetes/api/core/v1" 656 | meta "github.com/kubernetes/apimachinery/pkg/apis/meta/v1beta1" 657 | ) 658 | ``` 659 | 660 | 如果您需要引入一個與您想要使用的常見本地變量名稱衝突的套件(例如 `url`、`ssh`),並且您希望重新命名該套件,那麼建議的做法是使用 `pkg` 後綴(例如 `urlpkg`)。請注意,有可能用本地變量遮蔽一個套件;只有在這樣的變量在作用域內時仍需要使用該套件,才需要進行此重命名。 661 | 662 | 663 | 664 | ### 引入分組 665 | 666 | 引入應該組織成兩組: 667 | 668 | - 標準庫套件 669 | 670 | - 其他(專案和 vendored)套件 671 | 672 | ```go 673 | // 較佳: 674 | package main 675 | 676 | import ( 677 | "fmt" 678 | "hash/adler32" 679 | "os" 680 | 681 | "github.com/dsnet/compress/flate" 682 | "golang.org/x/text/encoding" 683 | "google.golang.org/protobuf/proto" 684 | foopb "myproj/foo/proto/proto" 685 | _ "myproj/rpc/protocols/dial" 686 | _ "myproj/security/auth/authhooks" 687 | ) 688 | ``` 689 | 690 | 如果您想要一個單獨的組,則可以將專案套件分成多個組,只要這些組有一些意義即可。這樣做的常見原因包括: 691 | 692 | - 重新命名的引入 693 | - 為了它們的副作用而引入的套件 694 | 695 | 範例: 696 | 697 | ```go 698 | // 較佳: 699 | package main 700 | 701 | import ( 702 | "fmt" 703 | "hash/adler32" 704 | "os" 705 | 706 | 707 | "github.com/dsnet/compress/flate" 708 | "golang.org/x/text/encoding" 709 | "google.golang.org/protobuf/proto" 710 | 711 | foopb "myproj/foo/proto/proto" 712 | 713 | _ "myproj/rpc/protocols/dial" 714 | _ "myproj/security/auth/authhooks" 715 | ) 716 | ``` 717 | 718 | **注意:** 維護可選組 - 分割超出標準庫和 Google 引入之間的強制性分離所必需的 - 不受 [goimports] 工具的支持。額外的引入子組需要作者和審查者的注意力來保持符合狀態。 719 | 720 | [goimports]: golang.org/x/tools/cmd/goimports 721 | 722 | 同時也是 AppEngine 應用的 Google 程序應該有一個單獨的組用於 AppEngine 引入。 723 | 724 | Gofmt 負責按引入路徑對每組進行排序。然而,它不會自動將引入分成組。流行的 [goimports] 工具結合了 Gofmt 和引入管理,根據上述決定將引入分成組。允許讓 [goimports] 完全管理引入排列,但是隨著文件的修訂,其引入列表必須保持內部一致性。 725 | 726 | 727 | 728 | ### 引入「空白」(`import _`) 729 | 730 | 731 | 732 | 只為了它們的副作用而被引入的套件(使用語法 `import _ "package"`)只能在主套件中引入,或在需要它們的測試中引入。 733 | 734 | 這類套件的一些例子包括: 735 | 736 | - [time/tzdata](https://pkg.go.dev/time/tzdata) 737 | 738 | - [image/jpeg](https://pkg.go.dev/image/jpeg) 在圖像處理代碼中 739 | 740 | 避免在庫套件中進行空白引入,即使庫間接依賴於它們。將副作用引入限制在主套件中有助於控制依賴關係,並使得撰寫依賴不同引入的測試成為可能,而不會產生衝突或浪費建構成本。 741 | 742 | 以下是此規則的唯一例外: 743 | 744 | - 您可以使用空白引入來繞過 [nogo 靜態檢查器] 中對禁止引入的檢查。 745 | 746 | - 您可以在使用 `//go:embed` 編譯器指令的源文件中,空白引入 [embed](https://pkg.go.dev/embed) 套件。 747 | 748 | **提示:** 如果您創建了一個在生產中間接依賴副作用引入的庫套件,請記錄預期的使用方式。 749 | 750 | [nogo 靜態檢查器]: https://github.com/bazelbuild/rules_go/blob/master/go/nogo.rst 751 | 752 | 753 | 754 | ### 引入「點」(`import .`) 755 | 756 | 757 | 758 | `import .` 形式是一種語言特性,它允許將另一個套件導出的標識符帶到當前套件中,無需資格限制。更多信息請參見 [語言規範](https://go.dev/ref/spec#Import_declarations)。 759 | 760 | **不要** 在 Google 代碼庫中使用此功能;它使得更難判斷功能來自何處。 761 | 762 | ```go 763 | // 不佳: 764 | package foo_test 765 | 766 | import ( 767 | "bar/testutil" // also imports "foo" 768 | . "foo" 769 | ) 770 | 771 | var myThing = Bar() // Bar defined in package foo; no qualification needed. 772 | ``` 773 | 774 | ```go 775 | // 較佳: 776 | package foo_test 777 | 778 | import ( 779 | "bar/testutil" // also imports "foo" 780 | "foo" 781 | ) 782 | 783 | var myThing = foo.Bar() 784 | ``` 785 | 786 | 787 | 788 | ## 錯誤 789 | 790 | 791 | 792 | ### 返回錯誤 793 | 794 | 795 | 796 | 使用 `error` 來表示一個函數可能會失敗。按照慣例,`error` 是最後一個結果參數。 797 | 798 | ```go 799 | // 較佳: 800 | func Good() error { /* ... */ } 801 | ``` 802 | 803 | 返回一個 `nil` 錯誤是表示一個本可以失敗的操作成功的慣用方法。如果一個函數返回一個錯誤,除非另有明確文檔記載,否則調用者必須將所有非錯誤返回值視為未指定。通常,非錯誤返回值是它們的零值,但這不能假定。 804 | 805 | ```go 806 | // 較佳: 807 | func GoodLookup() (*Result, error) { 808 | // ... 809 | if err != nil { 810 | return nil, err 811 | } 812 | return res, nil 813 | } 814 | ``` 815 | 816 | 導出的函數返回錯誤應該使用 `error` 類型返回它們。具體的錯誤類型容易出現微妙的錯誤:一個具體的 `nil` 指針可以被包裝進一個介面,從而變成一個非 `nil` 值(參見 [Go FAQ 中關於此主題的條目][nil error])。 817 | 818 | ```go 819 | // 不佳: 820 | func Bad() *os.PathError { /*...*/ } 821 | ``` 822 | 823 | **提示**:一個接受 `context.Context` 參數的函數通常應該返回一個 `error`,以便調用者可以確定在函數運行時上下文是否被取消。 824 | 825 | [nil error]: https://golang.org/doc/faq#nil_error 826 | 827 | 828 | 829 | ### 錯誤字串 830 | 831 | 832 | 833 | 錯誤字串不應該首字大寫(除非以導出名稱、專有名詞或首字母縮略詞開頭),並且不應該以標點符號結尾。這是因為錯誤字串通常會在打印給用戶之前出現在其他上下文中。 834 | 835 | ```go 836 | // 不佳: 837 | err := fmt.Errorf("Something bad happened.") 838 | ``` 839 | 840 | ```go 841 | // 較佳: 842 | err := fmt.Errorf("something bad happened") 843 | ``` 844 | 845 | 另一方面,完整顯示消息(日誌、測試失敗、API 回應或其他 UI)的風格取決於具體情況,但通常應該首字大寫。 846 | 847 | ```go 848 | // 較佳: 849 | log.Infof("Operation aborted: %v", err) 850 | log.Errorf("Operation aborted: %v", err) 851 | t.Errorf("Op(%q) failed unexpectedly; err=%v", args, err) 852 | ``` 853 | 854 | 855 | 856 | ### 處理錯誤 857 | 858 | 859 | 860 | 遇到錯誤的代碼應該有意識地選擇如何處理它。通常不適合使用 `_` 變量來丟棄錯誤。如果函數返回一個錯誤,請執行以下操作之一: 861 | 862 | - 立即處理並解決錯誤。 863 | - 將錯誤返回給調用者。 864 | - 在特殊情況下,調用 [`log.Fatal`] 或(如果絕對必要)`panic`。 865 | 866 | **注意:** `log.Fatalf` 不是標準庫日誌。參見 [#logging]。 867 | 868 | 在極少數適合忽略或丟棄錯誤的情況下(例如,對 [`(*bytes.Buffer).Write`] 的調用,該調用被記錄為永遠不會失敗),應該有一個相應的註解解釋為什麼這樣做是安全的。 869 | 870 | ```go 871 | // 較佳: 872 | var b *bytes.Buffer 873 | 874 | n, _ := b.Write(p) // never returns a non-nil error 875 | ``` 876 | 877 | 有關錯誤處理的更多討論和示例,請參見 [Effective Go](http://golang.org/doc/effective_go.html#errors) 和 [最佳實踐](best-practices.md#error-handling)。 878 | 879 | [`(*bytes.Buffer).Write`]: https://pkg.go.dev/bytes#Buffer.Write 880 | 881 | 882 | 883 | ### 帶內錯誤 884 | 885 | 886 | 887 | 在 C 語言和類似語言中,函數返回像 -1、null 或空字串這樣的值來表示錯誤或缺失結果是很常見的。這稱為帶內錯誤處理。 888 | 889 | ```go 890 | // 不佳: 891 | // Lookup returns the value for key or -1 if there is no mapping for key. 892 | func Lookup(key string) int 893 | ``` 894 | 895 | 未檢查內部錯誤值可能導致錯誤,並將錯誤歸咎於錯誤的函數。 896 | 897 | ```go 898 | // 不佳: 899 | // The following line returns an error that Parse failed for the input value, 900 | // whereas the failure was that there is no mapping for missingKey. 901 | return Parse(Lookup(missingKey)) 902 | ``` 903 | 904 | Go 對多返回值的支持提供了一個更好的解決方案(參見[Effective Go 關於多重返回的部分])。函數不應要求客戶端檢查內部錯誤值,而應返回一個額外的值來指示其其他返回值是否有效。這個返回值可能是一個錯誤或一個布林值(當不需要解釋時),並且應該是最後的返回值。 905 | 906 | ```go 907 | // 較佳: 908 | // Lookup returns the value for key or ok=false if there is no mapping for key. 909 | func Lookup(key string) (value string, ok bool) 910 | ``` 911 | 912 | 這種 API 防止調用者錯誤地寫 `Parse(Lookup(key))`,這會導致編譯時錯誤,因為 `Lookup(key)` 有 2 個輸出。 913 | 914 | 以這種方式返回錯誤鼓勵更健壯和明確的錯誤處理: 915 | 916 | ```go 917 | // 較佳: 918 | value, ok := Lookup(key) 919 | if !ok { 920 | return fmt.Errorf("no value for %q", key) 921 | } 922 | return Parse(value) 923 | ``` 924 | 925 | 一些標準庫函數,如 `strings` 套件中的函數,返回內部錯誤值。這大大簡化了字符串操作代碼,但代價是需要程序員更加謹慎。一般來說,Google 代碼庫中的 Go 代碼應該返回額外的錯誤值。 926 | 927 | [Effective Go 關於多重返回的部分]: http://golang.org/doc/effective_go.html#multiple-returns 928 | 929 | 930 | 931 | ### 縮進錯誤流程 (Indent error flow) 932 | 933 | 934 | 935 | 在繼續處理代碼的其餘部分之前,先處理錯誤。這通過使讀者能夠快速找到正常路徑來提高代碼的可讀性。這個邏輯同樣適用於任何測試條件然後以終止條件結束的塊(例如,`return`、`panic`、`log.Fatal`)。 936 | 937 | 如果未滿足終止條件,則運行的代碼應該出現在 `if` 塊之後,並且不應該在 `else` 子句中縮進。 938 | 939 | ```go 940 | // 較佳: 941 | if err != nil { 942 | // 錯誤處理 943 | return // 或者 continue 等等 944 | } 945 | // 正常代碼 946 | ``` 947 | 948 | ```go 949 | // 不佳: 950 | if err != nil { 951 | // 錯誤處理 952 | } else { 953 | // 由於縮進,看起來不正常的正常代碼 954 | } 955 | ``` 956 | 957 | > **提示:** 如果您在多於幾行代碼中使用變量,通常不值得使用帶初始化器的 `if` 風格。在這些情況下,通常最好將聲明移出並使用標準的 `if` 語句: 958 | > 959 | > ```go 960 | > // 較佳: 961 | > x, err := f() 962 | > if err != nil { 963 | > // 錯誤處理 964 | > return 965 | > } 966 | > // 使用 x 的大量代碼 967 | > // 跨越多行 968 | > ``` 969 | > 970 | > ```go 971 | > // 不佳: 972 | > if x, err := f(); err != nil { 973 | > // 錯誤處理 974 | > return 975 | > } else { 976 | > // 使用 x 的大量代碼 977 | > // 跨越多行 978 | > } 979 | > ``` 980 | 981 | 有關更多細節,請參見 [Go 提示 #1: 視線範圍] 和 982 | [TotT: 通過減少嵌套降低代碼複雜性](https://testing.googleblog.com/2017/06/code-health-reduce-nesting-reduce.html)。 983 | 984 | [Go 提示 #1: 視線範圍]: https://google.github.io/styleguide/go/index.html#gotip 985 | 986 | 987 | 988 | ## 語言 989 | 990 | 991 | 992 | ### 字面量格式化 993 | 994 | Go 擁有異常強大的[複合字面量語法],可以用單一表達式來表達深層嵌套、複雜的值。在可能的情況下,應該使用這種字面量語法,而不是逐字段構建值。`gofmt` 對字面量的格式化通常相當不錯,但是還有一些額外的規則來保持這些字面量的可讀性和可維護性。 995 | 996 | [複合字面量語法]: https://golang.org/ref/spec#Composite_literals 997 | 998 | 999 | 1000 | #### 字段名稱 1001 | 1002 | 對於在當前套件外定義的類型,結構字面量必須指定**字段名稱**。 1003 | 1004 | - 包括來自其他套件的類型的字段名稱。 1005 | 1006 | ```go 1007 | // 較佳: 1008 | // https://pkg.go.dev/encoding/csv#Reader 1009 | r := csv.Reader{ 1010 | Comma: ',', 1011 | Comment: '#', 1012 | FieldsPerRecord: 4, 1013 | } 1014 | ``` 1015 | 1016 | 結構中字段的位置和字段的完整集合(省略字段名稱時必須正確的兩個條件)通常不被認為是結構的公共 API 的一部分;指定字段名稱是為了避免不必要的耦合。 1017 | 1018 | ```go 1019 | // 不佳: 1020 | r := csv.Reader{',', '#', 4, false, false, false, false} 1021 | ``` 1022 | 1023 | - 對於套件內部類型,字段名稱是可選的。 1024 | 1025 | ```go 1026 | // 較佳: 1027 | okay := Type{42} 1028 | also := internalType{4, 2} 1029 | ``` 1030 | 1031 | 如果使用字段名稱可以使代碼更清晰,則仍應使用字段名稱,這樣做是非常常見的。例如,具有大量字段的結構幾乎總是應該使用字段名稱進行初始化。 1032 | 1033 | 1034 | 1035 | ```go 1036 | // 較佳: 1037 | okay := StructWithLotsOfFields{ 1038 | field1: 1, 1039 | field2: "two", 1040 | field3: 3.14, 1041 | field4: true, 1042 | } 1043 | ``` 1044 | 1045 | 1046 | 1047 | #### 匹配的大括號 1048 | 1049 | 一對大括號的閉合部分應該總是出現在與開括號相同縮進量的行上。單行字面量必然具有此屬性。當字面量跨越多行時,保持此屬性使得字面量的大括號匹配與 Go 常見的語法結構(如函數和 `if` 語句)的大括號匹配相同。 1050 | 1051 | 這方面最常見的錯誤是將多行結構字面量中的值與閉合括號放在同一行。在這些情況下,該行應該以逗號結束,閉合括號應該出現在下一行。 1052 | 1053 | ```go 1054 | // 較佳: 1055 | good := []*Type{{Key: "value"}} 1056 | ``` 1057 | 1058 | ```go 1059 | // 較佳: 1060 | good := []*Type{ 1061 | {Key: "multi"}, 1062 | {Key: "line"}, 1063 | } 1064 | ``` 1065 | 1066 | ```go 1067 | // 不佳: 1068 | bad := []*Type{ 1069 | {Key: "multi"}, 1070 | {Key: "line"}} 1071 | ``` 1072 | 1073 | ```go 1074 | // 不佳: 1075 | bad := []*Type{ 1076 | { 1077 | Key: "value"}, 1078 | } 1079 | ``` 1080 | 1081 | 1082 | 1083 | #### 貼合的大括號 (Cuddled braces) 1084 | 1085 | 對於切片和數組字面量,只有在以下兩個條件都滿足時,才允許去掉大括號之間的空白(也就是所謂的「緊靠」它們)。 1086 | 1087 | - [縮進匹配](#literal-matching-braces) 1088 | - 內部值也是字面量或 proto 構建器(即不是變量或其他表達式) 1089 | 1090 | ```go 1091 | // 較佳: 1092 | good := []*Type{ 1093 | { // Not cuddled 1094 | Field: "value", 1095 | }, 1096 | { 1097 | Field: "value", 1098 | }, 1099 | } 1100 | ``` 1101 | 1102 | ```go 1103 | // 較佳: 1104 | good := []*Type{{ // Cuddled correctly 1105 | Field: "value", 1106 | }, { 1107 | Field: "value", 1108 | }} 1109 | ``` 1110 | 1111 | ```go 1112 | // 較佳: 1113 | good := []*Type{ 1114 | first, // Can't be cuddled 1115 | {Field: "second"}, 1116 | } 1117 | ``` 1118 | 1119 | ```go 1120 | // 較佳: 1121 | okay := []*pb.Type{pb.Type_builder{ 1122 | Field: "first", // Proto Builders may be cuddled to save vertical space 1123 | }.Build(), pb.Type_builder{ 1124 | Field: "second", 1125 | }.Build()} 1126 | ``` 1127 | 1128 | ```go 1129 | // 不佳: 1130 | bad := []*Type{ 1131 | first, 1132 | { 1133 | Field: "second", 1134 | }} 1135 | ``` 1136 | 1137 | 1138 | 1139 | #### 重複的類型名稱 1140 | 1141 | 從切片和映射字面量中可以省略重複的類型名稱。這有助於減少混亂。當處理在您的項目中不常見的複雜類型時,明確重複類型名稱是一個合理的場合,特別是當重複的類型名稱出現在相隔很遠的行上,可以提醒讀者上下文。 1142 | 1143 | ```go 1144 | // 較佳: 1145 | good := []*Type{ 1146 | {A: 42}, 1147 | {A: 43}, 1148 | } 1149 | ``` 1150 | 1151 | ```go 1152 | // 不佳: 1153 | repetitive := []*Type{ 1154 | &Type{A: 42}, 1155 | &Type{A: 43}, 1156 | } 1157 | ``` 1158 | 1159 | ```go 1160 | // 較佳: 1161 | good := map[Type1]*Type2{ 1162 | {A: 1}: {B: 2}, 1163 | {A: 3}: {B: 4}, 1164 | } 1165 | ``` 1166 | 1167 | ```go 1168 | // 不佳: 1169 | repetitive := map[Type1]*Type2{ 1170 | Type1{A: 1}: &Type2{B: 2}, 1171 | Type1{A: 3}: &Type2{B: 4}, 1172 | } 1173 | ``` 1174 | 1175 | **提示:** 如果您想在結構字面量中移除重複的類型名稱,您可以執行 `gofmt -s`。 1176 | 1177 | 1178 | 1179 | #### 零值字段 1180 | 1181 | 當不會因此失去清晰度時,可以從結構字面量中省略[零值]字段。 1182 | 1183 | 設計良好的 API 經常使用零值構造來增強可讀性。例如,從下面的結構中省略三個零值字段,可以將注意力集中到正在指定的唯一選項上。 1184 | 1185 | [零值]: https://golang.org/ref/spec#The_zero_value 1186 | 1187 | ```go 1188 | // 不佳: 1189 | import ( 1190 | "github.com/golang/leveldb" 1191 | "github.com/golang/leveldb/db" 1192 | ) 1193 | 1194 | ldb := leveldb.Open("/my/table", &db.Options{ 1195 | BlockSize: 1<<16, 1196 | ErrorIfDBExists: true, 1197 | 1198 | // These fields all have their zero values. 1199 | BlockRestartInterval: 0, 1200 | Comparer: nil, 1201 | Compression: nil, 1202 | FileSystem: nil, 1203 | FilterPolicy: nil, 1204 | MaxOpenFiles: 0, 1205 | WriteBufferSize: 0, 1206 | VerifyChecksums: false, 1207 | }) 1208 | ``` 1209 | 1210 | ```go 1211 | // 較佳: 1212 | import ( 1213 | "github.com/golang/leveldb" 1214 | "github.com/golang/leveldb/db" 1215 | ) 1216 | 1217 | ldb := leveldb.Open("/my/table", &db.Options{ 1218 | BlockSize: 1<<16, 1219 | ErrorIfDBExists: true, 1220 | }) 1221 | ``` 1222 | 1223 | 在表驅動測試中的結構體通常會從[明確的字段名稱]中受益,特別是當測試結構體不是微不足道的時候。這允許作者在有關字段與測試案例無關時完全省略零值字段。例如,成功的測試案例應該省略任何與錯誤相關或失敗相關的字段。在零值對於理解測試案例是必要的情況下,例如測試零或 `nil` 輸入,應該指定字段名稱。 1224 | 1225 | [明確的字段名稱]: #literal-field-names 1226 | 1227 | 簡潔 1228 | 1229 | ```go 1230 | tests := []struct { 1231 | input string 1232 | wantPieces []string 1233 | wantErr error 1234 | }{ 1235 | { 1236 | input: "1.2.3.4", 1237 | wantPieces: []string{"1", "2", "3", "4"}, 1238 | }, 1239 | { 1240 | input: "hostname", 1241 | wantErr: ErrBadHostname, 1242 | }, 1243 | } 1244 | ``` 1245 | 1246 | 明確 1247 | 1248 | ```go 1249 | tests := []struct { 1250 | input string 1251 | wantIPv4 bool 1252 | wantIPv6 bool 1253 | wantErr bool 1254 | }{ 1255 | { 1256 | input: "1.2.3.4", 1257 | wantIPv4: true, 1258 | wantIPv6: false, 1259 | }, 1260 | { 1261 | input: "1:2::3:4", 1262 | wantIPv4: false, 1263 | wantIPv6: true, 1264 | }, 1265 | { 1266 | input: "hostname", 1267 | wantIPv4: false, 1268 | wantIPv6: false, 1269 | wantErr: true, 1270 | }, 1271 | } 1272 | ``` 1273 | 1274 | 1275 | 1276 | ### Nil 切片 1277 | 1278 | 對於大多數目的來說,`nil` 和空切片之間沒有功能上的差異。內建函數如 `len` 和 `cap` 對 `nil` 切片的行為如預期。 1279 | 1280 | ```go 1281 | // 較佳: 1282 | import "fmt" 1283 | 1284 | var s []int // nil 1285 | 1286 | fmt.Println(s) // [] 1287 | fmt.Println(len(s)) // 0 1288 | fmt.Println(cap(s)) // 0 1289 | for range s {...} // 無操作 1290 | 1291 | s = append(s, 42) 1292 | fmt.Println(s) // [42] 1293 | ``` 1294 | 1295 | 如果你聲明一個空切片作為局部變量(特別是如果它可以是返回值的來源),優先選擇 nil 初始化以減少呼叫者的錯誤風險。 1296 | 1297 | ```go 1298 | // 較佳: 1299 | var t []string 1300 | ``` 1301 | 1302 | ```go 1303 | // 不佳: 1304 | t := []string{} 1305 | ``` 1306 | 1307 | 不要創建強迫客戶端區分 nil 和空切片的 API。 1308 | 1309 | ```go 1310 | // 較佳: 1311 | // Ping 對其目標進行 ping 操作。 1312 | // 返回成功響應的主機。 1313 | func Ping(hosts []string) ([]string, error) { ... } 1314 | ``` 1315 | 1316 | ```go 1317 | // 不佳: 1318 | // Ping 對其目標進行 ping 操作並返回成功響應的主機列表。 1319 | // 如果輸入為空則可以為空。 1320 | // nil 表示發生了系統錯誤。 1321 | func Ping(hosts []string) []string { ... } 1322 | ``` 1323 | 1324 | 在設計介面時,避免區分 `nil` 切片和非 `nil`、長度為零的切片,因為這可能導致微妙的編程錯誤。這通常是通過使用 `len` 來檢查空值,而不是 `== nil` 來實現的。 1325 | 1326 | 這個實現接受 `nil` 和長度為零的切片作為「空」: 1327 | 1328 | ```go 1329 | // 較佳: 1330 | // describeInts 用給定的前綴描述 s,除非 s 為空。 1331 | func describeInts(prefix string, s []int) { 1332 | if len(s) == 0 { 1333 | return 1334 | } 1335 | fmt.Println(prefix, s) 1336 | } 1337 | ``` 1338 | 1339 | 而不是將區分作為 API 的一部分依賴: 1340 | 1341 | ```go 1342 | // 不佳: 1343 | func maybeInts() []int { /* ... */ } 1344 | 1345 | // describeInts 用給定的前綴描述 s;傳遞 nil 以完全跳過。 1346 | func describeInts(prefix string, s []int) { 1347 | // 這個函數的行為不經意地根據 maybeInts() 在「空」情況下返回的內容(nil 或 []int{})而改變。 1348 | if s == nil { 1349 | return 1350 | } 1351 | fmt.Println(prefix, s) 1352 | } 1353 | 1354 | describeInts("這裡有一些整數:", maybeInts()) 1355 | ``` 1356 | 1357 | 有關進一步討論,請參見[帶內錯誤]。 1358 | 1359 | [帶內錯誤]: #in-band-errors 1360 | 1361 | 1362 | 1363 | ### 縮排混淆 1364 | 1365 | 避免引入換行符,如果這樣會使剩餘的行與縮進的代碼塊對齊。如果這是不可避免的,請留一個空格來分隔塊中的代碼與換行後的行。 1366 | 1367 | ```go 1368 | // 不佳: 1369 | if longCondition1 && longCondition2 && 1370 | // Conditions 3 and 4 have the same indentation as the code within the if. 1371 | longCondition3 && longCondition4 { 1372 | log.Info("all conditions met") 1373 | } 1374 | ``` 1375 | 1376 | 請參閱以下部分以獲得具體指南和示例: 1377 | 1378 | - [函數格式化](#func-formatting) 1379 | - [條件和循環](#conditional-formatting) 1380 | - [字面量格式化](#literal-formatting) 1381 | 1382 | 1383 | 1384 | ### 函數格式化 1385 | 1386 | 函數或方法聲明的簽名應保持在單行上,以避免[縮進混淆](#indentation-confusion)。 1387 | 1388 | 函數參數列表可能會使 Go 源文件中的某些行變得很長。然而,它們預示著縮進的變化,因此很難以不會使後續行看起來像是以令人困惑的方式屬於函數體的一部分的方式斷行: 1389 | 1390 | ```go 1391 | // 不佳: 1392 | func (r *SomeType) SomeLongFunctionName(foo1, foo2, foo3 string, 1393 | foo4, foo5, foo6 int) { 1394 | foo7 := bar(foo1) 1395 | // ... 1396 | } 1397 | ``` 1398 | 1399 | 請參閱[最佳實踐](best-practices#funcargs)以獲得一些縮短具有許多參數的函數呼叫站點的選項。 1400 | 1401 | 通過提取局部變量,經常可以縮短行。 1402 | 1403 | ```go 1404 | // 較佳: 1405 | local := helper(some, parameters, here) 1406 | good := foo.Call(list, of, parameters, local) 1407 | ``` 1408 | 1409 | 同樣,函數和方法呼叫不應僅基於行長度而被分開。 1410 | 1411 | ```go 1412 | // 較佳: 1413 | good := foo.Call(long, list, of, parameters, all, on, one, line) 1414 | ``` 1415 | 1416 | ```go 1417 | // 不佳: 1418 | bad := foo.Call(long, list, of, parameters, 1419 | with, arbitrary, line, breaks) 1420 | ``` 1421 | 1422 | 盡可能避免對特定函數參數添加內聯註釋。相反,使用[選項結構](best-practices#option-structure)或在函數文檔中添加更多細節。 1423 | 1424 | ```go 1425 | // 較佳: 1426 | good := server.New(ctx, server.Options{Port: 42}) 1427 | ``` 1428 | 1429 | ```go 1430 | // 不佳: 1431 | bad := server.New( 1432 | ctx, 1433 | 42, // Port 1434 | ) 1435 | ``` 1436 | 1437 | 如果 API 無法更改或者局部呼叫是不尋常的(無論呼叫是否太長),如果它有助於理解呼叫,始終允許添加斷行。 1438 | 1439 | ```go 1440 | // 較佳: 1441 | canvas.RenderCube(cube, 1442 | x0, y0, z0, 1443 | x0, y0, z1, 1444 | x0, y1, z0, 1445 | x0, y1, z1, 1446 | x1, y0, z0, 1447 | x1, y0, z1, 1448 | x1, y1, z0, 1449 | x1, y1, z1, 1450 | ) 1451 | ``` 1452 | 1453 | 請注意,上面示例中的行不是在特定的列邊界處換行,而是基於坐標三元組分組。 1454 | 1455 | 函數內的長字符串字面量不應該僅為了行長而被斷開。對於包含此類字符串的函數,可以在字符串格式之後添加換行符,並在下一行或後續行提供參數。關於斷行位置的決定應基於輸入的語義分組,而不僅僅是基於行長。 1456 | 1457 | ```go 1458 | // 較佳: 1459 | log.Warningf("Database key (%q, %d, %q) incompatible in transaction started by (%q, %d, %q)", 1460 | currentCustomer, currentOffset, currentKey, 1461 | txCustomer, txOffset, txKey) 1462 | ``` 1463 | 1464 | ```go 1465 | // 不佳: 1466 | log.Warningf("Database key (%q, %d, %q) incompatible in"+ 1467 | " transaction started by (%q, %d, %q)", 1468 | currentCustomer, currentOffset, currentKey, txCustomer, 1469 | txOffset, txKey) 1470 | ``` 1471 | 1472 | 1473 | 1474 | ### 條件語句和循環 1475 | 1476 | `if` 語句不應該換行;多行 `if` 子句可能導致[縮進混淆](#indentation-confusion)。 1477 | 1478 | ```go 1479 | // 不佳: 1480 | // The second if statement is aligned with the code within the if block, causing 1481 | // indentation confusion. 1482 | if db.CurrentStatusIs(db.InTransaction) && 1483 | db.ValuesEqual(db.TransactionKey(), row.Key()) { 1484 | return db.Errorf(db.TransactionError, "query failed: row (%v): key does not match transaction key", row) 1485 | } 1486 | ``` 1487 | 1488 | 如果不需要短路行為,則可以直接提取布林運算元: 1489 | 1490 | ```go 1491 | // 較佳: 1492 | inTransaction := db.CurrentStatusIs(db.InTransaction) 1493 | keysMatch := db.ValuesEqual(db.TransactionKey(), row.Key()) 1494 | if inTransaction && keysMatch { 1495 | return db.Error(db.TransactionError, "query failed: row (%v): key does not match transaction key", row) 1496 | } 1497 | ``` 1498 | 1499 | 也可能有其他局部變量可以被提取,特別是如果條件已經重複: 1500 | 1501 | ```go 1502 | // 較佳: 1503 | uid := user.GetUniqueUserID() 1504 | if db.UserIsAdmin(uid) || db.UserHasPermission(uid, perms.ViewServerConfig) || db.UserHasPermission(uid, perms.CreateGroup) { 1505 | // ... 1506 | } 1507 | ``` 1508 | 1509 | ```go 1510 | // 不佳: 1511 | if db.UserIsAdmin(user.GetUniqueUserID()) || db.UserHasPermission(user.GetUniqueUserID(), perms.ViewServerConfig) || db.UserHasPermission(user.GetUniqueUserID(), perms.CreateGroup) { 1512 | // ... 1513 | } 1514 | ``` 1515 | 1516 | 包含閉包或多行結構字面量的 `if` 語句應確保[大括號匹配](#literal-matching-braces)以避免[縮進混淆](#indentation-confusion)。 1517 | 1518 | ```go 1519 | // 較佳: 1520 | if err := db.RunInTransaction(func(tx *db.TX) error { 1521 | return tx.Execute(userUpdate, x, y, z) 1522 | }); err != nil { 1523 | return fmt.Errorf("user update failed: %s", err) 1524 | } 1525 | ``` 1526 | 1527 | ```go 1528 | // 較佳: 1529 | if _, err := client.Update(ctx, &upb.UserUpdateRequest{ 1530 | ID: userID, 1531 | User: user, 1532 | }); err != nil { 1533 | return fmt.Errorf("user update failed: %s", err) 1534 | } 1535 | ``` 1536 | 1537 | 同樣,不要嘗試在 `for` 語句中插入人為的換行。如果沒有優雅的重構方式,可以讓行保持長度: 1538 | 1539 | ```go 1540 | // 較佳: 1541 | for i, max := 0, collection.Size(); i < max && !collection.HasPendingWriters(); i++ { 1542 | // ... 1543 | } 1544 | ``` 1545 | 1546 | 然而,通常有辦法: 1547 | 1548 | ```go 1549 | // 較佳: 1550 | for i, max := 0, collection.Size(); i < max; i++ { 1551 | if collection.HasPendingWriters() { 1552 | break 1553 | } 1554 | // ... 1555 | } 1556 | ``` 1557 | 1558 | `switch` 和 `case` 語句也應保持在單行上。 1559 | 1560 | ```go 1561 | // 較佳: 1562 | switch good := db.TransactionStatus(); good { 1563 | case db.TransactionStarting, db.TransactionActive, db.TransactionWaiting: 1564 | // ... 1565 | case db.TransactionCommitted, db.NoTransaction: 1566 | // ... 1567 | default: 1568 | // ... 1569 | } 1570 | ``` 1571 | 1572 | ```go 1573 | // 不佳: 1574 | switch bad := db.TransactionStatus(); bad { 1575 | case db.TransactionStarting, 1576 | db.TransactionActive, 1577 | db.TransactionWaiting: 1578 | // ... 1579 | case db.TransactionCommitted, 1580 | db.NoTransaction: 1581 | // ... 1582 | default: 1583 | // ... 1584 | } 1585 | ``` 1586 | 1587 | 如果行過長,將所有案例縮進並用空行分隔,以避免[縮進混淆](#indentation-confusion): 1588 | 1589 | ```go 1590 | // 較佳: 1591 | switch db.TransactionStatus() { 1592 | case 1593 | db.TransactionStarting, 1594 | db.TransactionActive, 1595 | db.TransactionWaiting, 1596 | db.TransactionCommitted: 1597 | 1598 | // ... 1599 | case db.NoTransaction: 1600 | // ... 1601 | default: 1602 | // ... 1603 | } 1604 | ``` 1605 | 1606 | 在將變量與常量進行比較的條件語句中,將變量值放在等號運算符的左側: 1607 | 1608 | ```go 1609 | // 較佳: 1610 | if result == "foo" { 1611 | // ... 1612 | } 1613 | ``` 1614 | 1615 | 而不是不太清晰的語句,其中常量首先出現(["Yoda 風格條件"](https://en.wikipedia.org/wiki/Yoda_conditions)): 1616 | 1617 | ```go 1618 | // 不佳: 1619 | if "foo" == result { 1620 | // ... 1621 | } 1622 | ``` 1623 | 1624 | 1625 | 1626 | ### 複製 Copying 1627 | 1628 | 1629 | 1630 | 為了避免意外的別名和類似的錯誤,在從另一個包複製結構體時要小心。例如,同步對象如 `sync.Mutex` 不能被複製。 1631 | 1632 | `bytes.Buffer` 類型包含一個 `[]byte` 切片,並且作為對小字符串的優化,它包含一個小的字節數組,切片可能會引用該數組。如果你複製了一個 `Buffer`,副本中的切片可能會與原始對象中的數組產生別名,導致後續的方法調用有意外的效果。 1633 | 1634 | 一般來說,如果一個類型 `T` 的方法與指針類型 `*T` 相關聯,則不要複製類型 `T` 的值。 1635 | 1636 | ```go 1637 | // 不佳: 1638 | b1 := bytes.Buffer{} 1639 | b2 := b1 1640 | ``` 1641 | 1642 | 調用一個接收值的方法可以隱藏複製行為。當你設計一個 API 時,如果你的結構體包含不應該被複製的字段,你應該通常接收並返回指針類型。 1643 | 1644 | 這些是可以接受的: 1645 | 1646 | ```go 1647 | // 較佳: 1648 | type Record struct { 1649 | buf bytes.Buffer 1650 | // other fields omitted 1651 | } 1652 | 1653 | func New() *Record {...} 1654 | 1655 | func (r *Record) Process(...) {...} 1656 | 1657 | func Consumer(r *Record) {...} 1658 | ``` 1659 | 1660 | 但這些通常是錯誤的: 1661 | 1662 | ```go 1663 | // 不佳: 1664 | type Record struct { 1665 | buf bytes.Buffer 1666 | // other fields omitted 1667 | } 1668 | 1669 | 1670 | func (r Record) Process(...) {...} // Makes a copy of r.buf 1671 | 1672 | func Consumer(r Record) {...} // Makes a copy of r.buf 1673 | ``` 1674 | 1675 | 這個指導原則也適用於複製 `sync.Mutex`。 1676 | 1677 | 1678 | 1679 | ### 不要恐慌 Don't panic 1680 | 1681 | 1682 | 1683 | 不要使用 `panic` 進行正常的錯誤處理。相反,使用 `error` 和多個返回值。參見 [Effective Go 中關於錯誤的部分]。 1684 | 1685 | 在 `package main` 和初始化代碼中,考慮使用 [`log.Exit`] 處理應該終止程序的錯誤(例如,無效配置),因為在許多這樣的情況下,堆棧跟踪不會幫助讀者。請注意,[`log.Exit`] 調用 [`os.Exit`],任何延遲的函數都不會被執行。 1686 | 1687 | 對於表示「不可能」條件的錯誤,即應該在代碼審查和/或測試期間始終被捕獲的錯誤,函數可以合理地返回一個錯誤或調用 [`log.Fatal`]。 1688 | 1689 | **注意:** `log.Fatalf` 不是標準庫日誌。參見 [#logging]。 1690 | 1691 | [Effective Go 中關於錯誤的部分]: http://golang.org/doc/effective_go.html#errors 1692 | [`os.Exit`]: https://pkg.go.dev/os#Exit 1693 | 1694 | 1695 | 1696 | ### 必須函數 Must functions 1697 | 1698 | 在失敗時停止程序的設置輔助函數遵循命名慣例 `MustXYZ`(或 `mustXYZ`)。一般來說,它們應該只在程序啟動初期被調用,而不是像用戶輸入這樣的情況,其中優先使用正常的 Go 錯誤處理。 1699 | 1700 | 這通常出現在僅在[包初始化時](https://golang.org/ref/spec#Package_initialization)調用的函數,用於初始化包級變量(例如 [template.Must](https://golang.org/pkg/text/template/#Must) 和 [regexp.MustCompile](https://golang.org/pkg/regexp/#MustCompile))。 1701 | 1702 | ```go 1703 | // 較佳: 1704 | func MustParse(version string) *Version { 1705 | v, err := Parse(version) 1706 | if err != nil { 1707 | panic(fmt.Sprintf("MustParse(%q) = _, %v", version, err)) 1708 | } 1709 | return v 1710 | } 1711 | 1712 | // Package level "constant". If we wanted to use `Parse`, we would have had to 1713 | // set the value in `init`. 1714 | var DefaultVersion = MustParse("1.2.3") 1715 | ``` 1716 | 1717 | 相同的慣例也可以用在測試輔助函數中,這些函數只停止當前測試(使用 `t.Fatal`)。這樣的輔助函數在創建測試值時經常很方便,例如在[表驅動測試](#table-driven-tests)的結構字段中,因為返回錯誤的函數不能直接分配給結構字段。 1718 | 1719 | ```go 1720 | // 較佳: 1721 | func mustMarshalAny(t *testing.T, m proto.Message) *anypb.Any { 1722 | t.Helper() 1723 | any, err := anypb.New(m) 1724 | if err != nil { 1725 | t.Fatalf("mustMarshalAny(t, m) = %v; want %v", err, nil) 1726 | } 1727 | return any 1728 | } 1729 | 1730 | func TestCreateObject(t *testing.T) { 1731 | tests := []struct{ 1732 | desc string 1733 | data *anypb.Any 1734 | }{ 1735 | { 1736 | desc: "my test case", 1737 | // Creating values directly within table driven test cases. 1738 | data: mustMarshalAny(t, mypb.Object{}), 1739 | }, 1740 | // ... 1741 | } 1742 | // ... 1743 | } 1744 | ``` 1745 | 1746 | 在這兩種情況下,這種模式的價值在於輔助函數可以在「值」上下文中被調用。這些輔助函數不應該在難以確保錯誤會被捕獲的地方調用,或者在應該[檢查](#handle-errors)錯誤的上下文中調用(例如,在許多請求處理程序中)。對於常量輸入,這允許測試輕鬆確保 `Must` 參數格式正確,對於非常量輸入,它允許測試驗證錯誤是否[被適當處理或傳播](best-practices#error-handling)。 1747 | 1748 | 在測試中使用 `Must` 函數時,它們通常應該[標記為測試輔助函數](#mark-test-helpers),並在出錯時調用 `t.Fatal`(參見[測試輔助函數中的錯誤處理](best-practices#test-helper-error-handling)以獲得更多使用該函數的考慮)。 1749 | 1750 | 當[普通的錯誤處理](best-practices#error-handling)是可能的時候(包括一些重構),它們不應該被使用: 1751 | 1752 | ```go 1753 | // 不佳: 1754 | func Version(o *servicepb.Object) (*version.Version, error) { 1755 | // Return error instead of using Must functions. 1756 | v := version.MustParse(o.GetVersionString()) 1757 | return dealiasVersion(v) 1758 | } 1759 | ``` 1760 | 1761 | 1762 | 1763 | ### Goroutine 生命週期 1764 | 1765 | 1766 | 1767 | 當你啟動 goroutines 時,要清楚它們何時或是否會退出。 1768 | 1769 | Goroutines 可以通過阻塞在通道發送或接收上而泄漏。即使沒有其他 goroutine 引用該通道,垃圾收集器也不會終止阻塞在通道上的 goroutine。 1770 | 1771 | 即使 goroutines 沒有泄漏,當它們不再需要時仍然讓它們在執行中,也可能導致其他微妙且難以診斷的問題。在已經關閉的通道上發送會導致恐慌。 1772 | 1773 | ```go 1774 | // 不佳: 1775 | ch := make(chan int) 1776 | ch <- 42 1777 | close(ch) 1778 | ch <- 13 // panic 1779 | ``` 1780 | 1781 | 在「結果不再需要後」修改仍在使用中的輸入可能導致數據競爭。任意長時間地保留 goroutines 在執行中可能導致不可預測的內存使用。 1782 | 1783 | 並發代碼應該寫得使 goroutine 的生命週期明顯。通常這將意味著將與同步相關的代碼限制在函數的範圍內,並將邏輯分解為[同步函數]。如果並發性仍然不明顯,記錄 goroutines 何時以及為什麼退出是很重要的。 1784 | 1785 | 遵循最佳實踐的上下文使用相關代碼通常有助於澄清這一點。它通常是用 `context.Context` 管理的: 1786 | 1787 | ```go 1788 | // 較佳: 1789 | func (w *Worker) Run(ctx context.Context) error { 1790 | // ... 1791 | for item := range w.q { 1792 | // process returns at latest when the context is cancelled. 1793 | go process(ctx, item) 1794 | } 1795 | // ... 1796 | } 1797 | ``` 1798 | 1799 | 還有其他使用原始信號通道如 `chan struct{}`、同步變量、[條件變量][rethinking-slides] 等的變體。重要的是後續維護者能夠明顯地看到 goroutine 的結束。 1800 | 1801 | 相比之下,以下代碼對其啟動的 goroutines 何時結束不夠謹慎: 1802 | 1803 | ```go 1804 | // 不佳: 1805 | func (w *Worker) Run() { 1806 | // ... 1807 | for item := range w.q { 1808 | // process returns when it finishes, if ever, possibly not cleanly 1809 | // handling a state transition or termination of the Go program itself. 1810 | go process(item) 1811 | } 1812 | // ... 1813 | } 1814 | ``` 1815 | 1816 | 這段代碼看起來可能沒問題,但存在幾個潛在問題: 1817 | 1818 | - 代碼在生產中可能有未定義的行為,即使操作系統釋放了資源,程序也可能無法乾淨地終止。 1819 | - 由於代碼的不確定生命週期,測試代碼意義不大。 1820 | - 如上所述,代碼可能泄漏資源。 1821 | 1822 | 另見: 1823 | 1824 | - [永遠不要啟動一個 goroutine 而不知道它將如何停止][cheney-stop] 1825 | - 重新思考傳統並發模式:[幻燈片][rethinking-slides],[視頻][rethinking-video] 1826 | - [Go 程序何時結束] 1827 | 1828 | [同步函數]: #synchronous-functions 1829 | [cheney-stop]: https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop 1830 | [rethinking-slides]: https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view 1831 | [rethinking-video]: https://www.youtube.com/watch?v=5zXAHh5tJqQ 1832 | [Go 程序何時結束]: https://changelog.com/gotime/165 1833 | 1834 | 1835 | 1836 | ### 介面 Interfaces 1837 | 1838 | 1839 | 1840 | Go 介面通常屬於*使用*介面類型值的包,而不是*實現*介面類型的包。實現包應該返回具體的(通常是指針或結構體)類型。這樣,新方法可以添加到實現中,而不需要進行大規模重構。有關更多細節,請參見 [GoTip #49: 接受介面,返回具體類型]。 1841 | 1842 | 不要從使用它的 API 中導出介面的[測試雙重][double types]實現。相反,設計 API 以便可以使用[真實實現]的[公共 API]進行測試。有關更多細節,請參見 [GoTip #42: 編寫用於測試的 Stub]。即使無法使用真實實現,也可能不需要引入完全涵蓋真實類型中所有方法的介面;消費者可以創建一個只包含其需要的方法的介面,如 [GoTip #78: 最小可行介面] 中所示。 1843 | 1844 | 要測試使用 Stubby RPC 客戶端的包,使用真實的客戶端連接。如果在測試中無法運行真實服務器,Google 內部的做法是使用內部的 rpctest 包(即將推出!)獲取到本地[測試雙重]的真實客戶端連接。 1845 | 1846 | 在它們被使用之前不要定義介面(參見 [TotT: 代碼健康:消除 YAGNI 味道][tott-438])。沒有一個現實的使用例子,很難看出介面是否甚至是必要的,更不用說它應該包含哪些方法了。 1847 | 1848 | 如果包的用戶不需要傳遞不同類型的參數,則不要使用介面類型的參數。 1849 | 1850 | 不要導出包的用戶不需要的介面。 1851 | 1852 | **待辦事項:** 編寫一份關於介面的更深入的文檔,並在這裡鏈接它。 1853 | 1854 | [GoTip #42: 編寫用於測試的 Stub]: https://google.github.io/styleguide/go/index.html#gotip 1855 | [GoTip #49: 接受介面,返回具體類型]: https://google.github.io/styleguide/go/index.html#gotip 1856 | [GoTip #78: 最小可行介面]: https://google.github.io/styleguide/go/index.html#gotip 1857 | [真實實現]: best-practices#use-real-transports 1858 | [公共 API]: https://abseil.io/resources/swe-book/html/ch12.html#test_via_public_apis 1859 | [double types]: https://abseil.io/resources/swe-book/html/ch13.html#techniques_for_using_test_doubles 1860 | [測試雙重]: https://abseil.io/resources/swe-book/html/ch13.html#basic_concepts 1861 | [tott-438]: https://testing.googleblog.com/2017/08/code-health-eliminate-yagni-smells.html 1862 | 1863 | ```go 1864 | // 較佳: 1865 | package consumer // consumer.go 1866 | 1867 | type Thinger interface { Thing() bool } 1868 | 1869 | func Foo(t Thinger) string { ... } 1870 | ``` 1871 | 1872 | ```go 1873 | // 較佳: 1874 | package consumer // consumer_test.go 1875 | 1876 | type fakeThinger struct{ ... } 1877 | func (t fakeThinger) Thing() bool { ... } 1878 | ... 1879 | if Foo(fakeThinger{...}) == "x" { ... } 1880 | ``` 1881 | 1882 | ```go 1883 | // 不佳: 1884 | package producer 1885 | 1886 | type Thinger interface { Thing() bool } 1887 | 1888 | type defaultThinger struct{ ... } 1889 | func (t defaultThinger) Thing() bool { ... } 1890 | 1891 | func NewThinger() Thinger { return defaultThinger{ ... } } 1892 | ``` 1893 | 1894 | ```go 1895 | // 較佳: 1896 | package producer 1897 | 1898 | type Thinger struct{ ... } 1899 | func (t Thinger) Thing() bool { ... } 1900 | 1901 | func NewThinger() Thinger { return Thinger{ ... } } 1902 | ``` 1903 | 1904 | 1905 | 1906 | ### 泛型 Generics 1907 | 1908 | 泛型(正式稱為"[類型參數]")在滿足業務需求的地方是允許的。在許多應用中,使用現有語言特性(切片、映射、介面等)的傳統方法同樣可以工作,而不會增加額外的複雜性,所以要謹慎使用以免過早。參見關於[最小機制](guide.md#least-mechanism)的討論。 1909 | 1910 | 當引入使用泛型的導出 API 時,請確保它有適當的文檔。強烈建議包括動機性的可運行[示例]。 1911 | 1912 | 不要僅僅因為你正在實現一個不關心其成員元素類型的算法或數據結構就使用泛型。如果實際上只有一種類型被實例化,首先開始讓你的代碼在那種類型上工作,而不使用泛型。與移除被發現是不必要的抽象相比,後來添加多態性將是直截了當的。 1913 | 1914 | 不要使用泛型來發明特定領域的語言(DSL)。特別是,避免引入可能對讀者造成重大負擔的錯誤處理框架。相反,優先使用既定的[錯誤處理](#errors)實踐。對於測試,要特別小心引入導致不太有用的[測試失敗](#useful-test-failures)的[斷言庫](#assert)或框架。 1915 | 1916 | 一般來說: 1917 | 1918 | - [寫代碼,不要設計類型]。來自 Robert Griesemer 和 Ian Lance Taylor 的 GopherCon 演講。 1919 | - 如果你有幾種類型共享一個有用的統一介面,考慮使用該介面來建模解決方案。可能不需要泛型。 1920 | - 否則,不要依賴 `any` 類型和過度的[類型切換](https://tour.golang.org/methods/16),考慮使用泛型。 1921 | 1922 | 另見: 1923 | 1924 | - [在 Go 中使用泛型],Ian Lance Taylor 的演講 1925 | 1926 | - Go 網頁上的[泛型教程] 1927 | 1928 | [泛型教程]: https://go.dev/doc/tutorial/generics 1929 | [類型參數]: https://go.dev/design/43651-type-parameters 1930 | [在 Go 中使用泛型]: https://www.youtube.com/watch?v=nr8EpUO9jhw 1931 | [寫代碼,不要設計類型]: https://www.youtube.com/watch?v=Pa_e9EeCdy8&t=1250s 1932 | 1933 | 1934 | 1935 | ### 傳遞值 Pass values 1936 | 1937 | 1938 | 1939 | 不要僅僅為了節省幾個字節就在函數參數中傳遞指針。如果一個函數只是作為 `*x` 讀取其參數 `x`,那麼該參數不應該是一個指針。這種情況的常見例子包括傳遞一個字符串的指針(`*string`)或一個介面值的指針(`*io.Reader`)。在這兩種情況下,值本身是固定大小,可以直接傳遞。 1940 | 1941 | 這條建議不適用於大型結構體,或者即使是小型結構體也可能增加大小。特別是,協議緩衝消息通常應該通過指針而不是值來處理。指針類型滿足 `proto.Message` 介面(被 `proto.Marshal`、`protocmp.Transform` 等接受),且協議緩衝消息可能相當大,並且經常隨時間增長。 1942 | 1943 | 1944 | 1945 | ### 接收器類型 Receiver type 1946 | 1947 | 1948 | 1949 | [方法接收器]可以作為值或指針傳遞,就像它是一個普通函數參數一樣。在兩者之間的選擇基於方法應該是哪個[方法集]的一部分。 1950 | 1951 | [方法接收器]: https://golang.org/ref/spec#Method_declarations 1952 | [方法集]: https://golang.org/ref/spec#Method_sets 1953 | 1954 | **正確性勝過速度或簡單性。** 有些情況下你必須使用指針值。在其他情況下,對於大型類型或作為未來證明如果你不確定代碼將如何增長,選擇指針;對於簡單的[普通舊數據],使用值。 1955 | 1956 | 下面的列表進一步詳細說明了每種情況: 1957 | 1958 | - 如果接收器是一個切片且方法不重新切片或重新分配切片,使用值而不是指針。 1959 | 1960 | ```go 1961 | // 較佳: 1962 | type Buffer []byte 1963 | 1964 | func (b Buffer) Len() int { return len(b) } 1965 | ``` 1966 | 1967 | - 如果方法需要改變接收器,接收器必須是一個指針。 1968 | 1969 | ```go 1970 | // 較佳: 1971 | type Counter int 1972 | 1973 | func (c *Counter) Inc() { *c++ } 1974 | 1975 | // 參見 https://pkg.go.dev/container/heap。 1976 | type Queue []Item 1977 | 1978 | func (q *Queue) Push(x Item) { *q = append([]Item{x}, *q...) } 1979 | ``` 1980 | 1981 | - 如果接收器是一個包含[不能安全複製]的字段的結構體,使用指針接收器。常見例子是 [`sync.Mutex`] 和其他同步類型。 1982 | 1983 | ```go 1984 | // 較佳: 1985 | type Counter struct { 1986 | mu sync.Mutex 1987 | total int 1988 | } 1989 | 1990 | func (c *Counter) Inc() { 1991 | c.mu.Lock() 1992 | defer c.mu.Unlock() 1993 | c.total++ 1994 | } 1995 | ``` 1996 | 1997 | **提示:** 檢查類型的 [Godoc] 以獲取關於它是否安全或不安全複製的信息。 1998 | 1999 | - 如果接收器是一個“大型”結構體或數組,指針接收器可能更有效。傳遞一個結構體等同於將其所有字段或元素作為參數傳遞給方法。如果這看起來太大而無法[按值傳遞],指針是一個好選擇。 2000 | 2001 | - 對於將與修改接收器的其他函數同時調用或運行的方法,如果這些修改不應該對你的方法可見,使用值;否則使用指針。 2002 | 2003 | - 如果接收器是一個結構體或數組,其任何元素是指向可能被改變的東西的指針,優先選擇指針接收器以使可變性的意圖對讀者清晰。 2004 | 2005 | ```go 2006 | // 較佳: 2007 | type Counter struct { 2008 | m *Metric 2009 | } 2010 | 2011 | func (c *Counter) Inc() { 2012 | c.m.Add(1) 2013 | } 2014 | ``` 2015 | 2016 | - 如果接收器是一個[內建類型],如整數或字符串,不需要被修改,使用值。 2017 | 2018 | ```go 2019 | // 較佳: 2020 | type User string 2021 | 2022 | func (u User) String() { return string(u) } 2023 | ``` 2024 | 2025 | - 如果接收器是一個映射、函數或通道,使用值而不是指針。 2026 | 2027 | ```go 2028 | // 較佳: 2029 | // 參見 https://pkg.go.dev/net/http#Header。 2030 | type Header map[string][]string 2031 | 2032 | func (h Header) Add(key, value string) { /* 省略 */ } 2033 | ``` 2034 | 2035 | - 如果接收器是一個“小型”數組或結構體,本質上是一個沒有可變字段和指針的值類型,值接收器通常是正確的選擇。 2036 | 2037 | ```go 2038 | // 較佳: 2039 | // 參見 https://pkg.go.dev/time#Time。 2040 | type Time struct { /* 省略 */ } 2041 | 2042 | func (t Time) Add(d Duration) Time { /* 省略 */ } 2043 | ``` 2044 | 2045 | - 如有疑問,使用指針接收器。 2046 | 2047 | 作為一般指導原則,傾向於使一個類型的方法要麼全部是指針方法,要麼全部是值方法。 2048 | 2049 | **注意:** 關於將值或指針傳遞給函數是否會影響性能有很多誤解。編譯器可以選擇將值的指針傳遞到棧上,也可以在棧上複製值,但在大多數情況下,這些考慮不應該超過代碼的可讀性和正確性。當性能確實重要時,重要的是在決定哪種方法性能更好之前,使用現實的基準測試對兩種方法進行分析。 2050 | 2051 | [普通舊數據]: https://en.wikipedia.org/wiki/Passive_data_structure 2052 | [`sync.Mutex`]: https://pkg.go.dev/sync#Mutex 2053 | [內建類型]: https://pkg.go.dev/builtin 2054 | 2055 | 2056 | 2057 | ### `switch` 與 `break` 2058 | 2059 | 2060 | 2061 | 不要在 `switch` 語句的末尾使用沒有目標標籤的 `break` 語句;它們是多餘的。與 C 和 Java 不同,Go 中的 `switch` 語句會自動中斷,需要 `fallthrough` 語句來實現 C 風格的行為。如果你想澄清一個空語句的目的,請使用註釋而不是 `break`。 2062 | 2063 | ```go 2064 | // 較佳: 2065 | switch x { 2066 | case "A", "B": 2067 | buf.WriteString(x) 2068 | case "C": 2069 | // handled outside of the switch statement 2070 | default: 2071 | return fmt.Errorf("unknown value: %q", x) 2072 | } 2073 | ``` 2074 | 2075 | ```go 2076 | // 不佳: 2077 | switch x { 2078 | case "A", "B": 2079 | buf.WriteString(x) 2080 | break // 這個 break 是多餘的 2081 | case "C": 2082 | break // 這個 break 是多餘的 2083 | default: 2084 | return fmt.Errorf("unknown value: %q", x) 2085 | } 2086 | ``` 2087 | 2088 | > **注意:** 如果 `switch` 語句在一個 `for` 循環內,那麼在 `switch` 內使用 `break` 不會退出包圍的 `for` 循環。 2089 | > 2090 | > ```go 2091 | > for { 2092 | > switch x { 2093 | > case "A": 2094 | > break // 退出 switch,不是循環 2095 | > } 2096 | > } 2097 | > ``` 2098 | > 2099 | > 要跳出包圍的循環,請在 `for` 語句上使用標籤: 2100 | > 2101 | > ```go 2102 | > loop: 2103 | > for { 2104 | > switch x { 2105 | > case "A": 2106 | > break loop // 退出循環 2107 | > } 2108 | > } 2109 | > ``` 2110 | 2111 | 2112 | 2113 | ### 同步函數 Synchronous functions 2114 | 2115 | 2116 | 2117 | 同步函數直接返回它們的結果,並在返回之前完成任何回調或通道操作。相比於異步函數,更推薦使用同步函數。 2118 | 2119 | 同步函數將 goroutine 局限在調用內。這有助於推理它們的生命週期,並避免泄漏和數據競爭。同步函數也更容易測試,因為調用者可以傳遞輸入並檢查輸出,無需輪詢或同步。 2120 | 2121 | 如果必要,調用者可以通過在單獨的 goroutine 中調用函數來添加並發性。然而,在調用方側移除不必要的並發性有時是相當困難(有時甚至是不可能的)。 2122 | 2123 | 另見: 2124 | 2125 | - Bryan Mills 的演講 "Rethinking Classical Concurrency Patterns":[幻燈片][rethinking-slides],[視頻][rethinking-video] 2126 | 2127 | 2128 | 2129 | ### 類型別名 Type aliases 2130 | 2131 | 2132 | 2133 | 使用*類型定義* `type T1 T2` 來定義一個新類型。使用[*類型別名*] `type T1 = T2` 來引用一個現有的類型,而不定義一個新類型。類型別名很少見;它們的主要用途是幫助將包遷移到新的源代碼位置。當不需要時,不要使用類型別名。 2134 | 2135 | [*類型別名*]: http://golang.org/ref/spec#Type_declarations 2136 | 2137 | 2138 | 2139 | ### 使用 %q 2140 | 2141 | 2142 | 2143 | Go 的格式化函數(`fmt.Printf` 等)有一個 `%q` 動詞,它會在雙引號內打印字符串。 2144 | 2145 | ```go 2146 | // 較佳: 2147 | fmt.Printf("value %q looks like English text", someText) 2148 | ``` 2149 | 2150 | 優先使用 `%q` 而不是手動進行等效操作,使用 `%s`: 2151 | 2152 | ```go 2153 | // 不佳: 2154 | fmt.Printf("value \"%s\" looks like English text", someText) 2155 | // 也避免手動用單引號包圍字符串: 2156 | fmt.Printf("value '%s' looks like English text", someText) 2157 | ``` 2158 | 2159 | 在為人類準備的輸出中,當輸入值可能為空或包含控制字符時,推薦使用 `%q`。一個無聲的空字符串可能很難被注意到,但 `""` 清楚地突出顯示為此。 2160 | 2161 | 2162 | 2163 | ### 使用 any 2164 | 2165 | Go 1.18 引入了 `any` 類型作為 `interface{}` 的[別名]。因為它是一個別名,`any` 在許多情況下等同於 `interface{}`,在其他情況下可以通過顯式轉換輕鬆互換。在新代碼中優先使用 `any`。 2166 | 2167 | [別名]: https://go.googlesource.com/proposal/+/master/design/18130-type-alias.md 2168 | 2169 | ## 常用庫 Common libraries 2170 | 2171 | 2172 | 2173 | ### 標誌 Flags 2174 | 2175 | 2176 | 2177 | Google 代碼庫中的 Go 程序使用 [標準 `flag` 包] 的內部變體。它具有類似的接口,但與內部 Google 系統良好互操作。Go 二進制文件中的標誌名稱應該優先使用下劃線來分隔單詞,儘管持有標誌值的變量應該遵循標準 Go 名稱風格([混合大小寫])。具體來說,標誌名稱應該是蛇形大小寫,變量名稱懲罰是駝峰大小寫的等效名稱。 2178 | 2179 | ```go 2180 | // 較佳: 2181 | var ( 2182 | pollInterval = flag.Duration("poll_interval", time.Minute, "Interval to use for polling.") 2183 | ) 2184 | ``` 2185 | 2186 | ```go 2187 | // 不佳: 2188 | var ( 2189 | poll_interval = flag.Int("pollIntervalSeconds", 60, "Interval to use for polling in seconds.") 2190 | ) 2191 | ``` 2192 | 2193 | 標誌只能在 `package main` 或等效包中定義。 2194 | 2195 | 通用包應該使用 Go API 進行配置,而不是穿透到命令行界面;不要讓導入一個庫作為副作用導出新的標誌。也就是說,優先考慮顯式函數參數或結構體字段賦值,或者在最嚴格的審查下不太頻繁地導出全局變量。在極少數必要打破此規則的情況下,標誌名稱必須清楚地指示它配置的包。 2196 | 2197 | 如果你的標誌是全局變量,請將它們放在自己的 `var` 組中,跟在導入部分之後。 2198 | 2199 | 有關創建[複雜 CLI]與子命令的最佳實踐的額外討論。 2200 | 2201 | 另見: 2202 | 2203 | - [每週提示 #45: 避免標誌,特別是在庫代碼中][totw-45] 2204 | - [Go 提示 #10: 配置結構體和標誌](https://google.github.io/styleguide/go/index.html#gotip) 2205 | - [Go 提示 #80: 依賴注入原則](https://google.github.io/styleguide/go/index.html#gotip) 2206 | 2207 | [標準 `flag` 包]: https://golang.org/pkg/flag/ 2208 | [複雜 CLI]: best-practices#complex-clis 2209 | [totw-45]: https://abseil.io/tips/45 2210 | 2211 | 2212 | 2213 | ### 日誌記錄 2214 | 2215 | Google 代碼庫中的 Go 程序使用標準 [`log`] 包的一個變體。它具有類似但更強大的接口,並且與內部 Google 系統良好互操作。這個庫的開源版本可作為 [package `glog`] 獲得,開源的 Google 項目可以使用它,但本指南始終將其稱為 `log`。 2216 | 2217 | **注意:** 對於異常程序退出,這個庫使用 `log.Fatal` 來中止並帶有堆棧跟蹤,使用 `log.Exit` 來停止但不帶堆棧跟蹤。標準庫中的 `log.Panic` 函數在這裡不存在。 2218 | 2219 | **提示:** `log.Info(v)` 等同於 `log.Infof("%v", v)`,其他日誌級別也是如此。當你沒有格式化要做時,優先使用非格式化版本。 2220 | 2221 | 另見: 2222 | 2223 | - 關於[錯誤日誌記錄](best-practices#error-logging)和[自定義詳細級別](best-practices#vlog)的最佳實踐 2224 | - 何時以及如何使用 log 包來[停止程序](best-practices#checks-and-panics) 2225 | 2226 | [`log`]: https://pkg.go.dev/log 2227 | [package `glog`]: https://pkg.go.dev/github.com/golang/glog 2228 | [`log.Exit`]: https://pkg.go.dev/github.com/golang/glog#Exit 2229 | [`log.Fatal`]: https://pkg.go.dev/github.com/golang/glog#Fatal 2230 | 2231 | 2232 | 2233 | ### 上下文 Contexts 2234 | 2235 | 2236 | 2237 | [`context.Context`] 類型的值在 API 和進程邊界之間傳遞安全憑證、追蹤信息、截止時間和取消信號。與在 Google 代碼庫中使用線程局部存儲的 C++ 和 Java 不同,Go 程序明確地沿著從接收 RPC 和 HTTP 請求到發出請求的整個函數調用鏈傳遞上下文。 2238 | 2239 | [`context.Context`]: https://pkg.go.dev/context 2240 | 2241 | 當傳遞給函數或方法時,`context.Context` 總是第一個參數。 2242 | 2243 | ```go 2244 | func F(ctx context.Context /* other arguments */) {} 2245 | ``` 2246 | 2247 | 例外情況有: 2248 | 2249 | - 在 HTTP 處理器中,上下文來自 [`req.Context()`](https://pkg.go.dev/net/http#Request.Context)。 2250 | - 在流式 RPC 方法中,上下文來自流。 2251 | 2252 | 使用 gRPC 流的代碼從生成的服務器類型中的 `Context()` 方法訪問上下文,該方法實現了 `grpc.ServerStream`。參見 [gRPC 生成代碼文檔](https://grpc.io/docs/languages/go/generated-code/)。 2253 | 2254 | - 在入口點函數中(見下面的例子),使用 [`context.Background()`](https://pkg.go.dev/context/#Background)。 2255 | 2256 | - 在二進制目標中:`main` 2257 | - 在通用代碼和庫中:`init` 2258 | - 在測試中:`TestXXX`、`BenchmarkXXX`、`FuzzXXX` 2259 | 2260 | > **注意**:在調用鏈中間的代碼很少需要使用 `context.Background()` 創建自己的基礎上下文。總是優先從你的調用者那裡獲取上下文,除非它是錯誤的上下文。 2261 | > 2262 | > 你可能會遇到服務器庫(Stubby、gRPC 或 Google 的 Go 服務器框架中的 HTTP 的實現),它們會為每個請求構造一個新的上下文對象。這些上下文立即填充了來自傳入請求的信息,因此當傳遞給請求處理器時,上下文的附加值已經跨網絡邊界從客戶端調用者傳播到它。此外,這些上下文的生命週期限於請求的生命週期:當請求完成時,上下文被取消。 2263 | > 2264 | > 除非你正在實現一個服務器框架,否則你不應該在庫代碼中使用 `context.Background()` 創建上下文。相反,如果有現有的上下文可用,優先使用下面提到的上下文分離。如果你認為在入口點函數之外需要 `context.Background()`,在承諾實施之前請與 Google Go 風格郵件列表討論。 2265 | 2266 | `context.Context` 在函數中排在第一位的慣例也適用於測試輔助函數。 2267 | 2268 | ```go 2269 | // 較佳: 2270 | func readTestFile(ctx context.Context, t *testing.T, path string) string {} 2271 | ``` 2272 | 2273 | 不要將上下文成員添加到結構體類型中。相反,為類型上需要傳遞它的每個方法添加一個上下文參數。唯一的例外是對於其簽名必須與標準庫或 Google 控制之外的第三方庫中的接口匹配的方法。這種情況非常罕見,在實施和可讀性審查之前應與 Google Go 風格郵件列表討論。 2274 | 2275 | Google 代碼庫中必須啟動可以在父上下文被取消後運行的後台操作的代碼可以使用內部包進行分離。關注 [問題 #40221](https://github.com/golang/go/issues/40221) 以獲取開源替代方案的討論。 2276 | 2277 | 由於上下文是不可變的,將相同的上下文傳遞給共享相同截止時間、取消信號、憑證、父追蹤等的多個調用是可以的。 2278 | 2279 | 另見: 2280 | 2281 | - [上下文和結構體] 2282 | 2283 | [上下文和結構體]: https://go.dev/blog/context-and-structs 2284 | 2285 | 2286 | 2287 | #### 自定義上下文 Custom contexts 2288 | 2289 | 不要創建自定義上下文類型或在函數簽名中使用除 `context.Context` 以外的接口。這條規則沒有例外。 2290 | 2291 | 想像如果每個團隊都有一個自定義上下文。從包 `p` 到包 `q` 的每個函數調用都必須確定如何將 `p.Context` 轉換為 `q.Context`,對於所有的包 `p` 和 `q` 組合。這對人類來說是不切實際和容易出錯的,它使得添加上下文參數的自動重構幾乎不可能。 2292 | 2293 | 如果你有應用數據要傳遞,將它放在參數中、接收器中、全局變量中,或者如果它真的屬於那裡,則放在 `Context` 值中。創建自己的上下文類型是不可接受的,因為它破壞了 Go 團隊使 Go 程序在生產中正常工作的能力。 2294 | 2295 | 2296 | 2297 | ### crypto/rand 2298 | 2299 | 2300 | 2301 | 不要使用包 `math/rand` 來生成密鑰,即使是臨時的也不行。如果未種子化,生成器是完全可預測的。使用 `time.Nanoseconds()` 作為種子,只有幾位的熵。相反,使用 `crypto/rand` 的 Reader,如果你需要文本,輸出為十六進制或 base64。 2302 | 2303 | ```go 2304 | // 較佳: 2305 | import ( 2306 | "crypto/rand" 2307 | // "encoding/base64" 2308 | // "encoding/hex" 2309 | "fmt" 2310 | 2311 | // ... 2312 | ) 2313 | 2314 | func Key() string { 2315 | buf := make([]byte, 16) 2316 | if _, err := rand.Read(buf); err != nil { 2317 | log.Fatalf("Out of randomness, should never happen: %v", err) 2318 | } 2319 | return fmt.Sprintf("%x", buf) 2320 | // or hex.EncodeToString(buf) 2321 | // or base64.StdEncoding.EncodeToString(buf) 2322 | } 2323 | ``` 2324 | 2325 | **注意:** `log.Fatalf` 不是標準庫的 log。參見 [#logging]。 2326 | 2327 | 2328 | 2329 | ## 有用的測試失敗信息 2330 | 2331 | 2332 | 2333 | 在不閱讀測試源碼的情況下,應該能夠診斷出測試的失敗原因。測試應該提供有幫助的消息來詳細說明: 2334 | 2335 | - 導致失敗的原因 2336 | - 什麼輸入導致了錯誤 2337 | - 實際結果 2338 | - 預期的結果 2339 | 2340 | 下面概述了實現此目標的具體慣例。 2341 | 2342 | 2343 | 2344 | ### 斷言庫 Assertion libraries 2345 | 2346 | 2347 | 2348 | 不要創建“斷言庫”作為測試的輔助工具。 2349 | 2350 | 斷言庫是試圖在測試中結合驗證和產生失敗消息的庫(儘管相同的陷阱也可能適用於其他測試輔助工具)。有關測試輔助工具和斷言庫之間區別的更多信息,請參見[最佳實踐](best-practices#test-functions)。 2351 | 2352 | ```go 2353 | // 不佳: 2354 | var obj BlogPost 2355 | 2356 | assert.IsNotNil(t, "obj", obj) 2357 | assert.StringEq(t, "obj.Type", obj.Type, "blogPost") 2358 | assert.IntEq(t, "obj.Comments", obj.Comments, 2) 2359 | assert.StringNotEq(t, "obj.Body", obj.Body, "") 2360 | ``` 2361 | 2362 | 斷言庫傾向於要麼提前停止測試(如果 `assert` 調用了 `t.Fatalf` 或 `panic`),要麼省略了關於測試正確部分的相關信息: 2363 | 2364 | ```go 2365 | // 不佳: 2366 | package assert 2367 | 2368 | func IsNotNil(t *testing.T, name string, val any) { 2369 | if val == nil { 2370 | t.Fatalf("Data %s = nil, want not nil", name) 2371 | } 2372 | } 2373 | 2374 | func StringEq(t *testing.T, name, got, want string) { 2375 | if got != want { 2376 | t.Fatalf("Data %s = %q, want %q", name, got, want) 2377 | } 2378 | } 2379 | ``` 2380 | 2381 | 複雜的斷言函數通常不提供[有用的失敗消息]和存在於測試函數內的上下文。過多的斷言函數和庫導致了開發者體驗的碎片化:我應該使用哪個斷言庫,它應該輸出什麼樣的輸出格式等等?碎片化產生了不必要的混亂,特別是對於庫維護者和大規模變更的作者,他們負責修復潛在的下游破壞。不要創建一個特定於領域的測試語言,而應該使用 Go 本身。 2382 | 2383 | 斷言庫經常將比較和相等檢查分離出來。優先使用標準庫,如 [`cmp`] 和 [`fmt`]: 2384 | 2385 | ```go 2386 | // 較佳: 2387 | var got BlogPost 2388 | 2389 | want := BlogPost{ 2390 | Comments: 2, 2391 | Body: "Hello, world!", 2392 | } 2393 | 2394 | if !cmp.Equal(got, want) { 2395 | t.Errorf("Blog post = %v, want = %v", got, want) 2396 | } 2397 | ``` 2398 | 2399 | 對於更具領域特定的比較輔助工具,優先返回一個值或錯誤,這可以在測試的失敗消息中使用,而不是傳遞 `*testing.T` 並調用其錯誤報告方法: 2400 | 2401 | ```go 2402 | // 較佳: 2403 | func postLength(p BlogPost) int { return len(p.Body) } 2404 | 2405 | func TestBlogPost_VeritableRant(t *testing.T) { 2406 | post := BlogPost{Body: "I am Gunnery Sergeant Hartman, your senior drill instructor."} 2407 | 2408 | if got, want := postLength(post), 60; got != want { 2409 | t.Errorf("Length of post = %v, want %v", got, want) 2410 | } 2411 | } 2412 | ``` 2413 | 2414 | **最佳實踐:** 如果 `postLength` 不是微不足道的,直接對其進行測試是有意義的,獨立於使用它的任何測試。 2415 | 2416 | 另見: 2417 | 2418 | - [相等性比較和差異](#types-of-equality) 2419 | - [打印差異](#print-diffs) 2420 | - 有關測試輔助工具和斷言輔助工具之間區別的更多信息,請參見[最佳實踐](best-practices#test-functions) 2421 | 2422 | [有用的失敗消息]: #useful-test-failures 2423 | [`fmt`]: https://golang.org/pkg/fmt/ 2424 | 2425 | 2426 | 2427 | ### 指明函數 Identify the function 2428 | 2429 | 在大多數測試中,失敗消息應該包括失敗的函數名稱,即使從測試函數的名稱看似顯而易見。具體來說,你的失敗消息應該是 `YourFunc(%v) = %v, want %v` 而不僅僅是 `got %v, want %v`。 2430 | 2431 | 2432 | 2433 | ### 指明輸入 Identify the input 2434 | 2435 | 在大多數測試中,如果輸入短小,失敗消息應該包括函數輸入。如果輸入的相關屬性不明顯(例如,因為輸入很大或不透明),你應該用正在測試的內容描述來命名你的測試用例,並將描述作為錯誤消息的一部分打印出來。 2436 | 2437 | 2438 | 2439 | ### 先得到再期望 Got before want 2440 | 2441 | 測試輸出應該包括函數返回的實際值,然後再打印期望的值。打印測試輸出的標準格式是 `YourFunc(%v) = %v, want %v`。在你會寫“實際”和“期望”的地方,優先使用“got”和“want”。 2442 | 2443 | 對於差異,方向性不那麼明顯,因此包括一個鍵以幫助解釋失敗是很重要的。參見[打印差異的部分]。無論你在失敗消息中使用哪種差異順序,你應該明確地指出它,因為現有代碼對於排序是不一致的。 2444 | 2445 | [打印差異的部分]: #print-diffs 2446 | 2447 | 2448 | 2449 | ### 完整結構比較 Full structure comparisons 2450 | 2451 | 如果你的函數返回一個結構體(或任何具有多個字段的數據類型,如切片、數組和映射),避免編寫進行手工逐字段比較結構體的測試代碼。相反,構造你期望你的函數返回的數據,並直接使用[深度比較]進行比較。 2452 | 2453 | **注意:** 如果你的數據包含不相關的字段,這些字段會掩蓋測試的意圖,則此建議不適用。 2454 | 2455 | 如果你的結構體需要進行大致相等(或等價類型的語義)比較,或者它包含無法進行相等比較的字段(例如,如果其中一個字段是 `io.Reader`),使用 [`cmp.Diff`] 或 [`cmp.Equal`] 比較並配合 [`cmpopts`] 選項,如 [`cmpopts.IgnoreInterfaces`],可能滿足你的需求([範例](https://play.golang.org/p/vrCUNVfxsvF))。 2456 | 2457 | 如果你的函數返回多個返回值,你不需要在比較它們之前將這些值包裝在一個結構體中。只需單獨比較返回值並打印它們。 2458 | 2459 | ```go 2460 | // 較佳: 2461 | val, multi, tail, err := strconv.UnquoteChar(`\"Fran & Freddie's Diner\"`, '"') 2462 | if err != nil { 2463 | t.Fatalf(...) 2464 | } 2465 | if val != `"` { 2466 | t.Errorf(...) 2467 | } 2468 | if multi { 2469 | t.Errorf(...) 2470 | } 2471 | if tail != `Fran & Freddie's Diner"` { 2472 | t.Errorf(...) 2473 | } 2474 | ``` 2475 | 2476 | [深度比較]: #types-of-equality 2477 | [`cmpopts`]: https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts 2478 | [`cmpopts.IgnoreInterfaces`]: https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts#IgnoreInterfaces 2479 | 2480 | 2481 | 2482 | ### 比較穩定的結果 Compare stable results 2483 | 2484 | 避免比較可能依賴於你不擁有的套件輸出穩定性的結果。相反,測試應該比較在語義上相關的資訊,這些資訊是穩定的且能抵抗依賴性的變化。對於返回格式化字符串或序列化字節的功能,通常不應假設輸出是穩定的。 2485 | 2486 | 例如,[`json.Marshal`] 可以改變(並且在過去已經改變過)它發出的特定字節。如果 `json` 套件改變了它序列化字節的方式,那麼對 JSON 字符串進行字符串等值比較的測試可能會失敗。相反,一個更穩健的測試將解析 JSON 字符串的內容,並確保它在語義上等同於某些預期的數據結構。 2487 | 2488 | [`json.Marshal`]: https://golang.org/pkg/encoding/json/#Marshal 2489 | 2490 | 2491 | 2492 | ### 繼續進行 Keep going 2493 | 2494 | 即使在失敗之後,測試也應該盡可能地繼續進行,以便在單次運行中打印出所有失敗的檢查。這樣,正在修復失敗測試的開發者就不必在修復每個錯誤後重新運行測試來找到下一個錯誤。 2495 | 2496 | 對於報告不匹配,優先調用 `t.Error` 而不是 `t.Fatal`。當比較一個函數輸出的幾個不同屬性時,對於這些比較中的每一個都使用 `t.Error`。 2497 | 2498 | 調用 `t.Fatal` 主要用於報告意外的錯誤條件,當後續的比較失敗不再有意義時。 2499 | 2500 | 對於表驅動測試,考慮使用子測試並使用 `t.Fatal` 而不是 `t.Error` 和 `continue`。另見 2501 | [GoTip #25: Subtests: Making Your Tests Lean](https://google.github.io/styleguide/go/index.html#gotip)。 2502 | 2503 | **最佳實踐:** 關於何時應該使用 `t.Fatal` 的更多討論,請參見 2504 | [最佳實踐](best-practices#t-fatal)。 2505 | 2506 | 2507 | 2508 | ### 等值比較和差異 Equality comparison and diffs 2509 | 2510 | `==` 運算子使用[語言定義的比較]來評估等值。標量值(數字、布林值等)基於它們的值進行比較,但只有某些結構體和介面可以以這種方式比較。指針基於它們是否指向相同的變數進行比較,而不是基於它們指向的值的等值。 2511 | 2512 | [`cmp`] 套件可以比較 `==` 無法適當處理的更複雜的數據結構,如切片。使用 [`cmp.Equal`] 進行等值比較和 [`cmp.Diff`] 獲取對象之間的人類可讀差異。 2513 | 2514 | ```go 2515 | // 較佳: 2516 | want := &Doc{ 2517 | Type: "blogPost", 2518 | Comments: 2, 2519 | Body: "This is the post body.", 2520 | Authors: []string{"isaac", "albert", "emmy"}, 2521 | } 2522 | if !cmp.Equal(got, want) { 2523 | t.Errorf("AddPost() = %+v, want %+v", got, want) 2524 | } 2525 | ``` 2526 | 2527 | 作為通用比較庫,`cmp` 可能不知道如何比較某些類型。例如,它只能在傳遞了 [`protocmp.Transform`] 選項的情況下比較協議緩衝消息。 2528 | 2529 | 2530 | 2531 | ```go 2532 | // 較佳: 2533 | if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { 2534 | t.Errorf("Foo() returned unexpected difference in protobuf messages (-want +got):\n%s", diff) 2535 | } 2536 | ``` 2537 | 2538 | 儘管 `cmp` 套件不是 Go 標準庫的一部分,但它由 Go 團隊維護,應該隨著時間產生穩定的等值結果。 2539 | 它是用戶可配置的,應該滿足大多數比較需求。 2540 | 2541 | [語言定義的比較]: http://golang.org/ref/spec#Comparison_operators 2542 | [`cmp`]: https://pkg.go.dev/github.com/google/go-cmp/cmp 2543 | [`cmp.Equal`]: https://pkg.go.dev/github.com/google/go-cmp/cmp#Equal 2544 | [`cmp.Diff`]: https://pkg.go.dev/github.com/google/go-cmp/cmp#Diff 2545 | [`protocmp.Transform`]: https://pkg.go.dev/google.golang.org/protobuf/testing/protocmp#Transform 2546 | 2547 | 現有代碼可能使用以下較舊的庫,並可能繼續使用它們以保持一致性: 2548 | 2549 | - [`pretty`] 產生美觀的差異報告。然而,它相當故意地將視覺表示相同的值視為等值。特別是,`pretty` 不會捕捉到 nil 切片和空切片之間的差異,對於具有相同字段的不同介面實現不敏感,並且可以使用嵌套映射作為與結構體值比較的基礎。它還在產生差異之前將整個值序列化為字符串,因此不適合比較大值。默認情況下,它比較未導出的字段,這使它對依賴性中的實現細節的變化很敏感。因此,不適合在 protobuf 消息上使用 `pretty`。 2550 | 2551 | [`pretty`]: https://pkg.go.dev/github.com/kylelemons/godebug/pretty 2552 | 2553 | 優先為新代碼使用 `cmp`,並且值得考慮在實際可行的情況下更新舊代碼以使用 `cmp`。 2554 | 2555 | 較舊的代碼可能使用標準庫 `reflect.DeepEqual` 函數來比較複雜結構。不應使用 `reflect.DeepEqual` 進行等值檢查,因為它對未導出字段和其他實現細節的變化敏感。使用 `reflect.DeepEqual` 的代碼應該更新為上述庫之一。 2556 | 2557 | **注意:** `cmp` 套件是為測試而設計的,而不是生產使用。因此,當它懷疑比較執行不正確時,它可能會恐慌,以向用戶提供指導,如何改進測試以使其不那麼脆弱。鑑於 cmp 傾向於恐慌,這使它不適合在生產中使用的代碼,因為偶發的恐慌可能是致命的。 2558 | 2559 | 2560 | 2561 | ### 細節層次 Level of detail 2562 | 2563 | 傳統的失敗訊息,適用於大多數 Go 測試,是 2564 | `YourFunc(%v) = %v, want %v`。然而,在某些情況下,可能需要更多或更少的細節: 2565 | 2566 | - 執行複雜互動的測試應該描述這些互動。例如,如果同一個`YourFunc`被多次呼叫,應該識別哪次呼叫導致測試失敗。如果系統的額外狀態很重要,應該在失敗輸出中包含這些資訊(或至少在日誌中)。 2567 | - 如果數據是一個包含大量樣板的複雜結構,只描述訊息中重要的部分是可以接受的,但不要過度隱藏數據。 2568 | - 設置失敗不需要同樣的細節層次。如果一個測試助手填充了一個 Spanner 表,但 Spanner 當機了,你可能不需要包含你打算儲存在數據庫中的哪個測試輸入。通常`t.Fatalf("Setup: Failed to set up test database: %s", err)`就足夠有幫助來解決問題。 2569 | 2570 | **提示:** 在開發過程中觸發你的失敗模式。審視失敗訊息的樣子,以及維護者是否能有效處理失敗。 2571 | 2572 | 以下是一些清晰重現測試輸入和輸出的技巧: 2573 | 2574 | - 在打印字符串數據時,[`%q`經常很有用](#use-percent-q)來強調值的重要性,並更容易發現錯誤的值。 2575 | - 在打印(小)結構時,`%+v`可能比`%v`更有用。 2576 | - 當大值的驗證失敗時,[打印差異](#print-diffs)可以使理解失敗變得更容易。 2577 | 2578 | 2579 | 2580 | ### 打印差異 Print diffs 2581 | 2582 | 如果你的函數返回大量輸出,當你的測試失敗時,閱讀失敗訊息的人可能很難找到差異。不要打印返回值和期望值,而是製作一個差異比較。 2583 | 2584 | 為了計算這些值的差異,首選使用`cmp.Diff`,特別是對於新的測試和新的代碼,但也可以使用其他工具。有關每個函數的優缺點,請參見[等值類型]的指南。 2585 | 2586 | - [`cmp.Diff`] 2587 | 2588 | - [`pretty.Compare`] 2589 | 2590 | 你可以使用[`diff`]包來比較多行字符串或字符串列表。你可以將其作為其他類型差異的構建塊。 2591 | 2592 | [等值類型]: #types-of-equality 2593 | [`diff`]: https://pkg.go.dev/github.com/kylelemons/godebug/diff 2594 | [`pretty.Compare`]: https://pkg.go.dev/github.com/kylelemons/godebug/pretty#Compare 2595 | 2596 | 在你的失敗訊息中添加一些文本,解釋差異的方向。 2597 | 2598 | 2601 | 2602 | - 當你使用`cmp`、`pretty`和`diff`包時(如果你將`(want, got)`傳遞給函數),像`diff (-want +got)`這樣的東西是好的,因為你添加到格式字符串的`-`和`+`將與實際出現在差異行開頭的`-`和`+`匹配。如果你將`(got, want)`傳遞給你的函數,正確的鍵將是`(-got +want)`。 2603 | 2604 | - `messagediff`包使用不同的輸出格式,所以當你使用它時(如果你將`(want, got)`傳遞給函數),`diff (want -> got)`訊息是合適的,因為箭頭的方向將與“修改”行中的箭頭方向匹配。 2605 | 2606 | 差異將跨越多行,所以你應該在打印差異之前打印一個換行符。 2607 | 2608 | 2609 | 2610 | ### 測試錯誤語義 Test error semantics 2611 | 2612 | 當單元測試進行字符串比較或使用普通的`cmp`來檢查特定輸入是否返回特定類型的錯誤時,如果將來這些錯誤訊息被改寫,你可能會發現你的測試很脆弱。由於這可能將你的單元測試變成變更檢測器(參見[ToTT: 考慮有害的變更檢測器測試][tott-350]),不要使用字符串比較來檢查你的函數返回了哪種類型的錯誤。然而,允許使用字符串比較來檢查來自被測試包的錯誤訊息是否滿足某些特性,例如,它是否包含了參數名稱。 2613 | 2614 | Go 中的錯誤值通常有一部分是為人眼而設計的,一部分是為了語義控制流程。測試應該只檢查可以可靠觀察到的語義信息,而不是為人類調試而設計的顯示信息,因為這經常會受到未來變化的影響。有關構建具有語義意義的錯誤的指南,請參見[關於錯誤的最佳實踐](best-practices#error-handling)。如果來自你無法控制的依賴項的錯誤缺乏語義信息,考慮對所有者提出錯誤報告以幫助改進 API,而不是依賴於解析錯誤訊息。 2615 | 2616 | 在單元測試中,通常只關心是否發生了錯誤。如果是這樣,那麼當你期望出現錯誤時,只測試錯誤是否非空就足夠了。如果你想測試錯誤在語義上是否與某些其他錯誤匹配,那麼考慮使用[`errors.Is`]或`cmp`與[`cmpopts.EquateErrors`]。 2617 | 2618 | > **注意:** 如果一個測試使用了[`cmpopts.EquateErrors`],但它的所有`wantErr`值要么是`nil`要么是`cmpopts.AnyError`,那麼使用`cmp`是[不必要的機制](guide.md#least-mechanism)。通過將 want 字段簡化為`bool`來簡化代碼。然後你可以使用一個簡單的比較與`!=`。 2619 | > 2620 | > ```go 2621 | > // 較佳: 2622 | > err := f(test.input) 2623 | > gotErr := err != nil 2624 | > if gotErr != test.wantErr { 2625 | > t.Errorf("f(%q) = %v, want error presence = %v", test.input, err, test.wantErr) 2626 | > } 2627 | > ``` 2628 | 2629 | 另見 2630 | [GoTip #13: 設計錯誤以便檢查](https://google.github.io/styleguide/go/index.html#gotip)。 2631 | 2632 | [tott-350]: https://testing.googleblog.com/2015/01/testing-on-toilet-change-detector-tests.html 2633 | [`cmpopts.EquateErrors`]: https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts#EquateErrors 2634 | [`errors.Is`]: https://pkg.go.dev/errors#Is 2635 | 2636 | 2637 | 2638 | ## Test structure 2639 | 2640 | 2641 | 2642 | ### 子測試 Subtests 2643 | 2644 | 標準 Go 測試庫提供了一種[定義子測試]的功能。這允許在設置和清理、控制並行性以及測試過濾方面提供靈活性。子測試可能很有用(特別是對於表驅動測試),但使用它們並非強制性的。另見 2645 | [Go 博客關於子測試的文章](https://blog.golang.org/subtests)。 2646 | 2647 | 子測試不應該依賴其他案例的執行來確保成功或初始狀態,因為預期子測試能夠單獨運行,使用`go test -run`標誌或 Bazel [測試過濾]表達式。 2648 | 2649 | [定義子測試]: https://pkg.go.dev/testing#hdr-Subtests_and_Sub_benchmarks 2650 | [測試過濾]: https://bazel.build/docs/user-manual#test-filter 2651 | 2652 | 2653 | 2654 | #### 子測試名稱 Subtest names 2655 | 2656 | 命名你的子測試,使其在測試輸出中可讀並對測試過濾的用戶在命令行上有用。當你使用`t.Run`創建子測試時,第一個參數被用作測試的描述性名稱。為了確保測試結果對閱讀日誌的人類來說是易讀的,選擇在轉義後仍然有用且可讀的子測試名稱。將子測試名稱視為函數標識符,而不是散文描述。測試運行器會將空格替換為下劃線,並轉義非打印字符。如果你的測試數據受益於更長的描述,請考慮將描述放在一個單獨的字段中(也許是使用`t.Log`打印或與失敗訊息一起)。 2657 | 2658 | 子測試可以使用[Go 測試運行器]或 Bazel [測試過濾]的標誌單獨運行,因此選擇描述性名稱,同時也易於輸入。 2659 | 2660 | > **警告:** 在子測試名稱中使用斜線字符特別不友好,因為它們在[測試過濾的特殊含義]。 2661 | > 2662 | > > ```sh 2663 | > > # 不佳: 2664 | > > # 假設TestTime和t.Run("America/New_York", ...) 2665 | > > bazel test :mytest --test_filter="Time/New_York" # 什麼都不運行! 2666 | > > bazel test :mytest --test_filter="Time//New_York" # 正確,但尷尬。 2667 | > > ``` 2668 | 2669 | 為了[識別函數的輸入],將它們包含在測試的失敗訊息中,在那裡它們不會被測試運行器轉義。 2670 | 2671 | ```go 2672 | // 較佳: 2673 | func TestTranslate(t *testing.T) { 2674 | data := []struct { 2675 | name, desc, srcLang, dstLang, srcText, wantDstText string 2676 | }{ 2677 | { 2678 | name: "hu=en_bug-1234", 2679 | desc: "regression test following bug 1234. contact: cleese", 2680 | srcLang: "hu", 2681 | srcText: "cigarettát és egy öngyújtót kérek", 2682 | dstLang: "en", 2683 | wantDstText: "cigarettes and a lighter please", 2684 | }, // ... 2685 | } 2686 | for _, d := range data { 2687 | t.Run(d.name, func(t *testing.T) { 2688 | got := Translate(d.srcLang, d.dstLang, d.srcText) 2689 | if got != d.wantDstText { 2690 | t.Errorf("%s\nTranslate(%q, %q, %q) = %q, want %q", 2691 | d.desc, d.srcLang, d.dstLang, d.srcText, got, d.wantDstText) 2692 | } 2693 | }) 2694 | } 2695 | } 2696 | ``` 2697 | 2698 | 以下是一些需要避免的例子: 2699 | 2700 | ```go 2701 | // 不佳: 2702 | // Too wordy. 2703 | t.Run("check that there is no mention of scratched records or hovercrafts", ...) 2704 | // Slashes cause problems on the command line. 2705 | t.Run("AM/PM confusion", ...) 2706 | ``` 2707 | 2708 | 另見 2709 | [Go 提示 #117:子測試名稱](https://google.github.io/styleguide/go/index.html#gotip)。 2710 | 2711 | [Go 測試運行器]: https://golang.org/cmd/go/#hdr-Testing_flags 2712 | [識別輸入]: #identify-the-input 2713 | [測試過濾的特殊含義]: https://blog.golang.org/subtests#:~:text=Perhaps%20a%20bit,match%20any%20tests 2714 | 2715 | 2716 | 2717 | ### 表驅動測試 Table-driven tests 2718 | 2719 | 當許多不同的測試案例可以使用類似的測試邏輯進行測試時,請使用表驅動測試。 2720 | 2721 | - 當測試函數的實際輸出是否等於預期輸出時。例如,許多[`fmt.Sprintf`的測試]或下面的最小片段。 2722 | - 當測試函數的輸出始終符合同一組不變量時。例如,[`net.Dial`的測試]。 2723 | 2724 | [`fmt.Sprintf`的測試]: https://cs.opensource.google/go/go/+/master:src/fmt/fmt_test.go 2725 | [`net.Dial`的測試]: https://cs.opensource.google/go/go/+/master:src/net/dial_test.go;l=318;drc=5b606a9d2b7649532fe25794fa6b99bd24e7697c 2726 | 2727 | 以下是表驅動測試的最小結構。如果需要,您可以使用不同的名稱或添加額外的設施,如子測試或設置和清理函數。始終記住[有用的測試失敗](#useful-test-failures)。 2728 | 2729 | ```go 2730 | // 較佳: 2731 | func TestCompare(t *testing.T) { 2732 | compareTests := []struct { 2733 | a, b string 2734 | want int 2735 | }{ 2736 | {"", "", 0}, 2737 | {"a", "", 1}, 2738 | {"", "a", -1}, 2739 | {"abc", "abc", 0}, 2740 | {"ab", "abc", -1}, 2741 | {"abc", "ab", 1}, 2742 | {"x", "ab", 1}, 2743 | {"ab", "x", -1}, 2744 | {"x", "a", 1}, 2745 | {"b", "x", -1}, 2746 | // test runtime·memeq's chunked implementation 2747 | {"abcdefgh", "abcdefgh", 0}, 2748 | {"abcdefghi", "abcdefghi", 0}, 2749 | {"abcdefghi", "abcdefghj", -1}, 2750 | } 2751 | 2752 | for _, test := range compareTests { 2753 | got := Compare(test.a, test.b) 2754 | if got != test.want { 2755 | t.Errorf("Compare(%q, %q) = %v, want %v", test.a, test.b, got, test.want) 2756 | } 2757 | } 2758 | } 2759 | ``` 2760 | 2761 | **注意**:上面例子中的失敗訊息滿足了[識別函數](#identify-the-function)和[識別輸入](#identify-the-input)的指導。沒有必要[數字識別行](#table-tests-identifying-the-row) 2762 | 2763 | 當某些測試案例需要使用與其他測試案例不同的邏輯進行檢查時,寫多個測試函數更為合適,如[GoTip #50: 不相關的表測試]中所解釋的。當表中的每個條目都有自己不同的條件邏輯來檢查其輸入的每個輸出時,你的測試代碼的邏輯可能會變得難以理解。如果測試案例有不同的邏輯但相同的設置,單個測試函數中的一系列[子測試](#subtests)可能是有意義的。 2764 | 2765 | 您可以將表驅動測試與多個測試函數結合使用。例如,當測試函數的輸出完全匹配預期輸出,並且函數對無效輸入返回非空錯誤時,則編寫兩個單獨的表驅動測試函數是最好的方法:一個用於正常的非錯誤輸出,一個用於錯誤輸出。 2766 | 2767 | [GoTip #50: 不相關的表測試]: https://google.github.io/styleguide/go/index.html#gotip 2768 | 2769 | 2770 | 2771 | #### 數據驅動的測試案例 Data-driven test cases 2772 | 2773 | 表測試的行有時可能變得複雜,行值在測試案例內指定條件行為。從測試案例之間的重複中獲得的額外清晰度對於可讀性是必要的。 2774 | 2775 | ```go 2776 | // 較佳: 2777 | type decodeCase struct { 2778 | name string 2779 | input string 2780 | output string 2781 | err error 2782 | } 2783 | 2784 | func TestDecode(t *testing.T) { 2785 | // setupCodex is slow as it creates a real Codex for the test. 2786 | codex := setupCodex(t) 2787 | 2788 | var tests []decodeCase // rows omitted for brevity 2789 | 2790 | for _, test := range tests { 2791 | t.Run(test.name, func(t *testing.T) { 2792 | output, err := Decode(test.input, codex) 2793 | if got, want := output, test.output; got != want { 2794 | t.Errorf("Decode(%q) = %v, want %v", test.input, got, want) 2795 | } 2796 | if got, want := err, test.err; !cmp.Equal(got, want) { 2797 | t.Errorf("Decode(%q) err %q, want %q", test.input, got, want) 2798 | } 2799 | }) 2800 | } 2801 | } 2802 | 2803 | func TestDecodeWithFake(t *testing.T) { 2804 | // A fakeCodex is a fast approximation of a real Codex. 2805 | codex := newFakeCodex() 2806 | 2807 | var tests []decodeCase // rows omitted for brevity 2808 | 2809 | for _, test := range tests { 2810 | t.Run(test.name, func(t *testing.T) { 2811 | output, err := Decode(test.input, codex) 2812 | if got, want := output, test.output; got != want { 2813 | t.Errorf("Decode(%q) = %v, want %v", test.input, got, want) 2814 | } 2815 | if got, want := err, test.err; !cmp.Equal(got, want) { 2816 | t.Errorf("Decode(%q) err %q, want %q", test.input, got, want) 2817 | } 2818 | }) 2819 | } 2820 | } 2821 | ``` 2822 | 2823 | 在下面的反例中,請注意在案例設置中很難區分每個測試案例使用的`Codex`類型。(突出顯示的部分違反了[ToTT: 數據驅動陷阱!][tott-97]的建議。) 2824 | 2825 | ```go 2826 | // 不佳: 2827 | type decodeCase struct { 2828 | name string 2829 | input string 2830 | codex testCodex 2831 | output string 2832 | err error 2833 | } 2834 | 2835 | type testCodex int 2836 | 2837 | const ( 2838 | fake testCodex = iota 2839 | prod 2840 | ) 2841 | 2842 | func TestDecode(t *testing.T) { 2843 | var tests []decodeCase // rows omitted for brevity 2844 | 2845 | for _, test := tests { 2846 | t.Run(test.name, func(t *testing.T) { 2847 | var codex Codex 2848 | switch test.codex { 2849 | case fake: 2850 | codex = newFakeCodex() 2851 | case prod: 2852 | codex = setupCodex(t) 2853 | default: 2854 | t.Fatalf("Unknown codex type: %v", codex) 2855 | } 2856 | output, err := Decode(test.input, codex) 2857 | if got, want := output, test.output; got != want { 2858 | t.Errorf("Decode(%q) = %q, want %q", test.input, got, want) 2859 | } 2860 | if got, want := err, test.err; !cmp.Equal(got, want) { 2861 | t.Errorf("Decode(%q) err %q, want %q", test.input, got, want) 2862 | } 2863 | }) 2864 | } 2865 | } 2866 | ``` 2867 | 2868 | [tott-97]: https://testing.googleblog.com/2008/09/tott-data-driven-traps.html 2869 | 2870 | 2871 | 2872 | #### 識別行 Identifying the row 2873 | 2874 | 不要使用測試表中測試的索引作為命名測試或打印輸入的替代品。沒有人想要通過你的測試表並計算條目數量來弄清楚哪個測試案例失敗了。 2875 | 2876 | ```go 2877 | // 不佳: 2878 | tests := []struct { 2879 | input, want string 2880 | }{ 2881 | {"hello", "HELLO"}, 2882 | {"wORld", "WORLD"}, 2883 | } 2884 | for i, d := range tests { 2885 | if strings.ToUpper(d.input) != d.want { 2886 | t.Errorf("Failed on case #%d", i) 2887 | } 2888 | } 2889 | ``` 2890 | 2891 | 不要使用測試表中測試的索引作為命名測試或打印輸入的替代品。沒有人想要通過你的測試表並計算條目數量來弄清楚哪個測試案例失敗了。 2892 | 2893 | 在你的測試結構中添加一個測試描述,並在失敗訊息中打印它。使用子測試時,你的子測試名稱應該有效地識別行。 2894 | 2895 | **重要:** 即使`t.Run`限定了輸出和執行,你必須始終[識別輸入]。表測試行名稱必須遵循[子測試命名]指南。 2896 | 2897 | [子測試命名]: #subtest-names 2898 | 2899 | 2900 | 2901 | ### 測試助手 Test helpers 2902 | 2903 | 測試助手是執行設置或清理任務的函數。在測試助手中發生的所有失敗都預期是環境的失敗(不是被測試代碼的失敗)——例如,當無法啟動測試數據庫,因為這台機器上沒有更多的空閒端口時。 2904 | 2905 | 如果你傳遞了一個`*testing.T`,調用[`t.Helper`]以將測試助手中的失敗歸因於調用助手的行。如果存在,這個參數應該在[上下文](#contexts)參數之後,以及在任何剩餘參數之前。 2906 | 2907 | ```go 2908 | // 較佳: 2909 | func TestSomeFunction(t *testing.T) { 2910 | golden := readFile(t, "testdata/golden-result.txt") 2911 | // ... tests against golden ... 2912 | } 2913 | 2914 | // readFile returns the contents of a data file. 2915 | // It must only be called from the same goroutine as started the test. 2916 | func readFile(t *testing.T, filename string) string { 2917 | t.Helper() 2918 | contents, err := runfiles.ReadFile(filename) 2919 | if err != nil { 2920 | t.Fatal(err) 2921 | } 2922 | return string(contents) 2923 | } 2924 | ``` 2925 | 2926 | 當這種模式模糊了測試失敗與導致它的條件之間的聯繫時,不要使用它。具體來說,關於[斷言庫](#assert)的指導仍然適用,[`t.Helper`]不應該用來實現這樣的庫。 2927 | 2928 | **提示:** 有關測試助手和斷言助手之間區別的更多信息,請參見[最佳實踐](best-practices#test-functions)。 2929 | 2930 | 雖然上面提到了`*testing.T`,但對於基準測試和模糊測試助手,大部分建議仍然相同。 2931 | 2932 | [`t.Helper`]: https://pkg.go.dev/testing#T.Helper 2933 | 2934 | 2935 | 2936 | ### Test package 2937 | 2938 | 2939 | 2940 | 2941 | 2942 | #### 同一套件中的測試 Tests in the same package 2943 | 2944 | 測試可以定義在與被測試代碼相同的套件中。 2945 | 2946 | 要在同一套件中寫測試: 2947 | 2948 | - 將測試放在`foo_test.go`文件中 2949 | - 對測試文件使用`package foo` 2950 | - 不要明確導入要測試的套件 2951 | 2952 | ```build 2953 | # 較佳: 2954 | go_library( 2955 | name = "foo", 2956 | srcs = ["foo.go"], 2957 | deps = [ 2958 | ... 2959 | ], 2960 | ) 2961 | 2962 | go_test( 2963 | name = "foo_test", 2964 | size = "small", 2965 | srcs = ["foo_test.go"], 2966 | library = ":foo", 2967 | deps = [ 2968 | ... 2969 | ], 2970 | ) 2971 | ``` 2972 | 2973 | 同一套件中的測試可以訪問套件中未導出的標識符。這可能使得測試覆蓋率更好並且測試更簡潔。請注意,測試中聲明的任何[範例]都不會有用戶在其代碼中需要的套件名稱。 2974 | 2975 | [範例]: #examples 2976 | 2977 | 2978 | 2979 | #### 不同套件中的測試 Tests in a different package 2980 | 2981 | 並不總是適合或甚至可能在與被測試代碼相同的套件中定義測試。在這些情況下,使用帶有`_test`後綴的套件名稱。這是對[套件名稱](#package-names)中“無下劃線”規則的一個例外。例如: 2982 | 2983 | - 如果集成測試沒有一個明顯屬於它的套件 2984 | 2985 | ```go 2986 | // 好的範例: 2987 | package gmailintegration_test 2988 | 2989 | import "testing" 2990 | ``` 2991 | 2992 | - 如果在同一套件中定義測試會導致循環依賴 2993 | 2994 | ```go 2995 | // 好的範例: 2996 | package fireworks_test 2997 | 2998 | import ( 2999 | "fireworks" 3000 | "fireworkstestutil" // fireworkstestutil 也導入了 fireworks 3001 | ) 3002 | ``` 3003 | 3004 | 3005 | 3006 | ### 使用 `testing` 套件 3007 | 3008 | Go 標準庫提供了[`testing`套件]。這是 Google 代碼庫中 Go 代碼唯一允許使用的測試框架。特別是,不允許使用[斷言庫](#assert)和第三方測試框架。 3009 | 3010 | `testing`套件為編寫良好的測試提供了一套最小但完整的功能: 3011 | 3012 | - 頂層測試 3013 | - 基準測試 3014 | - [可運行範例](https://blog.golang.org/examples) 3015 | - 子測試 3016 | - 日誌記錄 3017 | - 失敗和致命失敗 3018 | 3019 | 這些旨在與核心語言特性如[複合字面量]和[帶初始化器的 if 語句]語法協同工作,使測試作者能夠編寫[清晰、可讀和可維護的測試]。 3020 | 3021 | [`testing`套件]: https://pkg.go.dev/testing 3022 | [複合字面量]: https://go.dev/ref/spec#Composite_literals 3023 | [帶初始化器的 if 語句]: https://go.dev/ref/spec#If_statements 3024 | 3025 | 3026 | 3027 | ## 無法決策 Non-decisions 3028 | 3029 | 風格指南無法列舉所有事項的正面規定,也無法列舉所有它不提供意見的事項。話雖如此,以下是一些可讀性社群之前曾經討論過但尚未達成共識的事項。 3030 | 3031 | - **使用零值初始化局部變量**。`var i int`和`i := 0`是等價的。另見[初始化最佳實踐]。 3032 | - **空的複合字面量與`new`或`make`的對比**。`&File{}`和`new(File)`是等價的。`map[string]bool{}`和`make(map[string]bool)`也是如此。另見[複合聲明最佳實踐]。 3033 | - **在 cmp.Diff 調用中 got, want 參數排序**。保持局部一致性, 3034 | 並在您的失敗訊息中[包含一個圖例](#print-diffs)。 3035 | - **`errors.New`與`fmt.Errorf`在非格式化字符串上的使用**。 3036 | `errors.New("foo")`和`fmt.Errorf("foo")`可以互換使用。 3037 | 3038 | 如果在特殊情況下它們再次出現,可讀性導師可能會做出一個可選的評論,但通常作者可以自由選擇他們在給定情況下偏好的風格。 3039 | 3040 | 當然,如果風格指南未涵蓋的任何內容確實需要更多討論, 3041 | 作者歡迎提問 —— 不論是在特定的審查中,還是在內部訊息板上。 3042 | 3043 | [複合聲明最佳實踐]: https://google.github.io/styleguide/go/best-practices#vardeclcomposite 3044 | [初始化最佳實踐]: https://google.github.io/styleguide/go/best-practices#vardeclinitialization 3045 | -------------------------------------------------------------------------------- /guide.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Go 風格指南 4 | 5 | (英文版) 6 | 7 | [概覽](index.md) | [指南](guide.md) | [決策](decisions.md) | 8 | [最佳實踐](best-practices.md) 9 | 10 | **注意:** 這是 Google [Go 風格](index.md) 文件的一部分。本文件是 **[規範性](index.md#normative) 和 [典範性](index.md#canonical)** 的。更多資訊請見[概覽](index.md#about)。 11 | 12 | 13 | 14 | ## 風格原則 15 | 16 | 以下是編寫可讀 Go 代碼的幾個重要原則,按重要性排序: 17 | 18 | 1. **[清晰性][Clarity]**:代碼的目的和理由對讀者來說是清楚的。 19 | 1. **[簡潔性][Simplicity]**:代碼以最簡單的方式實現其目標。 20 | 1. **[簡練性][Concision]**:代碼具有高信噪比。 21 | 1. **[可維護性][Maintainability]**:代碼的編寫使其易於維護。 22 | 1. **[一致性][Consistency]**:代碼與更廣泛的 Google 代碼庫保持一致。 23 | 24 | [Clarity]: #clarity 25 | [Simplicity]: #simplicity 26 | [Concision]: #concision 27 | [Maintainability]: #maintainability 28 | [Consistency]: #consistency 29 | 30 | 31 | 32 | ### 清晰性 33 | 34 | 可讀性的核心目標是產生對讀者來說清晰的代碼。這主要通過有效的命名、有幫助的評論和高效的代碼組織來實現。 35 | 36 | 清晰性應該從讀者的角度來看,而不是代碼的作者。代碼易於閱讀比易於編寫更重要。清晰性有兩個方面: 37 | 38 | - [代碼實際上在做什麼?](#clarity-purpose) 39 | - [代碼為什麼要這樣做?](#clarity-rationale) 40 | 41 | 42 | 43 | #### 代碼實際上在做什麼? 44 | 45 | Go 的設計使得應該相對直觀地看到代碼在做什麼。在不確定的情況下,值得投入時間使代碼的目的對未來的讀者更清晰。例如,可能有助於: 46 | 47 | - 使用更描述性的變量名 48 | - 添加額外的評論 49 | - 用空白和評論分隔代碼 50 | - 將代碼重構為單獨的函數/方法使其更模塊化 51 | 52 | 這裡沒有一刀切的方法,但在開發 Go 代碼時優先考慮清晰性是重要的。 53 | 54 | 55 | 56 | #### 代碼為什麼要這樣做? 57 | 58 | 代碼的理由通常通過變量、函數、方法或套件的名稱充分傳達。在不是的情況下,添加評論很重要。當代碼包含讀者可能不熟悉的細微差別時,解釋“為什麼?”尤其重要,例如: 59 | 60 | - 語言的一個細微差別,例如,一個閉包將捕獲一個循環變量,但閉包距離很遠 61 | - 業務邏輯的一個細微差別,例如,一個需要區分實際用戶和冒充用戶的訪問控制檢查 62 | 63 | 一個 API 可能需要小心使用。例如,一段代碼可能因為性能原因而錯綜複雜,難以跟隨,或者一系列複雜的數學操作可能以意想不到的方式使用類型轉換。在這些情況下,重要的是附帶的評論和文檔解釋這些方面,以便未來的維護者不會犯錯,並且讀者可以理解代碼而無需逆向工程。 64 | 65 | 同樣重要的是要意識到,一些試圖提供清晰性的嘗試(例如添加額外的評論)實際上可能會通過添加雜亂、重述代碼已經說的話、與代碼矛盾或增加維護負擔來保持評論的最新狀態來模糊代碼的目的。允許代碼自己說話(例如,使符號名稱本身自我描述)而不是添加多餘的評論。評論通常更好地解釋為什麼要這樣做,而不是代碼在做什麼。 66 | 67 | Google 的代碼庫在很大程度上是統一和一致的。通常情況下,代碼脫穎而出(例如,通過使用不熟悉的模式)是出於好的原因,通常是出於性能。保持這一特性對於讓讀者清楚他們在閱讀新代碼時應該專注的地方很重要。 68 | 69 | 標準庫包含了許多這一原則的實例。其中包括: 70 | 71 | - [`package sort`](https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/sort/sort.go) 中的維護者評論。 72 | - 同一包中的好的 73 | [可運行示例](https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/sort/example_search_test.go), 74 | 這對用戶(它們 75 | [顯示在 godoc 中](https://pkg.go.dev/sort#pkg-examples))和維護者(它們 [作為測試的一部分運行](decisions.md#examples))都有益。 76 | - [`strings.Cut`](https://pkg.go.dev/strings#Cut) 只有四行代碼, 77 | 但它們改善了 78 | [調用點的清晰性和正確性](https://github.com/golang/go/issues/46336)。 79 | 80 | 81 | 82 | ### 簡潔性 83 | 84 | Go 代碼應該對使用它、閱讀它和維護它的人來說是簡單的。應該以實現其目標的最簡單方式編寫,無論是在行為還是性能方面。在 Google 的 Go 代碼庫中,簡單的代碼: 85 | 86 | - 從上到下易於閱讀 87 | - 不假設你已經知道它在做什麼 88 | - 不假設你能記住所有前面的代碼 89 | - 沒有不必要的抽象層次 90 | - 沒有引起注意某些平凡事物的名稱 91 | - 讓值和決策的傳播對讀者清晰 92 | - 有評論解釋為什麼,而不是代碼在做什麼,以避免未來的偏差 93 | - 有獨立的文檔 94 | - 有有用的錯誤和有用的測試失敗 95 | - 經常與“聰明”的代碼相互排斥 96 | 97 | 在代碼簡單性和 API 使用簡單性之間可能會出現權衡。例如,可能值得讓代碼更複雜,以便最終用戶更容易正確調用 API。相反,也可能值得留下一些額外的工作給 API 的最終用戶,以便代碼保持簡單易懂。 98 | 99 | 當代碼需要複雜性時,應該有意識地添加複雜性。這通常是必要的,如果需要額外的性能或有多個不同的客戶使用特定的庫或服務。複雜性可能是合理的,但它應該伴隨著相應的文檔,以便客戶和未來的維護者能夠理解和應對複雜性。這應該通過測試和示例補充,這些測試和示例展示了其正確的使用方式,特別是如果有“簡單”和“複雜”的使用代碼的方式。 100 | 101 | 這一原則並不意味著複雜的代碼不能或不應該用 Go 編寫,或者 Go 代碼不允許複雜。我們努力實現一個代碼庫,避免不必要的複雜性,以便當複雜性出現時,它表明有問題的代碼需要小心理解和維護。理想情況下,應該有附帶的評論解釋理由並識別應該採取的護理。這經常出現在為性能優化代碼時;這樣做通常需要更複雜的方法,比如預分配一個緩衝區並在一個 goroutine 的生命週期內重複使用它。當維護者看到這一點時,應該是一個線索,表明有問題的代碼是性能關鍵的,這應該影響未來變更時採取的護理。另一方面,如果不必要地使用,這種複雜性對於未來需要閱讀或更改代碼的人來說是一種負擔。 102 | 103 | 如果代碼的目的應該很簡單,但結果非常複雜,這通常是重新審視實現以查看是否有更簡單的方法來實現相同事物的信號。 104 | 105 | 106 | 107 | #### 最少機制 108 | 109 | 當有幾種方式可以表達相同的想法時,優先選擇使用最標準工具的那一個。復雜的機制經常存在,但不應該無故使用。添加復雜性很容易,而移除不必要的復雜性則要困難得多。 110 | 111 | 1. 當足夠滿足您的用例時,優先使用核心語言構造(例如通道、切片、映射、循環或結構)。 112 | 2. 如果沒有,尋找標準庫中的工具(如 HTTP 客戶端或模板引擎)。 113 | 3. 最後,考慮在引入新依賴或創建自己的東西之前,Google 代碼庫中是否有足夠的核心庫。 114 | 115 | 例如,考慮包含一個綁定到具有默認值的變量的標誌的生產代碼,該默認值必須在測試中覆蓋。除非打算測試程序的命令行界面本身(比如使用 `os/exec`),直接覆蓋綁定值而不是使用 `flag.Set` 更簡單,因此更可取。 116 | 117 | 同樣,如果一段代碼需要一個集合成員資格檢查,一個布爾值映射(例如 `map[string]bool`)通常就足夠了。只有在需要更複雜的操作且無法或過於複雜地使用映射時,才應該使用提供類似集合類型和功能的庫。 118 | 119 | 120 | 121 | ### 簡練性 122 | 123 | 簡練的 Go 代碼具有高信噪比。相關細節容易辨識,命名和結構引導讀者了解這些細節。有許多事情可能妨礙在任何特定時間顯示最突出的細節: 124 | 125 | - 重複的代碼 126 | - 多餘的語法 127 | - [不透明的名稱](#naming) 128 | - 不必要的抽象 129 | - 空白 130 | 131 | 重複的代碼尤其掩蓋了每個幾乎相同部分之間的差異,並要求讀者視覺比較類似的代碼行以找到變化。[表驅動測試]是一個好的機制,可以簡潔地從每次重複的重要細節中提取出共同的代碼,但選擇哪些部分包含在表中將影響表的易於理解程度。 132 | 133 | 在考慮多種結構代碼的方式時,值得考慮哪種方式使重要細節最為明顯。 134 | 135 | 理解和使用常見的代碼構造和慣用語也對於保持高信噪比很重要。例如,以下代碼塊在[錯誤處理]中非常常見,讀者可以快速理解這個塊的目的。 136 | 137 | ```go 138 | // 較佳: 139 | if err := doSomething(); err != nil { 140 | // ... 141 | } 142 | ``` 143 | 144 | 如果代碼看起來與此非常相似但略有不同,讀者可能不會注意到變化。在這種情況下,值得故意["提升"]錯誤檢查的信號,通過添加評論來引起注意。 145 | 146 | ```go 147 | // 較佳: 148 | if err := doSomething(); err == nil { // if NO error 149 | // ... 150 | } 151 | ``` 152 | 153 | [表驅動測試]: https://github.com/golang/go/wiki/TableDrivenTests 154 | [錯誤處理]: https://go.dev/blog/errors-are-values 155 | ["提升"]: best-practices#signal-boost 156 | 157 | 158 | 159 | ### 可維護性 160 | 161 | 代碼被編輯的次數比被寫的次數多得多。可讀的代碼不僅對試圖理解其運作方式的讀者有意義,對需要更改它的程序員也有意義。清晰是關鍵。可維護的代碼: 162 | 163 | - 方便未來的程序員正確修改 164 | - 擁有結構化的 API,使其可以優雅地成長 165 | - 清楚它所做的假設,選擇映射到問題結構而非代碼結構的抽象 166 | - 避免不必要的耦合,不包括未使用的功能 167 | - 擁有全面的測試套件以確保維護承諾的行為並確保重要邏輯正確,並且測試在失敗時提供清晰、可操作的診斷 168 | 169 | 在使用接口和類型等抽象時,這些抽象本質上從它們被使用的上下文中移除了資訊,確保它們提供足夠的好處很重要。當使用具體類型時,編輯器和 IDE 可以直接連接到方法定義並顯示相應的文檔,但否則只能參考接口定義。接口是一個強大的工具,但帶來了成本,因為維護者可能需要理解底層實現的具體情況才能正確使用接口,這必須在接口文檔或調用現場解釋。 170 | 171 | 可維護的代碼還避免在容易忽視的地方隱藏重要細節。例如,在以下每行代碼中,單個字符的存在或缺乏對於理解該行至關重要: 172 | 173 | ```go 174 | // 不佳: 175 | // The use of = instead of := can change this line completely. 176 | if user, err = db.UserByID(userID); err != nil { 177 | // ... 178 | } 179 | ``` 180 | 181 | ```go 182 | // 不好範例: 183 | // The ! in the middle of this line is very easy to miss. 184 | leap := (year%4 == 0) && (!(year%100 == 0) || (year%400 == 0)) 185 | ``` 186 | 187 | 這兩者都不是錯誤的,但都可以用更明確的方式書寫,或者可以有一個附帶的評論來提醒注意重要的行為: 188 | 189 | ```go 190 | // 較佳: 191 | u, err := db.UserByID(userID) 192 | if err != nil { 193 | return fmt.Errorf("invalid origin user: %s", err) 194 | } 195 | user = u 196 | ``` 197 | 198 | ```go 199 | // 較佳: 200 | // Gregorian leap years aren't just year%4 == 0. 201 | // See https://en.wikipedia.org/wiki/Leap_year#Algorithm. 202 | var ( 203 | leap4 = year%4 == 0 204 | leap100 = year%100 == 0 205 | leap400 = year%400 == 0 206 | ) 207 | leap := leap4 && (!leap100 || leap400) 208 | ``` 209 | 210 | 在同樣的方式下,一個隱藏關鍵邏輯或重要邊界情況的輔助函數可能會讓未來的更改未能適當地考慮到它。 211 | 212 | 可預測的名稱是可維護代碼的另一個特點。包的使用者或代碼的維護者應該能夠在給定的上下文中預測變量、方法或函數的名稱。對於相同概念的函數參數和接收者名稱通常應該共享相同的名稱,既為了保持文檔的可理解性,也為了便於以最小的開銷重構代碼。 213 | 214 | 可維護的代碼最小化其依賴性(無論是隱式的還是顯式的)。依賴較少的包意味著可以影響行為的代碼行更少。避免依賴內部或未記載的行為使代碼不太可能在未來這些行為變化時帶來維護負擔。 215 | 216 | 在考慮如何結構化或編寫代碼時,值得花時間思考代碼可能隨時間演變的方式。如果某種方法更有利於更容易和更安全的未來變更,那通常是一個好的權衡,即使它意味著略微複雜的設計。 217 | 218 | 219 | 220 | ### 一致性 221 | 222 | 一致的代碼在整個代碼庫中看起來、感覺和行為都應該相似。在團隊或套件的上下文中,甚至在單個文件中也是如此。 223 | 224 | 一致性問題不會取代上述任何原則,但如果必須打破平手,通常有利於一致性。 225 | 226 | 套件內的一致性通常是最直接重要的一致性層次。如果同一問題在一個套件中以多種方式處理,或者同一概念在一個文件中有許多名稱,可能會非常不協調。然而,即使是這樣,也不應該取代記載的風格原則或全局一致性。 227 | 228 | 229 | 230 | ## 核心指南 231 | 232 | 這些指南收集了所有 Go 代碼都預期遵循的 Go 風格最重要的方面。我們期望這些原則在獲得可讀性認可時被學習和遵循。這些不預期會經常變化,新的添加必須通過高標準。 233 | 234 | 以下指南擴展了 [Effective Go] 中的建議,為整個社區的 Go 代碼提供了共同的基線。 235 | 236 | [Effective Go]: https://go.dev/doc/effective_go 237 | 238 | 239 | 240 | ### 格式化 241 | 242 | 所有 Go 源文件必須符合 `gofmt` 工具輸出的格式。這種格式由 Google 代碼庫中的預提交檢查強制執行。[生成的代碼] 通常也應該格式化(例如,使用 [`format.Source`]),因為它也可以在代碼搜索中瀏覽。 243 | 244 | [生成的代碼]: https://docs.bazel.build/versions/main/be/general.html#genrule 245 | [`format.Source`]: https://pkg.go.dev/go/format#Source 246 | 247 | 248 | 249 | ### 混合大小寫 250 | 251 | Go 源代碼在寫多詞名稱時使用 `MixedCaps` 或 `mixedCaps`(駝峰式大小寫)而不是下劃線(蛇形大小寫)。 252 | 253 | 即使這樣做打破了其他語言中的慣例,也適用。例如,如果一個常量被導出,則為 `MaxLength`(不是 `MAX_LENGTH`),如果未導出,則為 `maxLength`(不是 `max_length`)。 254 | 255 | 局部變量被視為 [未導出] 用於選擇初始大寫。 256 | 257 | 258 | 259 | [未導出]: https://go.dev/ref/spec#Exported_identifiers 260 | 261 | 262 | 263 | ### 行長 264 | 265 | Go 源代碼沒有固定的行長。如果一行感覺太長,優先重構而不是分割它。如果它已經盡可能短了,則應該允許該行保持長度。 266 | 267 | 不要分割一行: 268 | 269 | - 在 [縮進變化](decisions.md#indentation-confusion) 之前(例如,函數聲明、條件) 270 | - 為了使長字符串(例如,URL)適應多個較短的行 271 | 272 | 273 | 274 | ### 命名 275 | 276 | 命名更多的是藝術而不是科學。在 Go 中,名稱傾向於比許多其他語言短一些,但相同的 [一般指南] 適用。名稱應該: 277 | 278 | - 在使用時不感覺 [重複](decisions.md#repetition) 279 | - 考慮上下文 280 | - 不重複已經清楚的概念 281 | 282 | 您可以在 [決策](decisions.md#naming) 中找到更具體的命名指導。 283 | 284 | [一般指南]: https://testing.googleblog.com/2017/10/code-health-identifiernamingpostforworl.html 285 | 286 | 287 | 288 | ### 本地一致性 289 | 290 | 在風格指南對某一特定風格點沒有任何說法的地方,作者可以自由選擇他們喜歡的風格,除非在近處的代碼(通常在同一文件或包中,但有時在團隊或項目目錄中)已經在該問題上採取了一致的立場。 291 | 292 | **有效** 本地風格考慮的例子: 293 | 294 | - 使用 `%s` 或 `%v` 進行格式化打印錯誤 295 | - 使用緩衝通道代替互斥鎖 296 | 297 | **無效** 本地風格考慮的例子: 298 | 299 | - 代碼的行長限制 300 | - 使用基於斷言的測試庫 301 | 302 | 如果本地風格與風格指南不一致,但可讀性影響僅限於一個文件,通常會在代碼審查中提出,對於一致的修復將超出問題 CL 的範圍。在這一點上,適合提出一個錯誤來跟踪修復。 303 | 304 | 如果更改會惡化現有的風格偏差,暴露更多的 API 表面,擴大偏差存在的文件數量,或引入實際錯誤,那麼本地一致性不再是違反新代碼風格指南的有效理由。在這些情況下,作者應該清理同一 CL 中的現有代碼庫,提前進行重構,或找到至少不會使本地問題惡化的替代方案。 305 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | # Go 風格指南 2 | 3 | (英文版) 4 | 5 | [概覽](index.md) | [指南](guide.md) | [決策](decisions.md) | 6 | [最佳實踐](best-practices.md) 7 | 8 | 9 | 10 | ## 關於 11 | 12 | Go 風格指南及其附帶文件編碼了當前編寫可讀性和符合慣用語法的 Go 語言的最佳方法。遵循風格指南並非絕對要求,這些文件也不會是全面的。我們的目的是減少編寫可讀 Go 語言的猜測工作,幫助新手避免常見錯誤。風格指南還旨在統一 Google 內部審查 Go 代碼時的風格指導。 13 | 14 | | 文件 | 鏈接 | 主要受眾 | [規範性] | [典範性] | 15 | | ------------ | ------------------------------------------------------- | -------------- | -------- | -------- | 16 | | **風格指南** | | 所有人 | 是 | 是 | 17 | | **風格決策** | | 可讀性導師 | 是 | 否 | 18 | | **最佳實踐** | | 任何有興趣的人 | 否 | 否 | 19 | 20 | [規範性]: #normative 21 | [典範性]: #canonical 22 | 23 | 24 | 25 | ### 文件 26 | 27 | 1. **[風格指南](https://google.github.io/styleguide/go/guide)** 概述了 Google 的 Go 風格基礎。此文件是決定性的,並用作風格決策和最佳實踐的基礎。 28 | 29 | 1. **[風格決策](https://google.github.io/styleguide/go/decisions)** 是一份更詳細的文件,總結了特定風格點的決策,並在適當情況下討論決策背後的原因。 30 | 31 | 這些決策可能會根據新的數據、新的語言特性、新的庫或新興模式而變化,但不期望 Google 內部的個別 Go 程序員需要隨時更新此文件。 32 | 33 | 1. **[最佳實踐](https://google.github.io/styleguide/go/best-practices)** 文件記錄了一些隨著時間演變而解決常見問題、易於閱讀且對代碼維護需求健壯的模式。 34 | 35 | 這些最佳實踐不是典範的,但鼓勵 Google 的 Go 程序員在可能的情況下使用它們,以保持代碼庫的統一性和一致性。 36 | 37 | 這些文件旨在: 38 | 39 | - 就權衡不同風格的原則達成一致 40 | - 編碼 Go 風格的既定事項 41 | - 提供 Go 慣用語法的文件和典範示例 42 | - 記錄各種風格決策的優缺點 43 | - 幫助減少 Go 可讀性審查中的驚喜 44 | - 幫助可讀性導師使用一致的術語和指導 45 | 46 | 這些文件**不**旨在: 47 | 48 | - 列出可在可讀性審查中給出的所有評論 49 | - 列出所有人都預期記住並隨時遵循的所有規則 50 | - 替代在使用語言特性和風格時的良好判斷 51 | - 為了消除風格差異而辯解大規模變更 52 | 53 | 從一位 Go 程序員到另一位,從一個團隊的代碼庫到另一個,總會有差異。然而,對 Google 和 Alphabet 而言,我們的代碼庫盡可能一致是最好的。 (參見[指南](guide.md#consistency)以獲得更多關於一致性的信息。) 為此,隨時可以進行風格改進,但您不需要吹毛求疵地指出您發現的每一個風格指南違規情況。特別是,這些文件可能會隨著時間而變化,這不是在現有代碼庫中引起額外變動的理由;使用最新的最佳實踐編寫新代碼並隨時間解決附近問題就足夠了。 54 | 55 | 重要的是要認識到風格問題本質上是個人的,總是存在固有的權衡。這些文件中的許多指導是主觀的,但就像 `gofmt` 一樣,它們提供的統一性具有重大價值。因此,沒有充分討論,不會更改風格建議,即使在可能不同意的情況下,也鼓勵 Google 的 Go 程序員遵循風格指南。 56 | 57 | 58 | 59 | ## 定義 60 | 61 | 以下在風格文件中使用的詞語在下面定義: 62 | 63 | - **典範性**:建立規範和持久的規則 64 | 65 | 66 | 在這些文件中,“典範性”用於描述被認為是所有代碼(新舊)都應遵循的標準,且不預期會隨時間大幅改變。典範文件中的原則應由作者和審查者共同理解,因此典範文件中包含的所有內容都必須達到高標準。典範文件通常較短,且比非典範文件規定的風格元素更少。 67 | 68 | 69 | 70 | - **規範性**:旨在建立一致性 71 | 72 | 在這些文件中,“規範性”用於描述某些被 Go 代碼審查者同意的風格元素,以便建議、術語和理由保持一致。這些元素可能會隨著時間而改變,這些文件將反映這種變化,以便審查者能夠保持一致性和最新狀態。Go 代碼的作者不預期熟悉規範文件,但審查者在可讀性審查中經常使用這些文件作為參考。 73 | 74 | 75 | 76 | - **慣用語**:常見且熟悉 77 | 78 | 在這些文件中,“慣用語”用於指代在 Go 代碼中普遍存在的、已成為易於識別的熟悉模式的東西。一般來說,如果兩者在上下文中服務於相同的目的,則應優先選擇慣用模式而非非慣用模式,因為這是讀者最熟悉的。 79 | 80 | 81 | 82 | 83 | 84 | ## 附加參考 85 | 86 | 本指南假定讀者熟悉 [Effective Go],因為它為整個 Go 社區的 Go 代碼提供了一個共同的基線。 87 | 88 | 以下是一些額外的資源,供那些希望自學 Go 風格的人以及希望在其審查中提供更多可鏈接上下文的審查者使用。參與 Go 可讀性過程的人不預期熟悉這些資源,但它們可能作為可讀性審查中的上下文出現。 89 | 90 | [Effective Go]: https://go.dev/doc/effective_go 91 | 92 | ### 外部參考 93 | 94 | - [Go 語言規範](https://go.dev/ref/spec) 95 | - [Go FAQ](https://go.dev/doc/faq) 96 | - [Go 內存模型](https://go.dev/ref/mem) 97 | - [Go 數據結構](https://research.swtch.com/godata) 98 | - [Go 接口](https://research.swtch.com/interfaces) 99 | - [Go 諺語](https://go-proverbs.github.io/) 100 | - Go 提示集 - 敬請期待。 101 | - 單元測試實踐 - 敬請期待。 102 | 103 | ### 相關的 Testing-on-the-Toilet 文章 104 | 105 | - [TotT: 標識符命名][tott-431] 106 | - [TotT: 測試狀態與測試互動][tott-281] 107 | - [TotT: 有效測試][tott-324] 108 | - [TotT: 風險驅動測試][tott-329] 109 | - [TotT: 考慮有害的變更檢測測試][tott-350] 110 | 111 | [tott-431]: https://testing.googleblog.com/2017/10/code-health-identifiernamingpostforworl.html 112 | [tott-281]: https://testing.googleblog.com/2013/03/testing-on-toilet-testing-state-vs.html 113 | [tott-324]: https://testing.googleblog.com/2014/05/testing-on-toilet-effective-testing.html 114 | [tott-329]: https://testing.googleblog.com/2014/05/testing-on-toilet-risk-driven-testing.html 115 | [tott-350]: https://testing.googleblog.com/2015/01/testing-on-toilet-change-detector-tests.html 116 | 117 | ### 額外的外部著作 118 | 119 | - [Go 與教條](https://research.swtch.com/dogma) 120 | - [少即是多](https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html) 121 | - [Esmerelda 的想像力](https://commandcenter.blogspot.com/2011/12/esmereldas-imagination.html) 122 | - [用於解析的正則表達式](https://commandcenter.blogspot.com/2011/08/regular-expressions-in-lexing-and.html) 123 | - [Gofmt 的風格沒有人最喜歡,但 Gofmt 是每個人的最愛](https://www.youtube.com/watch?v=PAAkCSZUG1c&t=8m43s) 124 | (YouTube) 125 | --------------------------------------------------------------------------------