10分钟搞懂20个Golang最佳实践

最佳实践是一些不成文的经验总结,遵循最佳实践可以使我们站在前人的肩膀上,避免某些常见错误,写出更好的代码。原文: Golang Best Practices (Top 20)

只需要花上10分钟阅读本文,就可以帮助你更高效编写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. 驼峰表示法(CamelCase): 以小写字母开头,后面每个单词的首字母大写。
  3. * 简短的名称: 简短、简洁的名称对于作用域小、寿命短的变量是可以接受的。
  4. 不使用缩写: 避免使用隐晦的缩写和首字母缩略词,尽量使用描述性名称。
  5. 一致性: 在整个代码库中保持命名一致性。
package main

import "fmt"

func main() {
    //使用有意义的名称声明变量
    userName := "John Doe"   // CamelCase:以小写字母开头,后面的单词大写。
    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(34)
    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鼓励开发者显式处理错误,原因如下:

  1. 安全性: 错误处理确保意外问题不会导致程序panic或突然崩溃。
  2. 清晰性: 显式错误处理使代码更具可读性,并有助于识别可能发生错误的地方。
  3. 可调试性: 处理错误为调试和故障排除提供了有价值的信息。

我们创建一个简单程序来读取文件并正确处理错误:

package main

import (
 "fmt"
 "os"
)

func main() {
 // Open a file
 file, err := os.Open("example.txt")
 if err != nil {
  // 处理错误
  fmt.Println("Error opening the file:", err)
  return
 }
 defer file.Close() // 结束时关闭文件

 // 读取文件内容
 buffer := make([]byte1024)
 _, 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通过名称向用户表示欢迎。
// Parameters:
//   name (string): 欢迎的用户名
// Returns:
//   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: 使用goroutine处理并发

利用goroutine来高效执行并发操作。在Go语言中,gooutine是轻量级的并发执行线程,能够并发的运行函数,而没有传统线程的开销。从而帮助我们编写高度并发和高效的程序。

我们用一个简单的例子来说明:

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()

 // 继续执行main
 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中Recover,并优雅处理
   fmt.Println("Recovered from panic:", r)
  }
 }()

 // 模拟panic条件
 panic("Oops! Something went wrong.")
}

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

 // 在从panic中恢复的函数中调用有风险的操作
 riskyOperation()

 fmt.Println("End of the program.")
}
#9. 避免使用'init'函数

除非必要,否则避免使用init函数,因为这会使代码更难理解和维护。

更好的方法是将初始化逻辑移到显式调用的常规函数中,通常从main中调用,从而提供了更好的控制,增强了代码可读性,并简化了测试。

下面是一个简单的Go程序,演示了如何避免使用init函数:

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 // Exit the program on error
 }
 defer file.Close() // 确保函数退出时关闭文件

 // 读取并打印文件内容
 data := make([]byte100)
 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: 选择复合字面值而不是构造函数

使用复合字面值来创建struct实例,而不是构造函数。

为什么要使用复合文字?

复合字面值提供了几个优点:

  1. 简洁
  2. 易读
  3. 灵活

我们用一个简单例子来说明:

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字段
 }

 // Printing the person's information
 fmt.Println("Person Details:")
 fmt.Println("First Name:", person.FirstName) // 访问并打印FirstName字段
 fmt.Println("Last Name:", person.LastName)   // 访问并打印LastName字段
 fmt.Println("Age:", person.Age)             // 访问并打印Age字段
}
#6. 最小化功能参数

在Go中,编写干净高效的代码至关重要。实现这一点的一种方法是尽量减少函数参数的数量,从而提高代码的可维护性和可读性。

我们用一个简单例子来说明:

package main

import "fmt"

// Option结构保存配置参数
type Option struct {
    Port    int
    Timeout int
}

// ServerConfig是接受Option结构作为参数的函数
func ServerConfig(opt Option) {
    fmt.Printf("Server configuration - Port: %d, Timeout: %d seconds\n", opt.Port, opt.Timeout)
}

func main() {
    // 创建Option结构并初始化为默认值
    defaultConfig := Option{
        Port:    8080,
        Timeout: 30,
    }

    // 用默认值配置服务
    ServerConfig(defaultConfig)

    // 创建新的Option结构并修改端口
    customConfig := Option{
        Port: 9090,
    }

    // 用自定义端口和默认Timeout配置服务
    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(35)
    fmt.Println("Named Return:", sum1)

    // 显示返回值
    sum2 := explicitReturn(35)
    fmt.Println("Explicit Return:", 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("Sum of %d and %d is %d\n", x, y, sum)
}

func main() {
 // 调用PrintSum函数来演示最小函数复杂度
 PrintSum()
}

在上面的示例程序中:

  1. 我们定义了两个函数, CalculateSumPrintSum,各自具有特定职责。
  2. CalculateSum是一个简单函数,用于计算两个数字的和。
  3. PrintSum调用 CalculateSum计算并打印5和3的和。
  4. 通过保持函数简洁并专注于单个任务,我们保持了较低的函数复杂性,提高了代码的可读性和可维护性。
#3: 避免隐藏变量

当在较窄的范围内声明具有相同名称的新变量时,就会发生变量隐藏,这可能导致意外行为。这种情况下,具有相同名称的外部变量会被隐藏,使其在该作用域中不可访问。尽量避免在嵌套作用域中隐藏变量以防止混淆。

参考如下示例程序:

package main

import "fmt"

func main() {
    // 声明并初始化值为10的外部变量'x'。
    x := 10
    fmt.Println("Outer x:", x)

    // 在内部作用域里用新变量'x'覆盖外部'x'。
    if true {
        x := 5 // 覆盖发生在这里
        fmt.Println("Inner x:", x) // 打印内部'x', 值为5
    }

    // 外部'x'保持不变并仍然可以访问
    fmt.Println("Outer x after inner scope:", x) // 打印外部'x', 值为10
}
#2: 使用接口进行抽象

抽象

抽象是Go中的基本概念,允许我们定义行为而不指定实现细节。

接口

在Go语言中,接口是方法签名的集合。

实现接口的所有方法的任何类型都隐式的满足该接口。

因此只要遵循相同的接口,我们就能够编写可以处理不同类型的代码。

下面是一个用Go语言编写的示例程序,演示了使用接口进行抽象的概念:

package main

import (
    "fmt"
    "math"
)

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

// Rectangle结构
type Rectangle struct {
    Width  float64
    Height float64
}

// Circle结构
type Circle struct {
    Radius float64
}

// 实现Rectangle的Area方法
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// 实现Circle的Area方法
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

// 打印任何形状面积的函数
func PrintArea(s Shape) {
    fmt.Printf("Area: %.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("Message from myutils:", message)
}

main.go:

// 主程序
package main

import (
 "fmt"
 "myproject/myutils" // 导入自定义包
)

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

 // 调用自定义包里的导出函数
 myutils.PrintMessage(message)

 // 演示主程序逻辑
 fmt.Println("Message from main:", message)
}
  1. 在上面的例子中,有两个独立的文件: myutils.gomain.go
  2. myutils.go定义了一个名为 myutils的自定义包,包含输出函数 PrintMessage,用于打印消息。
  3. main.go是使用相对路径( myproject/myutils)导入自定义包 myutils的可执行文件。
  4. main.go中的 main函数从 myutils包中调用 PrintMessage函数并打印一条消息。这种关注点分离保持了代码的组织性和可维护性。

你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

本文由 mdnice 多平台发布

你可能感兴趣的:(程序人生)