20 個 Go 編程最佳實踐

在本教程中,我們將探討 Golang 中的前 20 個最佳編碼實踐。這將幫助你編寫有效的 Go 代碼。

#20: 使用適當的縮進

良好的縮進使你的代碼易讀。一致地使用製表符或空格(最好是製表符),並遵循 Go 的縮進標準。

package main

import "fmt"

func main() {
    for i := 0; i < 5; i++ {
        fmt.Println("Hello, World!")
    }
}

運行gofmt以根據 Go 標準自動格式化(縮進)你的代碼。

$ gofmt -w your_file.go

#19: 正確導入包

僅導入你需要的包,並格式化導入部分以將標準庫包、第三方包和你自己的包分組。

package main

import (
    "fmt"
    "math/rand"
    "time"
)

#18: 使用描述性的變量和函數名

  1. 有意義的名稱:使用傳達變量目的的名稱。

  2. 駝峯命名法:以小寫字母開頭,並在名稱中的每個後續單詞的首字母大寫。

  3. 短名稱:對於生命週期短、範圍小的變量,可以使用簡潔的名稱。

  4. 不要使用縮寫:避免使用難以理解的縮寫和首字母縮寫,而使用描述性名稱。

  5. 一致性:在整個代碼庫中保持命名一致性。

package main

import "fmt"

func main() {
    // 使用有意義的名稱聲明變量
    userName := "John Doe"   // 駝峯命名法:以小寫字母開頭,並在名稱中的每個後續單詞的首字母大寫。
    itemCount := 10         // 短名稱:短小而簡潔,適用於生命週期短、範圍小的變量。
    isReady := true         // 不使用縮寫:避免使用縮寫。

    // 顯示變量值
    fmt.Println("User Name:", userName)
    fmt.Println("Item Count:", itemCount)
    fmt.Println("Is Ready:", isReady)
}

// 對於包級別的變量使用mixedCase
var exportedVariable int = 42

// 函數名應該具有描述性
func calculateSumOfNumbers(a, b int) int {
    return a + b
}

// 保持代碼庫中的命名一致性。

#17: 限制行長度

儘可能保持代碼行長度在 80 個字符以下,以提高可讀性。

package main

import (
    "fmt"
    "math"
)

func main() {
    result := calculateHypotenuse(3, 4)
    fmt.Println("Hypotenuse:", result)
}

func calculateHypotenuse(a, b float64) float64 {
    return math.Sqrt(a*a + b*b)
}

#16: 使用常量代替魔術值

避免在代碼中使用魔術值,即散佈在代碼中的硬編碼數字或字符串,缺乏上下文,使其難以理解目的。爲其定義常量,以使代碼更易維護。

package main

import "fmt"

const (
    // 定義最大重試次數的常量
    MaxRetries = 3

    // 定義默認超時時間(秒)的常量
    DefaultTimeout = 30
)

func main() {
    retries := 0
    timeout := DefaultTimeout

    for retries < MaxRetries {
        fmt.Printf("Attempting operation (Retry %d) with timeout: %d seconds\n", retries+1, timeout)

        // ... 在此處添加你的代碼邏輯 ...

        retries++
    }
}

#15: 錯誤處理

Go 鼓勵開發者顯式處理錯誤,有以下原因:

讓我們創建一個簡單的程序,它讀取一個文件並正確處理錯誤:

package main

import (
    "fmt"
    "os"
)

func main() {
    // 打開一個文件
    file, err := os.Open("example.txt")
    if err != nil {
        // 處理錯誤
        fmt.Println("Error opening the file:", err)
        return
    }
    defer file.Close() // 當完成時關閉文件

    // 從文件中讀取
    buffer := make([]byte, 1024)
    _, err = file.Read(buffer)
    if err != nil {
        // 處理錯誤
        fmt.Println("Error reading the file:", err)
        return
    }

    // 打印文件內容
    fmt.Println("File content:", string(buffer))
}

#14: 避免使用全局變量

最小化使用全局變量。全局變量可能導致不可預測的行爲,使調試變得困難,並阻礙代碼重用。它們還可能在程序的不同部分之間引入不必要的依賴關係。相反,通過函數參數和返回值傳遞數據。

讓我們編寫一個簡單的 Go 程序來說明避免使用全局變量的概念:

package main

import (
    "fmt"
)

func main() {
    // 在main函數中聲明並初始化變量
    message := "Hello, Go!"

    // 調用使用局部變量的函數
    printMessage(message)
}

// printMessage是一個帶參數的函數
func printMessage(msg string) {
    fmt.Println(msg)
}

#13: 使用結構體處理複雜數據

使用結構體將相關的數據字段和方法組合在一起。它們允許你將相關變量組合在一起,使你的代碼更有組織性和可讀性。

以下是一個完整的演示在 Go 中使用結構體的程序:

package main

import (
    "fmt"
)

// 定義一個名爲Person的結構體,表示一個人的信息。
type Person struct {
    FirstName string // 人的名字
    LastName  string // 人的姓氏
    Age       int    // 人的年齡
}

func main() {
    // 創建一個Person結構體的實例並初始化其字段。
    person := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }

    // 訪問並打印結構體字段的值。
    fmt.Println("First Name:", person.FirstName) // 打印名字
    fmt.Println("Last Name:", person.LastName)   // 打印姓氏
    fmt.Println("Age:", person.Age)             // 打印年齡
}

#12: 爲你的代碼添加註釋

添加註釋以解釋代碼的功能,特別是對於複雜或不明顯的部分。

單行註釋單行註釋以//開頭。用於解釋特定行的代碼。

package main

import "fmt"

func main() {
    // 這是一條單行註釋
    fmt.Println("Hello, World!") // 打印問候語
}

多行註釋多行註釋在/* */中。用於較長的解釋或跨多行的註釋。

package main

import "fmt"

func main() {
    /*
        這是一條多行註釋。
        它可以跨越多行。
    */
    fmt.Println("Hello, World!") // 打印問候語
}

函數註釋爲函數添加註釋,明確其用途、參數和返回值。使用 godoc 風格的函數註釋可以使代碼更易讀。

package main

import "fmt"

// greetUser 通過用戶名向用戶表示問候。
// 參數:
//   name (string): 要問候的用戶的名字。
// 返回:
//   string: 問候消息。
func greetUser(name string) string {
    return "Hello, " + name + "!"
}

func main() {
    userName := "Alice"
    greeting := greetUser(userName)
    fmt.Println(greeting)
}

包註釋在 Go 文件的頂部添加註釋,描述包的用途。使用相同的 godoc 風格。

package main

import "fmt"

// 這是我們 Go 程序的主要包。
// 它包含入口點(main)函數。
func main() {
    fmt.Println("Hello, World!")
}

#11: 使用 goroutines 進行併發操作

高效地利用 goroutine 進行併發操作。goroutine 是 Go 中輕量級的、併發的執行線程。它們使您能夠在沒有傳統線程開銷的情況下併發運行函數。這使您能夠編寫高度併發和高效的程序。

讓我們通過一個簡單的例子來演示:

package main

import (
    "fmt"
    "time"
)

// 併發運行的函數
func printNumbers() {
    for i := 1; i <= 5; i++ {
        fmt.Printf("%d ", i)
        time.Sleep(100 * time.Millisecond)
    }
}

// 運行在主 goroutine 中的函數
func main() {
    // 啓動 goroutine
    go printNumbers()

    // 繼續執行主函數
    for i := 0; i < 2; i++ {
        fmt.Println("Hello")
        time.Sleep(200 * time.Millisecond)
    }
    // 在退出之前確保 goroutine 完成
    time.Sleep(1 * time.Second)
}

#10: 使用 Recover 處理 panic

使用 recover 函數優雅的處理 panic,並防止程序崩潰。在 Go 中,panic 是意外的運行時錯誤,可能導致程序崩潰。然而,Go 提供了一種稱爲 recover 的機制來優雅的處理 panic

讓我們通過一個簡單的例子來演示:

package main

import "fmt"

// 可能會 panic 的函數
func riskyOperation() {
    defer func() {
        if r := recover(); r != nil {
            // 從 panic 中恢復並 gracefully 處理它
            fmt.Println("Recovered from panic:", r)
        }
    }()

    // 模擬 panic 條件
    panic("Oops! Something went wrong.")
}

func main() {
    fmt.Println("Start of the program.")

    // 在一個能從 panic 中恢復的函數中調用 riskyOperation
    riskyOperation()

    fmt.Println("End of the program.")
}

#9: 避免使用 init 函數

避免使用 init 函數,除非必要,因爲它可能使代碼更難理解和維護。

一個更好的方法是將初始化邏輯移到常規函數中,您可以從主函數中顯式調用它,通常更易於控制,增強代碼的可讀性,並簡化測試。

以下是演示避免使用 init 函數的簡單 Go 程序:

package main

import (
    "fmt"
)

// InitializeConfig 初始化配置。
func InitializeConfig() {
    // 在這裏初始化配置參數。
    fmt.Println("Initializing configuration...")
}

// InitializeDatabase 初始化數據庫連接。
func InitializeDatabase() {
    // 在這裏初始化數據庫連接。
    fmt.Println("Initializing database...")
}

func main() {
    // 顯式調用初始化函數。
    InitializeConfig()
    InitializeDatabase()

    // 主程序邏輯在這裏。
    fmt.Println("Main program logic...")
}

#8: 使用 defer 進行資源清理

defer 允許你延遲執行函數,直到包圍它的函數返回。它通常用於執行諸如關閉文件、解鎖互斥鎖或釋放其他資源等任務。

這確保即使在出現錯誤的情況下,清理操作也會被執行。

讓我們創建一個簡單的程序,從文件中讀取數據,並使用 defer 確保文件在發生任何錯誤時都能正確關閉:

package main

import (
 "fmt"
 "os"
)

func main() {
 // 打開文件(將 "example.txt" 替換爲你的文件名)
 file, err := os.Open("example.txt")
 if err != nil {
  fmt.Println("Error opening the file:", err)
  return // 出現錯誤時退出程序
 }
 defer file.Close() // 確保函數退出時文件被關閉

 // 讀取並打印文件的內容
 data := make([]byte, 100)
 n, err := file.Read(data)
 if err != nil {
  fmt.Println("Error reading the file:", err)
  return // 出現錯誤時退出程序
 }

 fmt.Printf("Read %d bytes: %s\n", n, data[:n])
}

#7: 推薦使用複合字面值而非構造函數

使用複合字面值來創建結構體的實例,而不是使用構造函數。

**爲什麼使用複合字面值?**複合字面值提供了幾個優勢:

讓我們通過一個簡單的例子來演示:

package main

import (
 "fmt"
)

// 定義一個表示個人信息的結構體類型
type Person struct {
 FirstName string // 個人的名字
 LastName  string // 個人的姓氏
 Age       int    // 個人的年齡
}

func main() {
 // 使用複合字面值創建一個 Person 實例
 person := Person{
  FirstName: "John", // 初始化 FirstName 字段
  LastName:  "Doe",  // 初始化 LastName 字段
  Age:       30,     // 初始化 Age 字段
 }

 // 打印個人信息
 fmt.Println("個人詳情:")
 fmt.Println("名字:", person.FirstName) // 訪問並打印名字字段
 fmt.Println("姓氏:", person.LastName)  // 訪問並打印姓氏字段
 fmt.Println("年齡:", person.Age)        // 訪問並打印年齡字段
}

#6: 減少函數參數

在 Go 中,編寫乾淨高效的代碼是至關重要的。其中一種方法是減少函數參數的數量,這可以導致更易維護和可讀的代碼。

讓我們通過一個簡單的例子來探討這個概念:

package main

import "fmt"

// Option 結構體用於保存配置選項
type Option struct {
    Port    int
    Timeout int
}

// ServerConfig 是一個接受 Option 結構體的函數
func ServerConfig(opt Option) {
    fmt.Printf("服務器配置 - 端口:%d,超時:%d 秒\n", opt.Port, opt.Timeout)
}

func main() {
    // 創建一個具有默認值的 Option 結構體
    defaultConfig := Option{
        Port:    8080,
        Timeout: 30,
    }

    // 使用默認選項配置服務器
    ServerConfig(defaultConfig)

    // 使用新的 Option 結構體修改端口
    customConfig := Option{
        Port: 9090,
    }

    // 使用自定義端口值和默認超時配置服務器
    ServerConfig(customConfig)
}

在這個例子中,我們定義了一個 Option 結構體,用於保存服務器的配置參數。與將多個參數傳遞給ServerConfig函數不同,我們使用一個單獨的Option結構體,使得代碼更易於維護和擴展。這種方法在處理具有大量配置參數的函數時特別有用。

#5: 使用顯式返回值而不是具名返回值以提高清晰度

在 Go 中,通常使用具名返回值,但它們有時會使代碼不夠清晰,尤其是在較大的代碼庫中。

讓我們通過一個簡單的例子來看看它們之間的區別。

package main

import "fmt"

// namedReturn 演示具名返回值。
func namedReturn(x, y int) (result int) {
    result = x + y
    return
}

// explicitReturn 演示顯式返回值。
func explicitReturn(x, y int) int {
    return x + y
}

func main() {
    // 具名返回值
    sum1 := namedReturn(3, 5)
    fmt.Println("具名返回值:", sum1)

    // 顯式返回值
    sum2 := explicitReturn(3, 5)
    fmt.Println("顯式返回值:", sum2)
}

在上面的示例程序中,我們有兩個函數,namedReturnexplicitReturn。它們的區別如下:

namedReturn 使用了具名返回值 result。雖然清楚函數返回的是什麼,但在更復雜的函數中可能不夠直觀。explicitReturn 直接返回結果。這更簡單、更明確。

#4: 保持函數複雜性最小化

函數複雜性指的是函數代碼中的錯綜複雜度、嵌套和分支程度。保持函數複雜性的低水平使得你的代碼更易讀、更易維護,且更不容易出錯。

讓我們通過一個簡單的例子來探討這個概念:

package main

import (
 "fmt"
)

// CalculateSum 返回兩個數字的和。
func CalculateSum(a, b int) int {
 return a + b
}

// PrintSum 打印兩個數字的和。
func PrintSum() {
 x := 5
 y := 3
 sum := CalculateSum(x, y)
 fmt.Printf("%d 和 %d 的和是 %d\n", x, y, sum)
}

func main() {
 // 調用 PrintSum 函數來演示最小函數複雜性。
 PrintSum()
}

在上面的示例程序中:

  1. 我們定義了兩個函數,CalculateSumPrintSum,各自負責特定的任務。

  2. CalculateSum 是一個簡單的函數,用於計算兩個數字的和。

  3. PrintSum 利用 CalculateSum 計算並打印出 53 的和。

  4. 通過保持函數簡潔並專注於單一任務,我們保持了較低的函數複雜性,提高了代碼的可讀性和可維護性。

#3: 避免變量的屏蔽

變量的屏蔽 (shadowing) 發生在在更小的作用域內聲明瞭一個同名的新變量,這可能導致意外的行爲。它隱藏了同名的外部變量,在該作用域內無法訪問。避免在嵌套作用域內屏蔽變量,以防止混淆。

讓我們看一個示例程序:

package main

import "fmt"

func main() {
    // 聲明並初始化一個外部變量 'x',其值爲 10。
    x := 10
    fmt.Println("外部 x:", x)

    // 進入一個內部作用域,其中新變量 'x' 屏蔽了外部的 'x'。
    if true {
        x := 5 // 屏蔽發生在這裏
        fmt.Println("內部 x:", x) // 打印內部的 'x',其值爲 5。
    }

    // 外部的 'x' 保持不變且仍然可訪問。
    fmt.Println("內部作用域後的外部 x:", x) // 打印外部的 'x',其值爲 10。
}

#2: 使用接口進行抽象

抽象抽象是 Go 語言中的一個基本概念,允許我們定義行爲而不指定實現細節。

接口在 Go 中,接口是一組方法簽名。

在泛型功能增加後,接口的是一組方法簽名和類型約束,也就是一組類型的集合。不過這裏介紹的還是原始的接口功能,所以上面的描述也每問題。

任何實現接口所有方法的類型都會隱式滿足該接口。

這使我們能夠編寫能夠與不同類型一起工作的代碼,只要它們遵循相同的接口。

下面是 Go 中的一個示例程序,演示了使用接口進行抽象的概念:

package main

import (
    "fmt"
    "math"
)

// 定義 Shape 接口
type Shape interface {
    Area() float64
}

// 矩形結構體
type Rectangle struct {
    Width  float64
    Height float64
}

// 圓形結構體
type Circle struct {
    Radius float64
}

// 爲矩形實現 Area 方法
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// 爲圓形實現 Area 方法
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

// 打印任意 Shape 的面積的函數
func PrintArea(s Shape) {
    fmt.Printf("面積: %.2f\n", s.Area())
}

func main() {
    rectangle := Rectangle{Width: 5, Height: 3}
    circle := Circle{Radius: 2.5}

    // 在矩形和圓形上調用 PrintArea,它們都實現了 Shape 接口
    PrintArea(rectangle) // 打印矩形的面積
    PrintArea(circle)    // 打印圓形的面積
}

在這個單一的程序中,我們定義了 Shape 接口,創建了兩個結構體 RectangleCircle,它們都實現了 Area() 方法,並使用 PrintArea 函數來打印滿足 Shape 接口的任何形狀的面積。

這演示了在 Go 中如何使用接口進行抽象,以使用一個共同的接口處理不同類型。

#1: 避免混淆庫包和可執行文件

在 Go 語言中,保持庫包和可執行文件之間清晰的分離是至關重要的,以確保代碼清晰和可維護。

以下是演示庫和可執行文件分離的示例項目結構:

myproject/
    ├── main.go
    ├── myutils/
       └── myutils.go

myutils/myutils.go:

package myutils

import "fmt"

// 導出的打印消息的函數
func PrintMessage(message string) {
    fmt.Println("來自 myutils 的消息:", message)
}

main.go:

package main

import (
    "fmt"
    "myproject/myutils" // 導入自定義包
)

func main() {
    message := "你好,Golang!"

    // 調用自定義包 myutils 中的導出函數
    myutils.PrintMessage(message)

    // 演示主程序邏輯
    fmt.Println("來自 main 的消息:", message)
}

在上面的示例中,我們有兩個獨立的文件:myutils.gomain.gomyutils.go 定義了一個名爲 myutils 的自定義包。它包含一個打印消息的導出函數 PrintMessagemain.go 是可執行文件,使用相對路徑("myproject/myutils")導入了自定義包 myutilsmain.go 中的 main 函數調用 myutils 包中的 PrintMessage 函數並打印一條消息。這種關注點分離使代碼保持有序和可維護。

快樂編碼!

原文: Golang Best Practices (Top 20)[1]

參考資料

[1]

Golang Best Practices (Top 20): https://medium.com/@golangda/golang-quick-reference-top-20-best-coding-practices-c0cea6a43f20

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/maH0cibncPNxALYi1QMRVA