十分钟搞懂20个Golang优秀实践

优秀实践是一些不成文的经验总结,遵循最佳实践可以使我们站在前人的肩膀上,避免某些常见错误,写出更好的代码。
首页 新闻资讯 行业资讯 十分钟搞懂20个Golang优秀实践

只需要花上10分钟阅读本文,就可以帮助你更高效编写Go代码。

12cb60f5187c214ab463634e32cc07c9cf0fcd.jpg

20: 使用适当缩进

良好的缩进使代码更具可读性,始终使用制表符或空格(最好是制表符),并遵循Go标准的缩进约定。

packagemainimport"fmt"funcmain(){fori:=0;i<5;i++{fmt.Println("Hello, World!")}}

运行gofmt根据Go标准自动格式化(缩进)代码。

$ gofmt-w your_file.go

19: 正确导入软件包

只导入需要的包,并格式化导入部分,将标准库包、第三方包和自己的包分组。

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

18: 使用描述性变量名和函数名

  • 有意义的名称: 使用能够传达变量用途的名称。

  • 驼峰表示法(CamelCase): 以小写字母开头,后面每个单词的首字母大写。

  • *简短的名称: 简短、简洁的名称对于作用域小、寿命短的变量是可以接受的。

  • 不使用缩写: 避免使用隐晦的缩写和首字母缩略词,尽量使用描述性名称。

  • 一致性: 在整个代码库中保持命名一致性。

packagemainimport"fmt"funcmain(){//使用有意义的名称声明变量userName:="John Doe"// CamelCase:以小写字母开头,后面的单词大写。itemCount:=10// 简短的名称:对于小作用域变量来说,短而简洁。isReady:=true// 不使用缩写:避免使用隐晦的缩写或首字母缩写。// 显示变量值fmt.Println("User Name:",userName)fmt.Println("Item Count:",itemCount)fmt.Println("Is Ready:",isReady)}// 对包级变量使用mixedCasevarexportedVariable int=42// 函数名应该是描述性的funccalculateSumOfNumbers(a,b int)int{returna+b}// 一致性:在整个代码库中保持命名的一致性。

17: 限制每行长度

尽可能将每行代码字符数控制在80个以下,以提高可读性。

packagemainimport("fmt""math")funcmain(){result:=calculateHypotenuse(3,4)fmt.Println("Hypotenuse:",result)}funccalculateHypotenuse(a,b float64)float64{returnmath.Sqrt(a*a+b*b)}

16: 将魔法值定义为常量

避免在代码中使用魔法值。魔法值是硬编码的数字或字符串,分散在代码中,缺乏上下文,很难理解其目的。将魔法值定义为常量,可以使代码更易于维护。

packagemainimport"fmt"const(// 为重试的最大次数定义常量MaxRetries=3// 为默认超时(以秒为单位)定义常量DefaultTimeout=30)funcmain(){retries:=0timeout:=DefaultTimeoutforretries<MaxRetries{fmt.Printf("Attempting operation (Retry %d) with timeout: %d seconds\n",retries+1,timeout)// ... 代码逻辑 ...retries++}}

15. 错误处理

Go鼓励开发者显式处理错误,原因如下:

  • 安全性: 错误处理确保意外问题不会导致程序panic或突然崩溃。

  • 清晰性: 显式错误处理使代码更具可读性,并有助于识别可能发生错误的地方。

  • 可调试性: 处理错误为调试和故障排除提供了有价值的信息。

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

packagemainimport("fmt""os")funcmain(){// Open a filefile,err:=os.Open("example.txt")iferr!=nil{// 处理错误fmt.Println("Error opening the file:",err)return}defer file.Close()// 结束时关闭文件// 读取文件内容buffer:=make([]byte,1024)_,err=file.Read(buffer)iferr!=nil{// 处理错误fmt.Println("Error reading the file:",err)return}// 打印文件内容fmt.Println("File content:",string(buffer))}

14. 避免使用全局变量

尽量减少使用全局变量,全局变量可能导致不可预测的行为,使调试变得困难,并阻碍代码重用,还会在程序的不同部分之间引入不必要的依赖关系。相反,通过函数参数传递数据并返回值。

我们编写一个简单的Go程序来说明避免全局变量的概念:

packagemainimport("fmt")funcmain(){// 在main函数中声明并初始化变量message:="Hello, Go!"// 调用使用局部变量的函数printMessage(message)}// printMessage是带参数的函数funcprintMessage(msg string){fmt.Println(msg)}

13: 使用结构体处理复杂数据

通过结构体将相关的数据字段和方法组合在一起,使代码更有组织性和可读性。

下面是一个完整示例程序,演示了结构体在Go中的应用:

packagemainimport("fmt")// 定义名为Person的结构体来表示人的信息。type Person struct{FirstName string// 名字LastName  string// 姓氏Age       int// 年龄}funcmain(){// 创建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. 对代码进行注释

添加注释来解释代码的功能,特别是对于复杂或不明显的部分。

(1) 单行注释

单行注释以//开头,用来解释特定的代码行。

packagemainimport"fmt"funcmain(){// 单行注释fmt.Println("Hello, World!")// 打印问候语}

(2) 多行注释

多行注释包含在/* */中,用于较长的解释或跨越多行的注释。

packagemainimport"fmt"funcmain(){/*
        多行注释。
        可以跨越多行。
    */fmt.Println("Hello, World!")// 打印问候语}

(3) 函数注释

在函数中添加注释,解释函数的用途、参数和返回值。函数注释使用'godoc'样式。

packagemainimport"fmt"// greetUser通过名称向用户表示欢迎。// Parameters://   name (string): 欢迎的用户名// Returns://   string: 问候语funcgreetUser(name string)string{return"Hello, "+name+"!"}funcmain(){userName:="Alice"greeting:=greetUser(userName)fmt.Println(greeting)}

(4) 包注释

在Go文件顶部添加注释来描述包的用途,使用相同的'godoc'样式。

packagemainimport"fmt"// 这是Go程序的主包。// 包含入口(main)函数。funcmain(){fmt.Println("Hello, World!")}

11: 使用goroutine处理并发

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

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

packagemainimport("fmt""time")// 并发运行的函数funcprintNumbers(){fori:=1;i<=5;i++{fmt.Printf("%d ",i)time.Sleep(100*time.Millisecond)}}// 在主goroutine中运行的函数funcmain(){// 开始goroutinegoprintNumbers()// 继续执行mainfori:=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。

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

packagemainimport"fmt"// 可能会panic的函数funcriskyOperation(){deferfunc(){ifr:=recover();r!=nil{// 从panic中Recover,并优雅处理fmt.Println("Recovered from panic:",r)}}()// 模拟panic条件panic("Oops! Something went wrong.")}funcmain(){fmt.Println("Start of the program.")// 在从panic中恢复的函数中调用有风险的操作riskyOperation()fmt.Println("End of the program.")}

9. 避免使用'init'函数

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

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

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

packagemainimport("fmt")// InitializeConfig初始化配置funcInitializeConfig(){// 初始化配置参数fmt.Println("Initializing configuration...")}// InitializeDatabase初始化数据库连接funcInitializeDatabase(){// 初始化数据库连接fmt.Println("Initializing database...")}funcmain(){// 显示调用初始化函数InitializeConfig()InitializeDatabase()// 主代码逻辑fmt.Println("Main program logic...")}

8: 使用Defer进行资源清理

defer可以将函数的执行延迟到函数返回的时候,通常用于关闭文件、解锁互斥锁或释放其他资源等任务。

这确保了即使在存在错误的情况下也能执行清理操作。

我们创建一个简单的程序,从文件中读取数据,使用defer确保文件被正确关闭,不用管可能发生的任何错误:

packagemainimport("fmt""os")funcmain(){// 打开文件(用实际文件名替换"example.txt")file,err:=os.Open("example.txt")iferr!=nil{fmt.Println("Error opening the file:",err)return// Exit the program on error}defer file.Close()// 确保函数退出时关闭文件// 读取并打印文件内容data:=make([]byte,100)n,err:=file.Read(data)iferr!=nil{fmt.Println("Error reading the file:",err)return// 出错退出程序}fmt.Printf("Read %d bytes: %s\n",n,data[:n])}

7: 选择复合字面值而不是构造函数

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

为什么要使用复合文字?

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

  • 简洁

  • 易读

  • 灵活

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

packagemainimport("fmt")// 定义一个表示人的结构类型type Person struct{FirstName string// 名字LastName  string// 姓氏Age       int// 年龄}funcmain(){// 使用复合字面量创建Person实例person:=Person{FirstName:"John",// 初始化FirstName字段LastName:"Doe",// 初始化LastName字段Age:30,// 初始化Age字段}// Printing the person's informationfmt.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中,编写干净高效的代码至关重要。实现这一点的一种方法是尽量减少函数参数的数量,从而提高代码的可维护性和可读性。

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

packagemainimport"fmt"// Option结构保存配置参数type Option struct{Port    int
    Timeout int}// ServerConfig是接受Option结构作为参数的函数funcServerConfig(opt Option){fmt.Printf("Server configuration - Port: %d, Timeout: %d seconds\n",opt.Port,opt.Timeout)}funcmain(){// 创建Option结构并初始化为默认值defaultConfig:=Option{Port:8080,Timeout:30,}// 用默认值配置服务ServerConfig(defaultConfig)// 创建新的Option结构并修改端口customConfig:=Option{Port:9090,}// 用自定义端口和默认Timeout配置服务ServerConfig(customConfig)}

在这个例子中,我们定义了一个Option结构体来保存服务器配置参数。我们没有向ServerConfig函数传递多个参数,而是使用Option结构体,这使得代码更易于维护和扩展。这种方法在具有大量配置参数的函数时特别有用。

5: 清晰起见,使用显式返回值而不是命名返回值

命名返回值在Go中很常用,但有时会使代码不那么清晰,特别是在较大的代码库中。

我们用一个简单例子来看看它们的区别。

packagemainimport"fmt"// namedReturn演示命名返回值。funcnamedReturn(x,y int)(result int){result=x+yreturn}// explicitReturn演示显式返回值。funcexplicitReturn(x,y int)int{returnx+y}funcmain(){// 命名返回值sum1:=namedReturn(3,5)fmt.Println("Named Return:",sum1)// 显示返回值sum2:=explicitReturn(3,5)fmt.Println("Explicit Return:",sum2)}

上面的示例程序中有两个函数,namedReturn和explicitReturn,以下是它们的不同之处:

  • namedReturn使用命名返回值result。虽然函数返回的内容很清楚,但在更复杂的函数中可能不会很明显。

  • explicitReturn直接返回结果,这样更简单、明确。

4: 将函数复杂性保持在最低限度

函数复杂性是指函数代码的复杂程度、嵌套程度和分支程度。保持较低的函数复杂度会使代码更具可读性、可维护性,并且不易出错。

我们用一个简单的例子来探索这个概念:

packagemainimport("fmt")// CalculateSum返回两个数字的和funcCalculateSum(a,b int)int{returna+b}// PrintSum打印两个数字的和funcPrintSum(){x:=5y:=3sum:=CalculateSum(x,y)fmt.Printf("Sum of %d and %d is %d\n",x,y,sum)}funcmain(){// 调用PrintSum函数来演示最小函数复杂度PrintSum()}

在上面的示例程序中:

  • 我们定义了两个函数,CalculateSum和PrintSum,各自具有特定职责。

  • CalculateSum是一个简单函数,用于计算两个数字的和。

  • PrintSum调用CalculateSum计算并打印5和3的和。

  • 通过保持函数简洁并专注于单个任务,我们保持了较低的函数复杂性,提高了代码的可读性和可维护性。

3: 避免隐藏变量

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

参考如下示例程序:

packagemainimport"fmt"funcmain(){// 声明并初始化值为10的外部变量'x'。x:=10fmt.Println("Outer x:",x)// 在内部作用域里用新变量'x'覆盖外部'x'。iftrue{x:=5// 覆盖发生在这里fmt.Println("Inner x:",x)// 打印内部'x', 值为5}// 外部'x'保持不变并仍然可以访问fmt.Println("Outer x after inner scope:",x)// 打印外部'x', 值为10}

2: 使用接口进行抽象

(1) 抽象

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

(2) 接口

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

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

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

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

packagemainimport("fmt""math")// 定义Shape接口type Shapeinterface{Area()float64}// Rectangle结构type Rectangle struct{Width  float64
    Height float64}// Circle结构type Circle struct{Radius float64}// 实现Rectangle的Area方法func(r Rectangle)Area()float64{returnr.Width*r.Height}// 实现Circle的Area方法func(c Circle)Area()float64{returnmath.Pi*c.Radius*c.Radius}// 打印任何形状面积的函数funcPrintArea(s Shape){fmt.Printf("Area: %.2f\n",s.Area())}funcmain(){rectangle:=Rectangle{Width:5,Height:3}circle:=Circle{Radius:2.5}// 对矩形和圆形调用PrintArea,因为都实现了Shape接口PrintArea(rectangle)// 打印矩形面积PrintArea(circle)// 打印圆形面积}

在这个程序中,我们定义了Shape接口,创建了两个结构体Rectangle和Circle,每个结构体都实现了Area()方法,并使用PrintArea函数打印满足Shape接口的任何形状的面积。

这演示了如何在Go中使用接口进行抽象,从而使用公共接口处理不同的类型。

1: 避免混合库包和可执行文件

在Go中,在包和可执行文件之间保持清晰的隔离以确保代码干净和可维护性至关重要。

下面是演示库和可执行文件分离的示例项目结构:

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

myutils/myutils.go:

// 包声明——为实用程序函数创建单独的包packagemyutilsimport"fmt"// 导出打印消息的函数funcPrintMessage(message string){fmt.Println("Message from myutils:",message)}

main.go:

// 主程序packagemainimport("fmt""myproject/myutils"// 导入自定义包)funcmain(){message:="Hello, Golang!"// 调用自定义包里的导出函数myutils.PrintMessage(message)// 演示主程序逻辑fmt.Println("Message from main:",message)}

在上面的例子中,有两个独立的文件: myutils.go和main.go。

  • myutils.go定义了一个名为myutils的自定义包,包含输出函数PrintMessage,用于打印消息。

  • main.go是使用相对路径(myproject/myutils)导入自定义包myutils的可执行文件。

  • main.go中的main函数从myutils包中调用PrintMessage函数并打印一条消息。这种关注点分离保持了代码的组织性和可维护性。

参考资料:

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

15    2024-01-29 00:20:00    Golang Go 代码